Issuu on Google+


Piensa


Piensa en Java Bruce Eckel Traducción: Jorge González Barturen Facultad de Ingeniería Universidad de Deusto

Revisión técnica: Javier Parra Fuente Ricardo Lozano Quesada Departamento de Lenguajes y Sistemas Informáticos e Ingeniería de Software Universidad Pontijicia de Salamanca en Madrid

Coordinación general y revisión técnica: Luis Joyanes Aguilar Departamento de Lenguajes y Sistemas Informáticos e Ingeniería de Software Universidad Pontificia de Salamanca en Madrid

Madrid México Santafé de Bogotá Buenos Aires Caracas Lima Montevideo San Juan San José Santiago Sao Paulo White Plains


atos de catalogación bibliográfica

i Bruce Eckel

PIENSA EN JAVA Segunda edición PEARSON EDUCACIÓN, S.A. Madrid, 2002 ISBN: 84-205-3 192-8 Materia: Informática 68 1.3 Formato 195 x 250

Páginas: 960

No está permitida la reproducción total o parcial de esta obra ni su tratamiento o transmisión por cualquier medio o método sin autorización escrita de la Editorial. DERECHOS RESERVADOS O 2002 respecto a la segunda edición en español por: PEARSON E D U C A C I ~ NS.A. , Núñez de Balboa, 120 28006 Madrid

Bruce Eckel PIENSA EN JAVA, segunda edición. ISBN: 84-205-3192-8 Depósito Legal: M.4.162-2003 Última reimpresión, 2003 PRENTICE HALL es un sello editorial autorizado de PEARSON EDUCACIÓN, S.A. Traducido de: Thinking in JAVA, Second Edition by Bruce Eckel. Copyright O 2000, Al1 Rights Reserved. Published by arrangement with the original publisher, PRENTICE HALL, INC., a Pearson Education Company. ISBN: 0- 13-027363-5 Edición en espuñol: Equipo editorial: Editor: Andrés Otero Asistente editorial: Ana Isabel García Equipo de producción: Director: José A. Clares Técnico: Diego Marín Diseño de cubierta: Mario Guindel, Lía Sáenz y Begoña Pérez Compo\ición: COMPOMAR. S.L. Impreso por: LAVEL, S. A . IMPRESO EN ESPANA - PRINTED IN SPAIN

Este libro ha sido impreso con papel y tintas ecológicos


A la persona que, incluso en este momento, est谩 creando el pr贸ximo gran lenguaje de programaci贸n.


@

Indice de contenido

Prólogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Prólogo a la 2." edición

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxiii

Java2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

ElCDROM

xxi xxiv

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxv

Prólogo a la edición en español . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxvii El libro como referencia obligada a Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxvii El libro como formación integral de programador . . . . . . . . . . . . . . . . . . . . . . . xxvii Recursos gratuitos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxviii Unas palabras todavía más elogiosas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxviii

Comentarios de los lectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxix Introducción . Prerrequisitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxxv AprendiendoJava . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .xxxvi Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .xxxvi Documentación en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .xxxvii Capítulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxxviii . ... Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xlii CD ROM Multimedia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xliii Códigofuente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xliii

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xlv Versiones de Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xlv Seminarios y mi papel como mentor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xlvi Estándares de codificación

Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nota sobre el diseño de la portada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Agradecimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Colaboradores Internet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

xlvi xlvi xlvii xlix


vi¡¡

Piensa en Java

1:Introducción a los objetos

...................................

El progreso de la abstracción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Todo objeto tiene una interfaz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La implementación oculta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reutilizar la implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herencia: reutilizar la interfaz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La relación es-un frente a la relación es-como-un . . . . . . . . . . . . . . . . . . . . . . . . Objetos intercambiables con polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . Clases base abstractas e interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Localización de objetos y longevidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Colecciones e iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La jerarquía de raíz única . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bibliotecas de colecciones y soporte al fácil manejo de colecciones . . . . . . . . . El dilema de las labores del hogar: ¿quién limpia la casa? . . . . . . . . . . . . . . . . . Manejo de excepciones: tratar con errores . . . . . . . . . . . . . . . . . . . . . . . . . . Multihilo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Persistencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JavaeInternet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . lQuéeslaWeb? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programación en el lado del cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programación en el lado del servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un ruedo separado: las aplicaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Análisisydiseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase O: Elaborar un plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase 1: ¿Qué estamos construyendo? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase 2: ¿Cómo construirlo? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase 3: Construir el núcleo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase 4: Iterar los casos de uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase5:Evolución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los planes merecen la pena . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Programación extrema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Escritura de las pruebas en primer lugar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programación a pares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

PorquéJavatieneéxito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los sistemas son más fáciles de expresar y entender . . . . . . . . . . . . . . . . . . . . Ventajas máximas con las bibliotecas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Manejo de errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programación a lo grande . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Estrategias para la transición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Guías . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Obstáculosdegestión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . davafrenteaC++? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


índice de contenido

2:Todoesunobjeto

.........................................

Los objetos se manipulan mediante referencias . . . . . . . . . . . . . . . . . . . . . . Uno debe crear todos los objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dónde reside el almacenamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un caso especial: los tipos primitivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ArraysenJava . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nunca e s necesario destruir un objeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ámbito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ámbito de los objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Crear nuevos tipos d e datos: clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Camposymétodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Métodos. parámetros y valores d e retorno . . . . . . . . . . . . . . . . . . . . . . . . . . La lista de parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Construcción de un programa Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Visibilidad de los nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilización de otros componentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La palabra clave static . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tu primer programa Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compilación y ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comentarios y documentación empotrada . . . . . . . . . . . . . . . . . . . . . . . . . . Documentación en forma de comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sintaxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HTMLempotrado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . asee: referencias a otras clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Etiquetas de documentación de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Etiquetas de documentación de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Etiquetas de documentación de métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejemplo de documentación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Estilodecod~cación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3: Controlar el flujo del programa

...............................

Utilizar operadores d e Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Precedencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operadores matemáticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Autoincremento y Autodecremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operadores relacionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operadores lógicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operadoresdebit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operadores de desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operador ternario if-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eloperadorcoma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


x

Piensa en Java

EloperadordeString+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pequeños fallos frecuentes al usar operadores . . . . . . . . . . . . . . . . . . . . . . . . . . Operadores de conversión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java no tiene "sizeof" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Volver a hablar acerca de la precedencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un compendio de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Control de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Trueyfalse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . If-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . return . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Iteración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . do-while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . break y continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resuiiieil

...................................................

Ejercicios

...................................................

4: Inicialización y limpieza

.....................................

Inicialización garantizada con el constructor . . . . . . . . . . . . . . . . . . . . . . . . Sobrecargademétodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Distinguir métodos sobrecargados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sobrecarga con tipos primitivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sobrecarga en los valores de retorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Constructores por defecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

La palabra clave this . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Limpieza: finalización y recolección de basura . . . . . . . . . . . . . . . . . . . . . . . ¿Para qué sirve finalize( ) ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hay que llevar a cabo la limpieza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La condición de muerto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cómo funciona un recolector de basura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicialización de miembros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Especificación de la inicialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicialización de constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicialización de arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arrays multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5: Ocultar la implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El paquete: la unidad de biblioteca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creando nombres de paquete únicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Una biblioteca de herramientas a medida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar el comando import para cambiar el comportamiento . . . . . . . . . . . . . . . Advertencia relativa al uso de paquetes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


índice de contenido

Modificadores de acceso en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "Amistoso" ("Friendly") . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . public: acceso a interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private: jeso no se toca! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protected: "un tipo de amistad" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaz e implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Acceso a clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

6: Reutilizando clases

........................................

Sintaxis de la composición

......................................

Sintaxis de la herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicializando la clase base

.........................................

Combinando la composición y la herencia . . . . . . . . . . . . . . . . . . . . . . . . . . Garantizar una buena limpieza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ocultación de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elcción entre composición y herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Protegido (protected) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Desaerrollo incremental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conversión hacia arriba . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¿Por qué "conversión hacia arriba"? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lapalabraclavefinal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Paradatos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Métodosconstante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Precaución con constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Carga de clases e inicialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicialización con herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . De nuevo la conversión hacia arriba . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Olvidando el tipo de objeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elcambio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La ligadura en las llamadas a métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Produciendo el comportamiento adecuado . . . . . . . . . . . . . . . . . . . . . . . . . . . . Extensibilidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Superposición frente a sobrecarga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases y métodos abstractos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases y métodos abstractos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Orden de llamadas a constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herencia y finahe( ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comportamiento de métodos polimórficos dentro de constructores . . . . . . . . .

xi


xii

Piensa en Java

Diseñoconherencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herencia pura frente a extensión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conversión hacia abajo e identificación de tipos en tiempo de ejecución . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

8: Interfaces y clases internas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "Herencia múltiple" en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Extender una interfaz con herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Constantes de agrupamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Iniciando atributos en interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaces anidados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases internas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases internas y conversiones hacia arriba . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ámbitos y clases internas en métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases internas anónimas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El enlace con la clase externa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases internas estáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Referirse al objeto de la clase externa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Acceso desde una clase múltiplemente anidada . . . . . . . . . . . . . . . . . . . . . . . . . Heredar de clases internas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¿Pueden superponerse las clases internas? . . . . . . . . . . . . . . . . . . . . . . . . . . . . Identificadores de clases internas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¿Por qué clases internas? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases internas y sistema de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

9: Guardar objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los arrays son objetos d e primera clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Devolverunarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . LaclaseArrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rellenarunarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Copiarunarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comparar arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comparaciones de elementos de arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ordenar u n array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Buscar en un array ordenado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen de arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducción a los contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Visualizar contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rellenar contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Desventaja d e los contenedores: tipo desconocido . . . . . . . . . . . . . . . . . . . . . .


índice de contenido

En ocasiones funciona de cualquier modo . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Hacer un ArrayList consciente de los tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Taxonomía de contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funcionalidad de la Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funcionalidad del interfaz List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Construir una pila a partir de un objeto LinkedList . . . . . . . . . . . . . . . . . . . . Construir una cola a partir de un objeto LinkedList . . . . . . . . . . . . . . . . . . . . . Funcionalidad de la interfaz Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conjunto ordenado (SortedSet) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funcionalidad Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mapa ordenado (Sorted Map) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hashing y códigos de hash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Superponer el método hashCode( ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Guardar referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El nhjetn HasMap dkhil (WeakHashMa~). . . . . . . . . . . . . . . . . . . . . . . . . . . . Revisitando los iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elegir una implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elegir entre Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elegir entre Conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elegir entre Mapas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ordenar y buscar elementos en Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hacer inmodificable una Colección o un Mapa . . . . . . . . . . . . . . . . . . . . . . . . Sincronizar una Colección o Mapa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operaciones no soportadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Contenedores de Java 1.0/1.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vector y enumeration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hashtable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pila(Stack) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conjunto de bits (BitSet) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

10: Manejo de errores con excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . Excepciones básicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parámetros de las excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ............................................ Capturarunaexcepcion Elbloquetry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.

Manejadores de excepciones

.......................................

Crear sus propias excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La especificación de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Capturar cualquier excepción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Relanzarunaexcepción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

ExcepcionesestándardeJava . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

xiii


xiv

Piensa en Java

El caso especial de RuntimeException . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Limpiando con finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ~Paraquésirvefinally? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Peligro: la excepción perdida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Restricciones a las excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Emparejamiento de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Guías de cara a las excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

11: El sistema de E/S de Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La clase File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un generador de listados de directorio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Comprobando y creando directorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Entradaysalida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TiposdeInputStream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TiposdeOutputStream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Añadir atributos e interfaces útiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Leer de un InputStream con un FilterInputStream . . . . . . . . . . . . . . . . . . . . Escribir en un OutputStream con FilterOutputStream . . . . . . . . . . . . . . . . .

Readers & Writers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fuentes y consumidores de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modificar el comportamiento del flujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases no cambiadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Por sí mismo: RandomAccessFile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Usos típicos de flujos de E/S . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flujosdeentrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flujosdesalida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¿Unerror? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flujosentubados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . E/Sestándar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Leerdelaentradaestándar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Convirtiendo System.out en un PrintWriter . . . . . . . . . . . . . . . . . . . . . . . . . . RedingiendolaE/Sestándar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compresión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compresión sencilla con GZIP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Almacenamiento múltiple con ZIP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ARchivos Java UAR) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Serialización de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Encontrarlaclase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Controlar la serialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar la persistencia

............................................

Identificar símbolos de una entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . StreamTokenizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


índice de contenido

StringTokenizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comprobar el estilo de escritura de mayúsculas . . . . . . . . . . . . . . . . . . . . . . . .

Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

12: Identificación de tipos en tiempo de ejecución . . . . . . . . . . . . . . . . . . . La necesidad de RTTI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ElobjetoClass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comprobar antes de una conversión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sintaxis RTTI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reflectividad: información de clases en tiempo de ejecución . . . . . . . . . . . . . . Un extractor de métodos de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resuinen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

13: Crear ventanas y applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El applet básico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Restricciones de applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ventajas de los applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Marcos de trabajo de aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejecutar applets dentro de un navegador web . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar Appletviewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Probarapplets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Ejecutar applets desde la línea de comandos . . . . . . . . . . . . . . . . . . . . . . . . . . . Un marco de trabajo de visualización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Usar el Explorador de Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Hacer un botón . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Capturarunevento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Áreas de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Controlar la disposición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Borderhyout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flowhyout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gridhyout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . GridBagLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Posicionamiento absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Boxhyout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¿Elmejorenfoque? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

El modelo de eventos de Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tipos de eventos y oyentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Seguimiento de múltiples eventos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Un catálogo de componentes Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Botones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Iconos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Etiquetas de aviso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Camposdetexto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

xv


xvi

Piensa en Java

Bordes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JScrollPanes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unminieditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Casillas de verificación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Botonesdeopción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Combo boxes (listas desplegables) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PanelesTabulados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cajasdemensajes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Menús . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Menúsemergentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generacióndedibujos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cajasdediálogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Diálogos de archivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HTMLencomponentesSwing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Deslizadores y barras de progreso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Árboles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tablas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Seleccionar Apariencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elportapapeles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Empaquetando un applet en un fichero JAR . . . . . . . . . . . . . . . . . . . . . . . . . . . Técnicas de programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Correspondencia dinámica de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Separar la lógica de negocio de la lógica IU . . . . . . . . . . . . . . . . . . . . . . . . . . . Una forma canónica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programación visual y Beans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ~ Q u é e s u n B e a n ?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Extraer BeanInfo con el Introspector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un Bean más sofisticado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EmpaquetarunBean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Soporte a Beans más complejo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MássobreBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

14: Hilos múltiples

..........................................

Interfaces de respuesta de usuario rápida . . . . . . . . . . . . . . . . . . . . . . . . . . HeredardeThread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hilos para una interfaz con respuesta rápida . . . . . . . . . . . . . . . . . . . . . . . . . . . Combinar el hilo con la clase principal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Construir muchos hilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hilosdemonio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compartir recursos limitados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Acceder a los recursos de forma inadecuada . . . . . . . . . . . . . . . . . . . . . . . . . . . Cómo comparte Java los recursos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


índice de contenido

Revisar los JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bloqueo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bloqueándose . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interbloqueo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Prioridades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Leer y establecer prioridades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gruposdehilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Volver a visitar Runnable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Demasiados hilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

15: Computación distribuida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programación en red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Identificar una máquina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Servir a múltiples clientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datagramas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar URL en un applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Más aspectos de redes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Conectividad a Bases de Datos de Java (JDBC) . . . . . . . . . . . . . . . . . . . . . . . . Hacer que el ejemplo funcione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Una versión con IGU del programa de búsqueda . . . . . . . . . . . . . . . . . . . . . . . Por qué el API JDBC parece tan complejo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un ejemplo más sofisticado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Servlets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El servlet básico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Servlets y multihilo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gestionar sesiones con servlets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejecutar los ejemplos de servlets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java Server Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objetos implícitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Directivas JSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elementos de escritura de guiones JSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Extraer campos y valores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Atributos JSP de página y su ámbito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Manipular sesiones en JSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Crear y modificar cookies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ResumendeJSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

RMI (Invocation Remote Method) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaces remotos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementar la interfaz remota . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Crearstubsyskeletons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar el objeto remoto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

CORBA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

xvii


xviii

Piensa en Java

FundamentosdeCORBA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Applets de Java y CORBA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CORBAfrenteaRMI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Enterprise JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JavaBeans frente a EJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La especificación EJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ComponentesEJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Las partes de un componente EJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funcionamiento de un EJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TiposdeEJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Desarrollar un EJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ResumendeEJB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Jini: servicios distribuidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

773 775 780 780 780 781 782 783 784 785 785 786 791 791

Jini en contcxto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

791

¿Qué es Jini? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cómo funciona Jini . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

792 792 793 793 794 795 796 796 796

El proceso de discovery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El proceso join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El proceso lookup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Separación de interfaz e implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abstraer sistemas distribuidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

A: Paso y Retorno de Objetos . . . . . . . . . . Pasando referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Usodealias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Haciendo copias locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pasoporvalor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clonandoobjetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Añadiendo a una clase la capacidad de ser clonable . . . . . . . . . . . . . . . . . . . . . Clonación con éxito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El efecto de Object.clone( ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clonando un objeto compuesto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Una copia en profundidad con ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Copia en profundidad vía serialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Añadiendo "clonabilidad" a lo largo de toda una jerarquía . . . . . . . . . . . . . . . . . {Por qué un diseño tan extraño? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Controlando la "clonabilidad" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elconstructordecopias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Clases de sólo lectura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creando clases de sólo lectura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los inconvenientes de la inmutabilidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strings inmutables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


índice de contenido

Las clases String y StringBuffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los Strings son especiales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

B . El Interfaz Nativo Java (JNI1) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Invocando a un método nativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El generador de cabeceras de archivo: javah . . . . . . . . . . . . . . . . . . . . . . . . . . . renombrado de nombres y signaturas de funciones . . . . . . . . . . . . . . . . . . . . . . Implementando la DLL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Accediendo a funciones JNI: el parámetro JNIEnv . . . . . . . . . . . . . . . . . . . . . Accediendo a Strings Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pasando y usando objetos Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

JNI y las excepciones Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JNIyloshilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Usando un código base preexistente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Información adicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

C: Guías de programación Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Diseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implemenentación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

D: Recursos Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Libros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Análisis y Diseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mi propia lista d e libros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

E: Correspondencias español-inglés de clases. bases de datos. tablas y campos del CD ROM que acompaña al libro . . . . . . . . . . . . . . . . . . .

xix


Prólogo Sugerí a mi hermano Todd, que está dando el salto del hardware a la programación, que la siguiente gran revolución será en ingeniería genética. Tendremos microbios diseñados para hacer comida, combustible y plástico; limpiarán la polución y en general, nos permitirán dominar la manipulación del mundo físico por una fracción de lo que cuesta ahora. De hecho yo afirmé que la revolución de los computadores parecería pequeña en com-

paración. Después, me di cuenta de que estaba cometiendo un error frecuente en los escritores de ciencia fic-

ción: perderme en la tecnología (lo que por supuesto es fácil de hacer en ciencia ficción). Un escritor experimentado sabe que la historia nunca tiene que ver con los elementos, sino con la gente. La genética tendrá un gran impacto en nuestras vidas, pero no estoy seguro de que haga sombra a la revolución de los computadores (que hace posible la revolución genética) -o al menos la revolución de la información. La información hace referencia a comunicarse con otros: sí, los coches, los zapatos y especialmente la terapia genética son importantes, pero al final, ésto no son más que adornos. Lo que verdaderamente importa es cómo nos relacionamos con el mundo. Y cuánto de eso es comunicación. Este libro es un caso. La mayoría de colegas pensaban que estaba un poco loco al poner todo en la Web. "¿Por qué lo compraría alguien?", se preguntaban. Si hubiera sido de naturaleza más conservadora no lo habría hecho, pero lo que verdaderamente no quería era escribir más libros de computación al estilo tradicional. No sabía qué pasaría pero resultó que fue una de las cosas más inteligentes que he hecho con un libro. Por algún motivo, la gente empezó a mandar correcciones. Éste ha sido un proceso divertido, porque todo el mundo ha recorrido el libro y ha detectado tanto los errores técnicos como los gramaticales, y he podido eliminar fallos de todos los tipos que de otra forma se habrían quedado ahí. La gente ha sido bastante amable con ésto, diciendo a menudo " yo no quiero decir esto por criticar...", y tras darme una colección de errores estoy seguro de que de otra forma nunca los hubiera encontrado. Siento que éste ha sido un tipo de grupo de procesos que ha convertido el libro en algo especial. Pero cuando empecé a oír: "De acuerdo, bien, está bien que hayas puesto una versión electrónica, pero quiero una copia impresa proveniente de una auténtica editorial", puse mi mayor empeño en facilitar que todo se imprimiera con formato adecuado, pero eso no frenó la demanda de una versión publicada. La mayoría de la gente no quiere leer todo el libro en pantalla, y merodear por un conjunto de papeles, sin que importe cuán bien impresos estén, simplemente no era suficiente. (Además, tampoco creo que resulte tan barato en términos de tóner para impresora láser.) Parece que a fin de cuentas, la revolución de los computadores no conseguirá dejar sin trabajo a las editoriales. Sin embargo, un alumno me sugirió que éste podría ser un modelo para publicaciones finales: los libros se publicarán primero en la Web, y sólo si hay el suficiente interés, merecerá la pena pasar el libro a papel. Actualmente, la gran mayoría de libros conllevan problemas financieros, y quizás este nuevo enfoque pueda hacer que el negocio de la publicación sea más beneficioso. Este li-


xxii

Piensa en Java

bro se convirtió en una experiencia reveladora para mí de otra forma. Originalmente me acerqué a Java como "simplemente a otro lenguaje de programación", lo que en cierto sentido es verdad. Pero a medida que pasaba el tiempo y lo estudiaba más en profundidad, empecé a ver que la intención fundamental de este lenguaje es distinta de la de otros lenguajes que he visto.

La programación está relacionada con gestionar la complejidad: la complejidad del problema que se quiere solucionar, que yace sobre la complejidad de la máquina en que se soluciona. Debido a esta complejidad, la mayoría de nuestros proyectos fallan. Y lo que es más, de todos los lenguajes de programación de los que soy consciente, ninguno se ha lanzado completamente decidiendo que la meta de diseño principal fuera conquistar la complejidad del desarrollo y mantenimiento de programas1. Por supuesto, muchas decisiones de diseño de lenguajes se hicieron sin tener en mente la complejidad, pero en algún punto había siempre algún otro enfoque que se consideraba esencial añadirlo al conjunto. Inevitablemente, estos otros aspectos son los que hacen que generalmente los programadores "se den con la pared" contra ese lenguaje. Por ejemplo, C++ tenía que ser compatible con C (para permitir la migración fácil a los programadores de C), además de eficiente. Estas metas son

ambas muy útiles y aportan mucho al éxito de C t t , pero también exponen complejidad extra que evita que los proyectos se acaben (ciertamente, se puede echar la culpa a los programadores y la gestión, pero si un lenguaje puede ayudar 'a capturar los errores, ¿por qué no hacer uso de ello?). Como otro ejemplo, Visual Basic (VB) estaba atado a BASIC, que no estaba diseñado verdaderamente para ser un lenguaje ampliable, por lo que todas las aplicaciones que se apilaban sobre VB producían sintaxis verdaderamente horribles e inmantenibles. Perl es retrocompatible con Awk, Sed, Grep y otras herramientas Unix a las que iba a reemplazar, y como resultado se le acusa a menudo, de producir "código de sólo escritura" (es decir, código que tras unos pocos meses no hay quien lea). Por otro lado, C++,VB, Perl, y otros lenguajes como Smalltalk han visto cómo algunos de sus esfuerzos de diseño se centraban en el aspecto de la complejidad y como resultado son remarcadamente exitosos para solucionar ciertos tipos de problemas.

Lo que más me impresionó es que he llegado a entender que Java parece tener el objetivo de reducir la complejidad para el programador. Como si se dijera "no nos importa nada más que reducir el tiempo y la dificultad para producir un código robusto". En los primeros tiempos, esta meta llevaba a un código que no se ejecutaba muy rápido (aunque se habían hecho promesas sobre lo rápido que se ejecutaría Java algún día), pero sin duda ha producido reducciones sorprendentes de tiempo de desarrollo; la mitad o menos del tiempo que lleva crear un programa C++equivalente. Este resultado sólo puede ahorrar cantidades increíbles de tiempo y dinero, pero Java no se detiene ahí. Envuelve todas las tareas complejas que se han convertido en importantes, como el multihilo y la programación en red, en bibliotecas o aspectos del lenguaje que en ocasiones pueden convertir esas tareas en triviales. Y finalmente, asume muchos problemas de complejidad grandes: programas multiplataforma, cambios dinámicos de código, e incluso seguridad, cada uno de los cuales pueden encajar dentro de un espectro de complejidades que oscila en el rango de "impedimento" a "motivos de cancelación". Por tanto, a pesar de los problemas de rendimiento que se han visto, la promesa de Java es tremenda: puede convertirnos en programadores significativamente más productivos. Uno de los sitios en los que veo el mayor impacto de esto es en la Web. La programación en red siempre ha sido complicada, y Java la convierte en fácil Or los diseñadores el lenguaje Java están Esto lo retomo de la 2." edición: creo que el lenguaje Python se acerca aun más a esto. Ver http://www.Python.org.


Prefacio

xxiii

trabajando en facilitarla aún más). La programación en red es como hablar simultáneamente de forma efectiva y de forma más barata de lo que nunca se logró con teléfonos (sólo el correo electrónico ya ha revolucionado muchos negocios). Al intercomunicarnos más, empiezan a pasar cosas divertidas, probablemente mucho más interesantes que las que pasarán con la ingeniería genética. De todas formas -al crear los programas, trabajar para crear programas, construir interfaces para los programas, de forma que éstos se puedan comunicar con el usuario, ejecutar los programas en distintos tipos de máquinas, y escribir de forma sencilla programas que pueden comunicarse a través de Internet- Java incrementa el ancho de banda de comunicación entre la gente. Creo que quizás los resultados de la revolución de la comunicación no se contemplarán por lo que conlleva el transporte de grandes cantidades de bits; veremos la auténtica revolución porque podremos comunicarnos con mayor facilidad: de uno en uno, pero también en grupos y, como planeta. He oído la sugerencia de que la próxima revolución es la formación de cierto tipo de mente global para suficiente gente y suficiente nivel de interconectividad. Puede decirse que Java puede fomentar o no esa revolución, pero al menos la mera posibilidad me ha hecho sentir como si estuviera haciendo algo lleno de sentido al intentar ensenar ese lenguaje.

Prólogo a la 2.a edición La gente ha hecho muchos, muchos comentarios maravillosos sobre la primera edición de este libro, cosa que ha sido para mí muy, pero que muy, placentero. Sin embargo, en todo momento habrá quien tenga quejas, y por alguna razón una queja que suele aparecer periódicamente es que "el libro es demasiado grande". Para mí, esto no es verdaderamente una queja, si se reduce a que "tiene demasiadas páginas". (Uno se acuerda de las quejas del Emperador de Austria sobre el trabajo de Mozart: "¡Demasiadas páginas!", y no es que me esté intentando comparar con Mozart de ninguna forma). Además, sólo puedo asumir que semejante queja puede provenir de gente que no tiene aún una idea clara de la vasta extensión del propio lenguaje Java en sí, y que no ha visto el resto de libros sobre la materia -por ejemplo, mi referencia favorita es el Core Java de Cay Horstmann & Cary Cornell (Prentice-Hall), que creció tanto que hubo que dividirlo en dos tomos. A pesar de esto, una de las cosas que he intentado hacer en esta edición es eliminar las portes que se han vuelto obsoletas o al menos no esenciales. Me siento a gusto haciendo esto porque el material original sigue en la Web y en el CD ROM que acompaña al libro, en la misma forma de descarga gratuita que la primera edición del libro (en http: / / www.BruceEckel.com). Si se desea el material antiguo, sigue ahí, y esto es algo maravilloso para un autor. Por ejemplo, puede verse que el último capítulo original, "Proyectos", ya no está aquí; dos de los proyectos se han integrado en los otros capítulos, y el resto ya no son adecuados. También el capítulo de "Patrones de diseño" se volvió demasiado extenso y ha sido trasladado a un libro que versa sobre ellos (descargable también en el sitio web). Por tanto, el libro debería ser más fino. Pero no lo es. El aspecto mayor es el continuo desarrollo del lenguaje Java en sí, y en particular las API que se expanden, y prometen proporcionar interfaces estándar para casi todo lo que se desee hacer (y no me sorprendería ver aparecer la API "JTostadora"). Cubrir todas estas API se escapa por supuesto del ámbito de este libro, y es una tarea relegada a otros autores, pero algunos aspectos no pueden ig-


xxiv

Piensa en Java

norarse. El mayor de éstos incluye el Java de lado servidor (principalmente Servlets & Java Server Pages o JSP), que es verdaderamente una solución excelente al problema de la World Wide Web, donde se descubrió que las distintas plataformas de navegadores web no son lo suficientemente consistentes como para soportar programación en el lado cliente. Además, está todo el problema de crear de forma sencilla aplicaciones que interactúen de forma sencilla con bases de datos, transacciones, seguridad y semejante, cubiertos gracias a los Enterprise Java Beans (EJB). Estos temas están desarrollados en el capítulo que antes se llamaba "Programación de red" y ahora "Computación distribuida", un tema que se está convirtiendo en esencial para todo el mundo. También se verá que se ha compilado este capítulo para incluir un repaso de Jini (pronunciado "yeni", y que no es un acrónimo, sino sólo un nombre), que es una tecnología emergente que permite que cambiemos la forma de pensar sobre las aplicaciones interconectadas. Y por supuesto, el libro se ha cambiado para usar la biblioteca IGU Swing a lo largo de todo el mismo. De nuevo, si se desea el material Java 1.0/1.1 antiguo, e s posible conseguirlo gratuitamente del libro de descarga gratuita de http:llwww.BruceEckel.corn (también está incluido en el nuevo CD ROM de esta edición, que se adjunta al mismo; hablaré más de él un poco más adelante). Aparte de nuevas características del lenguaje añadidas a Java 2, y varias correcciones hechas a lo largo de todo el libro, el otro cambio principal está en el capítulo de colecciones que ahora se centra en las colecciones de Java 2, que se usan a lo largo de todo el libro. También he mejorado ese capítulo para que entre más en profundidad en algunos aspectos importantes de las colecciones, en particular, en cómo funcionan las funciones de hashing (de forma que se puede saber cómo crear una adecuadamente). Ha habido otros movimientos y cambios, incluida la reescritura del Capítulo 1, y la eliminación de algunos apéndices y de otros materiales que ya no consideraba necesarios para el libro impreso, que son un montón de ellos. En general, he intentado recorrer todo, eliminar de la 2." edición lo que ya no es necesario (pero que sigue existiendo en la primera edición electrónica), incluir cambios y mejorar todo lo que he podido. A medida que el lenguaje continúa cambiando -aunque no a un ritmo tan frenético como antiguamente- no cabe duda de que habrá más ediciones de este libro. Para aquellos de vosotros que siguen sin poder soportar el tamaño del libro, pido perdón. Lo creáis o no, he trabajado duro para que se mantenga lo menos posible. A pesar de todo, creo que hay bastantes alternativas que pueden satisfacer a todo el mundo. Además, el libro está disponible electrónicamente (en idioma inglés desde el sitio web, y desde el CD ROM que acompaña al libro), por lo que si se dispone de un ordenador de bolsillo, se puede disponer del libro sin tener que cargar un gran peso. Si sigue interesado en tamaños menores, ya existen de hecho versiones del libro para Palm Pilot. (Alguien me dijo en una ocasión que leería el libro en la cama en su Palm, con la luz encendida a la espalda para no molestar a su mujer. Sólo espero que le ayude a entrar en el mundo de los sueños.) Si se necesita en papel, sé de gente que lo va imprimiendo capítulo a capítulo y se lo lee en el tren.

Java 2 En el momento de escribir el libro, es inminente el lanzamiento del Java Development Kit UDK) 1.3

de Sun, y ya se ha publicado los cambios propuestos para JDK 1.4. Aunque estos números de versión se corresponden aún con los "unos", la forma estándar de referenciar a las versiones posterio-


Prefacio

xxv

res a la JDK 1.2 es llamarles "Java 2". Esto indica que hubo cambios muy significativos entre el "viejo Java" -que tenía muchas pegas de las que ya me quejé en la primera edición de este libro- y esta nueva versión más moderna y mejorada del lenguaje, que tiene menos pegas y más adiciones y buenos diseños. Este libro está escrito para Java 2. Tengo la gran ventaja de librarme de todo el material y escribir sólo para el nuevo lenguaje ya mejorado porque la información vieja sigue existiendo en la l."versión electrónica disponible en la Web y en el CD-ROM (que es a donde se puede ir si se desea obcecarse en el uso de versiones pre-Java 2 del lenguaje). También, y dado que cualquiera puede descargarse gratuitamente el JDK de http: / / java.sun.com, se supone que por escribir para Java 2, no estoy imponiendo ningún criterio financiero o forzando a nadie a hacer una actualización del software. Hay, sin embargo, algo que reseñar. JDK 1.3 tiene algunas mejoras que verdaderamente me gusta-

ría usar, pero la versión de Java que está siendo actualmente distribuida para Linux es la JDK 1.2.2 (ver http:/ /www.Linux.org). Linux es un desarrollo importante en conjunción con Java, porque es rápido, robusto, seguro, está bien mantenido y es gratuito; una auténtica revolución en la historia de la computación (no creo que se hayan visto todas estas características unidas en una única herramienta anteriormente). Y Java ha encontrado un nicho muy importante en la programación en el lado servidor en forma de Serulets, una tecnología que es una grandísima mejora sobre la programación tradicional basada en CGI (todo ello cubierto en el capítulo "Computación Distribuida"). Por tanto, aunque me gustaría usar sólo las nuevas características, es crítico que todo se compile bajo Linux, y por tanto, cuando se desempaquete el código fuente y se compile bajo ese SO (con el último JDK) se verá que todo compila. Sin embargo, se verá que he puesto notas sobre características de JDK 1.3 en muchos lugares.

El CD ROM Otro bonus con esta edición es el CD ROM empaquetado al final del libro. En el pasado me he resistido a poner CD ROM al final de mis libros porque pensaba que no estaba justificada una carga de unos pocos Kbytes de código fuente en un soporte tan grande, prefiriendo en su lugar permitir a la gente descargar los elementos desde el sitio web. Sin embargo, pronto se verá que este CD ROM es diferente. El CD contiene el código fuente del libro, pero también contiene el libro en su integridad, en varios formatos electrónicos. Para mí, el preferido es el formato HTML porque es rápido y está completamente indexado -simplemente se hace clic en una entrada del índice o tabla de contenidos y se estará inmediatamente en esa parte del libro.

La carga de más de 300 Megabytes del CD, sin embargo, es un curso multimedia denominado Thinking in C: Foundationsfor C++ & Java. Originalmente encargué este seminario en CD ROM a Chuck Allison, como un producto independiente, pero decidí incluirlo con la segunda edición tanto de Thinking in C++ como de Piensa en Java, gracias a la consistente experiencia de haber tenido gente viniendo a los seminarios sin la requerida experiencia en C. El pensamiento parece aparentemente ser: "Soy un programador inteligente y no deseo aprender C, y sí C++ o Java, por lo que me saltaré C e iré directamente a C++/Java." Tras llegar al seminario, todo el mundo va comprendiendo que el


xxvi

Piensa en Java

prerrequisito de aprender C está ahí por algo. Incluyendo el CD ROM con el libro, se puede asegurar que todo el mundo atienda al seminario con la preparación adecuada. El CD también permite que el libro se presente para una audiencia mayor. Incluso aunque el Capítulo 3 («Controlando el flujo del programa») cubre los aspectos fundamentales de las partes de Java que provienen de C, el CD es una introducción más gentil, y asume incluso un trasfondo de C menor que el que supone el libro. Espero que al introducir el CD será más la gente que se acerque a la programación en Java.


Prólogo a la edición en espanoi Java se convierte día a día en un lenguaje de programación universal; es decir, ya no sólo sirve como lenguaje para programar en entornos de Internet, sino que se está utilizando cada vez más como herramienta de programación orientada a objetos y también como herramienta para cursos específicos de programación o de estructuras de datos, aprovechando sus características de lenguaje "multiparadigma". Por estas razones, los libros que afronten temarios completos y amplios sobre los temas anteriores siempre serán bienvenidos. Si, además de reunir estos requisitos, el autor es uno de los más galardonados por sus obras anteriores, n n s enfrentamos ante iin reto considerable: "la posibilidad de encontrarnos" ante un gran libro, de esos que hacen "historia". Éste, pensamos, es el caso del libro que tenemos entre las manos. ¿Por qué pensamos así?

El libro como referencia obligada a Java Piensa en Java introduce todos los fundamentos teóricos y prácticos del lenguaje Java, tratando de explicar con claridad y rigor no sólo lo que hace el lenguaje sino también el porqué. Eckel introduce los fundamentos de objetos y cómo los utiliza Java. Éste es el caso del estudio que hace de la ocultación de las implementaciones, reutilización de clases y polimorfismo. Además, estudia en profundidad propiedades y características tan importantes como AWT, programación concurrente (multihilo, multithreading2), programación en red, e incluso diseño de patrones.

Es un libro que puede servir para iniciarse en Java y llegar hasta un nivel avanzado. Pero, en realidad se sacará el máximo partido al libro si se conoce otro lenguaje de programación, o al menos técnicas de programación (como haber seguido un curso de Fundamentos de Programación, Metodología de la Programación, Algoritmos, o cursos similares) y ya se puede apostar por un alto y eficiente rendimiento si la migración a Java se hace desde un lenguaje orientado a objetos, como C++.

El libro como formación integral de programador Una de las fortalezas más notables del libro es su contenido y la gran cantidad de temas importantes cubiertos con toda claridad y rigor, y con gran profundidad. El contenido es muy amplio y sobre todo completo. Eckel prácticamente ha tocado casi todas las técnicas existentes y utilizadas hoy día en el mundo de la programación y de la ingeniería del software. Algunos de los temas más sobresalientes analizados en el libro son: fundamentos de diseño orientado a objetos, implementación de herencia y polimorfismo, manejo de excepciones, multihilo y persistencia, Java en Internet, recolección de basura, paquetes Java, diseño por reutilización: composición, herencia, interfaces y clases internas, arrays y contenedores de clases, clases de E/S Java, programación de redes con sockets, JDBC para bases de datos, JSPs (JavaServer Pages), RMI, CORBA, EJBs (Enterprise JauaBeans) y Jini, JNI (Java Native Interface).


xxviii

Piensa en Java

El excelente y extenso contenido hacen al libro idóneo para la preparación de cursos de nivel medio y avanzado de programación, tanto a nivel universitario como profesional. Asimismo, por el enfoque masivamente profesional que el autor da al libro, puede ser una herramienta muy útil como referencia básica o complementaria para preparar los exámenes de certificación Java que la casa Sun Microsystems otorga tras la superación de las correspondientes pruebas. Esta característica es un valor añadido muy importante, al facilitar considerablemente al lector interesado las directrices técnicas necesarias para la preparación de la citada certificación.

Recursos gratuitos en línea Si las características citadas anteriormente son de por sí lo suficientemente atractivas para la lectura del libro, es sin duda el excelente sitio en Internet del autor otro valor añadido difícil de medir, por no decir inmedible y valiosísimo. La generosidad del autor -y, naturalmente, de Pearson-, que ofrece a cualquier lector, sin necesidad de compra previa, todo el contenido en línea, junto a las frecuentes revisiones de la obra y soluciones a ejercicios seleccionados, con la posibilidad de descargarse gratuitamente todo este inmenso conocimiento incluido en el libro, junto al conocimiento complementario ofertado (ejercicios, revisiones, actualizaciones...), hacen a esta experiencia innovadora del autor digna de los mayores agradecimientos por parte del cuerpo de programadores noveles o profesionales de cualquier lugar del mundo donde se utilice Java (que hoy es prácticamente "todo el mundo mundial", que dirían algunos periodistas). De igual forma es de agradecer el CD kOM que acompaña al libro y la oferta de un segundo CD gratuito que se puede conseguir siguiendo las instrucciones incluidas en el libro con el texto completo de la versión original en inglés y un gran número de ejercicios seleccionados resueltos y recursos Java de todo tipo. Para facilitar al lector el uso del CD ROM incluido en el libro, el equipo de revisión técnica ha realizado el Apéndice E: Correspondencias español-inglés de clases, bases de datos, tablas y campos del CD ROM que acompaña al libro, a fin de identificar el nombre asignado en la traducción al español, con el nombre original en inglés de dichas clases.

Unas palabras todavía más elogiosas Para las personas que, como el autor de este prólogo, llevamos muchos años (ya décadas) dedicándonos a programar computadores, enseñar a programar y escribir sobre programación, un libro como éste sólo nos trae elevados y elogiosos pensamientos. Consideramos que es un libro magnífico, maduro, consistente, intelectualmente honesto, bien escrito y preciso. Sin duda, como lo demuestra su larga lista de premios y sus numerosas y magníficas cualidades, Piensa en Java, no sólo es una excelente obra para aprender y llegar a dominar el lenguaje Java y su programación, sino también una excelente obra para aprender y dominar las técnicas modernas de programación. Luis Joyanes Aguilar Director del Departamento de Lenguajes y Sistemas Informáticos e Zngeniená de Software

Universidad Pontificia de Salamanca campus Madrid


Comentarios

los lectores

Mucho mejor que cualquier otro libro de Java que haya visto. Esto se entiende "en orden de magnitud" ... muy completo, con ejemplos directos y al grano, excelentes e inteligentes, sin embarullarse, lleno de explicaciones....En contraste con muchos otros libros de Java lo he encontrado inusualmente maduro, consistente, intelectualmente honesto, bien escrito y preciso. En mi honesta opinión, un libro ideal para estudiar Java. Anatoly Vorobey, Technion University, Haifa, Israel.

Uno de los mejores tutoriales de programación, que he visto de cualquier lenguaje. Joakim Ziegler, FIX sysop. Gracias por ese libro maravilloso, maravilloso en Java. Dr. Gavin Pillary, Registrar, King Eduard VI11 Hospital, Suráfrica. Gracias de nuevo por este maravilloso libro. Yo estaba completamente perdido (soy un programador que no viene de C) pero tu libro me ha permitido avanzar con la misma velocidad con la que lo he leído. Es verdaderamente fácil entender los principios subyacentes y los conceptos desde el principio, en vez de tener que intentar construir todo el modelo conceptual mediante prueba y error. Afortunadamente podré acudir a su seminario en un futuro no demasiado lejano. Randa11 R. Hawley, Automation Technician, Eli Lilly & Co. El mejor libro escrito de computadores que haya visto jamás. Tom Holland. Éste es uno de los mejores libros que he leído sobre un lenguaje de programación ... El mejor libro sobre Java escrito jamás. Revindra Pai, Oracle Corporation, línea de productos SUNOS. ¡Éste es el mejor libro sobre Java que haya visto nunca! Has hecho un gran trabajo. Tu profundidad es sorprendente. Compraré el libro en cuanto se publique. He estado aprendiendo Java desde octubre del 96. He leído unos pocos libros y considero el tuyo uno que "SE DEBE LEER". En estos últimos meses nos hemos centrado en un producto escrito totalmente en Java. Tu libro ha ayudado a consolidar algunos temas en los que andábamos confusos y ha expandido mi conocimiento base. Incluso he usado algunos de tus ejemplos y explicaciones como información en mis entrevistas para ayudar al equipo. He averiguado el conocimiento de Java que tienen preguntándoles por algunas de las cosas que he aprendido a partir de la lectura de tu libro (por ejemplo, la diferencia entre arrays y Vectores). ¡El libro es genial! Steve Wilkinson, Senior Staff Specialist, MCI Telecommunications. Gran libro. El mejor libro de Java que he visto hasta la fecha. Jeff Sinlair, ingeniero de Software, Kestral Computing. Gracias por Piensa en Java. Ya era hora de que alguien fuera más allá de una mera descripción del lenguaje para lograr un tutorial completo, penetrante, impactante y que no se centra en los fabricante. He leído casi todos los demás -y sólo el tuyo y el de Patrick Winston han encontrado un lugar en mi corazón. Se lo estoy recomendando ya a los clientes. Gracias de nuevo. Richard Brooks, consultor de Java, Sun Professional Services, Dallas. Otros libros contemplan o abarcan el QUÉ de Java (describiendo la sintaxis y las bibliotecas) o el CÓMO de Java (ejemplos de programación prácticos). Piensa en Jaual es el único libro que conoz' Thinking in Java (titulo original de la obra en inglés).


xxx

Piensa en Java

co que explica el PORQUÉ de Java; por qué se diseñó de la manera que se hizo, por qué funciona como lo hace, por qué en ocasiones no funciona, por qué es mejor que C++,por qué no lo es. Aunque hace un buen trabajo de enseñanza sobre el qué y el cómo del lenguaje, Piensa en Java es la elección definitiva que toda persona interesada en Java ha de hacer. Robert S. Stephenson. Gracias por escribir un gran libro. Cuanto más lo leo más me gusta. A mis estudiantes también les gusta. Chuck Iverson. Sólo quiero comentarte tu trabajo en Piensa en Java. Es la gente como tú la que dignifica el futuro de Internet y simplemente quiero agradecerte el esfuerzo. Patrick Barrell, Network Officer Mamco, QAF M@. Inc.

La mayoría de libros de Java que existen están bien para empezar, y la mayoría tienen material para principiantes y muchos los mismos ejemplos. El tuyo es sin duda el mejor libro y más avanzado para pensar que he visto nunca. iPor favor, publícalo rápido!... También compré Thinking in C++ simplemente por lo impresionado que me dejó Piensa en Java. George Laframboise, LightWorx Technology Consulting Inc. Te escribí anteriormente con mis impresiones favorables relativas a Piensa en Java (un libro que empieza prominentemente donde hay que empezar). Y hoy que he podido meterme con Java con tu libro electrónico en mi mano virtual, debo decir (en mi mejor Chevy Chase de Modern Problems) "¡Me gusta!". Muy informativo y explicativo, sin que parezca que se lee un texto sin sustancia. Cubres los aspectos más importantes y menos tratados del desarrollo de Java: los porqués. Sean Brady. Tus ejemplos son claros y fáciles de entender. Tuviste cuidado con la mayoría de detalles importantes de Java que no pueden encontrarse fácilmente en la débil documentación de Java. Y no malgastas el tiempo del lector con los hechos básicos que todo programador conoce. Kai Engert, Innovative Software, Alemania. Soy un gran fan de Piensa en Java y lo he recomendado a mis asociados. A medida que avanzo por la versión electrónica de tu libro de Java, estoy averiguando que has retenido el mismo alto nivel de escritura. Peter R. Neuvald. Un libro de Java M W BIEN escrito... Pienso que has hecho un GRAN trabajo con él. Como líder de un grupo de interés especial en Java del área de Chicago, he mencionado de forma favorable tu libro y sitio web muy frecuentemente en mis últimas reuniones. Me gustaría usar Piensa en Java como la base de cada reunión mensual del grupo, para poder ir repasando y discutiendo sucesivamente cada capítulo. Mark Ertes. Verdaderamente aprecio tu trabajo, y tu libro es bueno. Lo recomiendo aquí a nuestros usuarios y estudiantes de doctorado. Hughes Leroy // Irisa-Inria Rennes France, jefe de Computación Científica y Transferencia Industrial. De acuerdo, sólo he leído unas 40 páginas de Piensa en Java, pero ya he averiguado que es el libro de programación mejor escrito y más claro que haya visto jamás ... Yo también soy escritor, por lo que probablemente soy un poco crítico. Tengo Piensa en Java encargado y ya no puedo esperar más -soy bastante nuevo en temas de programación y no hago más que enfrentarme a curvas de


Comentarios de los lectores

xxxi

aprendizaje en todas partes. Por tanto, esto no es más que un comentario rápido para agradecerte este trabajo tan excelente. Ya me había empezado a quemar de tanto navegar por tanta y tanta prosa de tantos y tantos libros de computadores -incluso muchos que venían con magníficas recomendaciones. Me siento muchísimo mejor ahora. Glenn Becker, Educational Theatre ssociation. Gracias por permitirme disponer de este libro tan maravilloso. Lo he encontrado inmensamente útil en el entendimiento final de lo que he experimentado -algo confuso anteriormente- con Java y C++. Leer tu libro ha sido muy gratificante. Felix Bizaoui, Twin Oaks Industnes, Luisa, Va. Debo felicitarte por tu excelente libro. He decidido echar un vistazo a Piensa en Java guiado por mi experiencia en Thinking in C++, y no me ha defraudado. Jaco van der Merwe, Software Specialist, DataFusion Systems Ltd., Steíienbosch, Suráfnca. Este libro hace que todos los demás libros de Java que he leído parezcan un insulto o sin duda inútiles. 13rett g Porter, Senior Programmer, Art & Logic. He estado leyendo tu libro durante una semana o dos y lo he comparado con otros libros de Java que he leído anteriormente. Tu libro parece tener un gran comienzo. He recomendado este libro a muchos de mis amigos y todos ellos lo han calificado de excelente. Por favor, acepta mis felicitaciones por escribir un libro tan excelente. Rama Krishna Bhupathi, Ingeniera de Software, TCSI Corporation, San José. Simplemente quería decir lo "brillante" que es tu libro. Lo he estado usando como referencia principal durante mi trabajo de Java hecho en casa. He visto que la tabla de contenidos es justo la más adecuada para localizar rápidamente la sección que se requiere en cada momento. También es genial ver un libro que no es simplemente una compilación de las API o que no trata al programador como a un monigote. Grant Sayer, Java Components Group Leader, Ceedata Systems Pty Ltd., Australia. ~ G u ~Un u ! libro de Java profundo y legible. Hay muchos libros pobres (y debo admitir también que un par de ellos buenos) de Java en el mercado, pero por lo que he visto, el tuyo es sin duda uno de los mejores. John Root, desarrollador Web, Departamento de la Seguridad Social, Londres. *Acabo* de empezar Piensa en Java. Espero que sea bueno porque me gustó mucho Thinking in C++ (que leí como programador ya experimentado en C++, intentado adelantarme a la curva de aprendizaje). En parte estoy menos habituado a Java, pero espero que el libro me satisfaga igualmente. Eres un autor maravilloso. Kevin K. Lewis, Tecnólogo, ObjectSpace Inc. Creo que es un gran libro. He aprendido todo lo que sé de Java a partir de él. Gracias por hacerlo disponible gratuitamente a través de Internet. Si no lo hubieras hecho no sabría nada de Java. Pero lo mejor es que tu libro no es un mero folleto publicitario de Java. También muestra sus lados negativos. TÚ has hecho aquí un gran trabajo. FrederikFix, Bélgica. Siempre me han enganchado tus libros. Hace un par de años, cuando quería empezar con C++,fue C++Inside & Out el que me introdujo en el fascinante mundo de C++.Me ayudó a disponer de mejores oportunidades en la vida. Ahora, persiguiendo más conocimiento y cuando quería aprender Java, me introduje en Piensa en Java -sin dudar de que gracias a él ya no necesitaría ningún otro libro. Simplemente fantástico. Es casi como volver a descubrirme a mí mismo a medida que avanzo


xxxii

Piensa en Java

en el libro. Apenas hace un mes que he empezado con Java y mi corazón late gracias a ti. Ahora lo entiendo todo mucho mejor. Anand Kumar S., ingeniero de Software Computervision, India. Tu libro es una introducción general excelente. Peter Robinson, Universidad de Cambridge, Computar Laboratory. Es con mucho el mejor material al que he tenido acceso al aprender Java y simplemente quería que supieras la suerte que he tenido de poder encontrarlo. ¡GRACIAS! Chuck Peterson, Product Leader, Internet Product Line, M S International. Este libro es genial. Es el tercer libro de Java que he empezado y ya he recorrido prácticamente dos tercios. Espero acabar éste. Me he enterado de su existencia porque se usa en algunas clases internas de Lucen Technologies y un amigo me ha dicho que el libro estaba en la Red. Buen trabajo. Jerry Nowlin, M13, Lucent Technologies. De los aproximadamente seis libros de Java que he acumulado hasta la fecha, tu Piensa en Java es sin duda el mejor y el más claro. Michael Van Waas, doctor, presidente, TMR Associates. Simplemente quiero darte las gracias por Piensa en Java. ¡Qué libro tan maravilloso has hecho! iY para qué mencionar el poder bajárselo gratis! Como estudiante creo que tus libros son de valor incalculable, tengo una copia de C++ Inside & Out, otro gran libro sobre C++),porque no sólo me enseñan el cómo hacerlo, sino que también los porqués, que sin duda son muy importantes a la hora de sentar unas buenas bases en lenguajes como C++y Java. Tengo aquí bastantes amigos a los que les encanta programar como a mí, y les he hablado de tus libros. ¡Todos piensan que son geniales! Por cierto, soy indonesio y vivo en Java. Ray Frederick Djajadinata, estudiante en Trisakti University, Jakarta. El mero hecho de que hayas hecho que este trabajo esté disponible gratuitamente en la Red me deja conmocionado. Pensé que debía decirte cuánto aprecio y respeto lo que estás haciendo. Shane KeBouthillier, estudiante de Ingeniería en Informática, Universidad de Alberta, Canadá. Tengo que decirte cuánto ansío leer tu columna mensual. Como novato en el mundo de la programación orientada a objetos, aprecio el tiempo y el grado de conocimiento que aportas en casi todos los temas elementales. He descargado tu libro, pero puedes apostar a que compraré una copia en papel en cuanto se publique. Gracias por toda tu ayuda. Dan Cashmer, D. C. Ziegler & Co. Simplemente quería felicitarte por el buen trabajo que has hecho. Primero me recorrí la versión PDF de Piensa en Java. Incluso antes de acabar de leerla, corrí a la tienda y compré Thinking in C++.Ahora que llevo en el negocio de la informática ocho años, como consultor, ingeniero de software, profesor/formador, y últimamente autónomo, creo que puedo decir que he visto suficiente (fíjate que no digo haber visto "todo" sino suficiente). Sin embargo, estos libros hacen que mi novia me llame "geek. No es que tenga nada contra el concepto en sí -simplemente pensaba que ya había dejado atrás esta fase. Pero me veo a mí mismo disfrutando sinceramente de ambos libros, de una forma que no había sentido con ningún otro libro que haya tocado o comprado hasta la fecha. Un estilo de escritura excelente, una introducción genial de todos los temas y mucha sabiduría en ambos textos. Bien hecho. Simon Goland, simonsez@smartt.com, Simon Says Consulting, Inc.


Comentarios de los lectores

xxxiii

¡Debo decir que tu Piensa en Java es genial! Es exactamente el tipo de documentación que buscaba. Especialmente las secciones sobre los buenos y malos diseños basados en Java. Dirk Duehr, Lexikon Verlag, Bertelsmann AG, Alemania. Gracias por escribir dos grandes libros (Thinking in C++, Piensa en Java). Me has ayudado inmensamente en mi progresión en la programación orientada a objetos. Donald Lawon, DCL Enterprises. Gracias por tomarte el tiempo de escribir un libro de Java que ayuda verdaderamente. Si enseñar hace que aprendas algo, tú ya debes estar más que satisfecho. Dominic Turner, GEAC Support. Es el mejor libro de Java que he leído jamás -y he leído varios. Jean-Yves MENGANT, Chief Software Architect NAT-SYSTEM, París, Francia.

Piensa en Java proporciona la mejor cobertura y explicación. Muy fácil de leer, y quiero decir que esto se extiende también a los fragmentos de código. Ron Chan, Ph. D., Expert Choice Ind., Pittsburg PA. Tu libro es genial. He leído muchos libros de programación y el tuyo sigue añadiendo luz a la programación en mi mente. Ningjian Wang, Information System Engineer, The Vanguard Group. Piensa en Java es un libro excelente y legible. Se lo recomiendo a todos mis alumnos. Dr. Paul Gorman, Department of Computer Sciente, Universidad de Otago, Dunedin, Nueva Zelanda. Haces posible que exista el proverbial almuerzo gratuito, y no simplemente una comida basada en sopa de pollo, sino una delicia de gourmet para aquéllos que aprecian el buen software y los libros sobre él mismo. Jose Suriol, Scylax Corporation. ¡Gracias por la oportunidad de ver cómo este libro se convierte en una obra maestra! ES EL MEJOR libro de la materia que he leído o recorrido. Jeff Lapchinsky, programador, Net Result Tecnologies.

Tu libro es conciso, accesible y gozoso de leer. Keith Ritchie, Java Research & Develpment Team, KL Group Inc. ¡ESsin duda el mejor libro de Java que he leído! Daniel Eng. ¡ESel mejor libro de Java que he visto! Rich Hoffarth, Arquitecto Senior, West Group. Gracias por un libro tan magnífico. Estoy disfrutando mucho a medida que leo capítulos. Fred Trimble, Actium Corporation. Has llegado a la maestría en el arte de hacernos ver los detalles, despacio y con éxito. Haces que la lectura sea MUY fácil y satisfactoria. Gracias por un tutorial tan verdaderamente maravilloso. Rajesh Rau, Software Consultant.

Piensa en Java es un rock para el mundo libre! Miko O'Sullivan, Presidente, Idocs Inc.


xxxiv

Piensa en Java

Sobre Thinking in C++: Best Book! Ganador en 1995 del Software Development Magazine Jolt Award! "Este libro es un tremendo logro. Deberías tener una copia en la estantería. El capítulo sobre flujos de E/S presenta el tratamiento más comprensible y fácil de entender sobre ese tema que jamás haya visto."

Al Stevens Editor, Doctor Dobbs Journal "El libro de Eckel es el único que explica claramente cómo replantearse la construcción de programas para la orientación a objetos. Que el libro es también un tutorial excelente en las entradas y en las salidas de C++ es un valor añadido."

Andrew Binstock Editor, Unix Review "Bruce continúa deleitándome con esta introspección de C++,y Thinking in C++ es la mejor colección de ideas hasta la fecha. Si se desean respuestas rápidas a preguntas difíciles sobre C++, compre este libro tan sobresaliente."

Gary Entsminger Autor, The Tao of Objects "Thinking in C++"explora paciente y metódicamente los aspectos de cuándo y cómo usar los interlineado~,referencias, sobrecargas de operadores, herencia, y objetos dinámicos, además de temas avanzados como el uso adecuado de plantillas, excepciones y la herencia múltiple. Todo el esfuerzo se centra en un producto que engloba la propia filosofía de Eckel del diseño de objetos y programas. Un libro que no debe faltar en la librería de un desarrollador de C++, Piensa en Jaua es el libro de C++ que hay que tener si se están haciendo desarrollos serios con C++."

Richard Hale Shaw Ayudante del Editor, P C Magazine


Introducción Como cualquier lenguaje humano, Java proporciona una forma de expresar conceptos. Si tiene éxito, la expresión media será significativamente más sencilla y más flexible que las alternativas, a medida que los problemas crecen en tamaño y complejidad. No podemos ver Java como una simple colección de características -algunas de las características no tienen sentido aisladas. Se puede usar la suma de partes sólo si se está pensando en diseño,y no simplemente en codificación. Y para entender Java así, hay que entender los problemas del lenguaje y de la programación en general. Este libro habla acerca de problemas de programación, por qué son problemas y el enfoque que Java sigue para solucionarlos. Por consiguiente, algunas características que explico en cada capítulo se basan en cómo yo veo que se ha solucionado algún problema en particular con el lenguaje. Así, espero conducir poco a poco al lector, hasta el punto en que Java se convierta en lengua casi materna. Durante todo el tiempo, estaré tomando la actitud de que el lector construya un modelo mental que le permita desarrollar un entendimiento profundo del lenguaje; si se encuentra un puzzle se podrá alimentar de éste al modelo para tratar de deducir la respuesta.

Prerrequisitos Este libro asume que se tiene algo de familiaridad con la programación: se entiende que un programa es una colección de sentencias, la idea de una subrutina/función/macro, sentencias de control como "ir' y bucles estilo "while", etc. Sin embargo, se podría haber aprendido esto en muchos sitios, como, por ejemplo, la programación con un lenguaje de macros o el trabajo con una herramienta como Perl. A medida que se programa hasta el punto en que uno se siente cómodo con las ideas básicas de programación, se podrá ir trabajando a través de este libro. Por supuesto, el libro será más fácil para los programadores de C y aún más para los de C++,pero tampoco hay por qué excluirse a sí mismo cuando se desconocen estos lenguajes (aunque en este caso es necesario tener la voluntad de trabajar duro; además, el CD multimedia que acompaña a este texto te permitirá conocer rápidamente los conceptos de la sintaxis de C necesarios para aprender Java). Presentaré los conceptos de la programación orientada a objetos (POO) y los mecanismos de control básicos de Java, para tener conocimiento de ellos, y los primeros ejercicios implicarán las secuencias de flujo de control básicas. Aunque a menudo aparecerán referencias a aspectos de los lenguajes C y C++, no deben tomarse como comentarios profundos, sino que tratan de ayudar a los programadores a poner Java en perspectiva con esos lenguajes, de los que, después de todo, es de los que desciende Java. Intentaré hacer que estas referencias sean lo más simples posibles, y explicar cualquier cosa que crea que una persona que no haya programado nunca en C o C++ pueda desconocer.


xxxvi

Piensa en Java

Aprendiendo Java Casi a la vez que mi primer libro Using C++ (Osborne/McGraw-Hill, 1989)apareció, empecé a enseñar ese lenguaje. Enseñar lenguajes de programación se ha convertido en mi profesión; he visto cabezas dudosas, caras en blanco y expresiones de puzzle en audiencias de todo el mundo desde 1989. A medida que empecé con formación in situ a grupos de gente más pequeños, descubrí algo en los ejercicios. Incluso aquéllos que sonreían tenían pegas con muchos aspectos. Al dirigir la sesión de C++ en la Software Development Conference durante muchos años (y después la sesión de Java), descubrí que tanto yo como otros oradores tendíamos a ofrecer a la audiencia, en general, muchos temas demasiado rápido. Por tanto, a través, tanto de la variedad del nivel de audiencia como de la forma de presentar el material, siempre se acababa perdiendo parte de la audiencia. Quizás es pedir demasiado, pero dado que soy uno de ésos que se resisten a las conferencias tradicionales (y en la mayoría de casos, creo que esta 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 consiguiente, acabé aprendiendo a base de experimentación e iteración (una técnica que también funciona bien en un diseño de un programa en Java). Eventualmente, desarrollé un curso usando todo lo que había aprendido de mi experiencia en la enseñanza -algo que me gustaría hacer durante bastante tiempo. Descompone el problema de aprendizaje en pasos discretos, fáciles de digerir, y en un seminario en máquina (la situación ideal de aprendizaje) hay ejercicios seguidos cada uno de pequeñas lecciones. Ahora doy cursos en seminarios públicos de Java, que pueden encontrarse en http:/ /www.BruceEckel.com. (El seminario introductorio también está disponible como un CD ROM. En el sitio web se puede encontrar más información al respecto.) La respuesta que voy obteniendo de cada seminario me ayuda a cambiar y reenfocar el material hasta que creo que funciona bien como medio docente. Pero este libro no es simplemente un conjunto de notas de los seminarios -intenté empaquetar tanta información como pude en este conjunto de páginas, estructurándola de forma que cada tema te vaya conduciendo al siguiente. Más que otra cosa, el libro está diseñado para servir al lector solitario que se está enfrentando y dando golpes con un nuevo lenguaje de programación.

Objetivos Como en mi libro anterior Thinking in C++, este libro pretende estar estructurado en torno al proceso de enseñanza de un lenguaje. En particular, mi motivación es crear algo que me proporcione una forma de enseñar el lenguaje en mis propios seminarios. Cuando pienso en un capítulo del libro, lo pienso en términos de lo que constituiría una buena lección en un seminario. Mi objetivo es lograr fragmentos que puedan enseñarse en un tiempo razonable, seguidos de ejercicios que sean fáciles de llevar a cabo en clase. Mis objetivos en este libro son: 1.

Presentar el material paso a paso de forma que se pueda digerir fácilmente cada concepto antes de avanzar.


Introducción

xxxvii

2.

Utilizar ejemplos que sean tan simples y cortos como se pueda. Esto evita en ocasiones acometer problemas del "mundo real", pero he descubierto que los principiantes suelen estar más contentos cuando pueden entender todos los detalles de un ejemplo que cuando se ven impresionados por el gran rango del problema que solucionan. Además, hay una limitación severa de cara a la cantidad de código que se puede absorber en una clase. Por ello, no dudaré en recibir críticas por usar "ejemplos de juguete", sino que estoy deseoso de aceptarlas en aras de lograr algo pedagógicamente útil.

3.

Secuenciar cuidadosamente la presentación de características de forma que no se esté viendo algo que aún no se ha expuesto. Por supuesto, esto no es siempre posible; en esas situaciones sc da11br-cves descr-ipcioiies iiitr-oductoi-ias.

4.

Dar lo que yo considero que es importante que se entienda del lenguaje, en lugar de todo lo que sé. Creo que hay una jerarquía de importancia de la información, y que hay hechos que el 95%de los programadores nunca necesitarán saber y que simplemente confunden a la gente y añaden su percepción de la complejidad del lenguaje. Por tomar un ejemplo de C, si se memoriza la tabla de precedencia de los operadores (algo que yo nunca hice) se puede escribir un código más inteligente. Pero si se piensa en ello, también confundirá la legibilidad y mantenibilidad de ese código. Por tanto, hay que olvidarse de la precedencia, y usar paréntesis cuando las cosas no estén claras.

5.

Mantener cada sección lo suficientemente enfocada de forma que el tiempo de exposición -el tiempo entre periodos de ejercicios- sea pequeño. Esto no sólo mantiene más activas las mentes de la audiencia, que están en un seminario en máquina, sino que también transmite más sensación de avanzar.

6.

Proporcionar una base sólida que permita entender los aspectos lo suficientemente bien como para avanzar a cursos y libros más difíciles.

Documentación en línea El lenguaje Java y las bibliotecas de Sun Microsystems (de descarga gratuita) vienen con su documentación en forma electrónica, legible utilizando un navegador web, y casi toda implementación de Java de un tercero tiene éste u otro sistema de documentación equivalente. Casi todos los libros publicados de Java, incorporan esta documentación. Por tanto, o ya se tiene, o se puede descargar, y a menos que sea necesario, este libro no repetirá esa documentación pues es más rápido encontrar las descripciones de las clases en el navegador web que buscarlas en un libro Cy la documentación en línea estará probablemente más actualizada). Este libro proporcionará alguna descripción extra de las clases sólo cuando sea necesario para complementar la documentación, de forma que se pueda entender algún ejemplo particular.


xxxviii Piensa en Java

Capítulos Este libro se diseñó con una idea en la cabeza: la forma que tiene la gente de aprender Java. La realimentación de la audiencia de mis seminarios me ayudó a ver las partes difíciles que necesitaban aclaraciones. En las áreas en las que me volvía ambiguo e incluía varias características a la vez, descubrí -a través del proceso de presentar el material- que si se incluyen muchas características de golpe, hay que explicarlas todas, y esto suele conducir fácilmente a la confusión por parte del alumno. Como resultado, he tenido bastantes problemas para presentar las características agrupadas de tan pocas en pocas como me ha sido posible. El objetivo, por tanto, es que cada capítulo enseñe una única característica, o un pequeño grupo de características asociadas, sin pasar a características adicionales. De esa forrria se puede diger-ir-cada fragmento en el contexto del conocimiento actual antes de continuar. He aquí una breve descripción de los capítulos que contiene el libro, que corresponde a las conferencias y periodos de ejercicio en mis seminarios en máquina.

Capítulo 1: Introducción a los objetos Este capítulo presenta un repaso de lo que es la programación orientada a objetos, incluyendo la respuesta a la cuestión básica "¿Qué es un objeto?", interfaz frente a implementación, abstracción y encapsulación, mensajes y funciones, herencia y composición, y la importancia del polimorfismo. También se obtendrá un repaso a los aspectos de la creación de objetos como los constructores, en los que residen los objetos, dónde ponerlos una vez creados, y el mágico recolector de basura que limpia los objetos cuando dejan de ser necesarios. Se presentarán otros aspectos, incluyendo el manejo de errores con excepciones, el multihilo para interfaces de usuario con buen grado de respuesta, y las redes e Internet. Se aprenderá qué convierte a Java en especial, por qué ha tenido tanto éxito, y también algo sobre análisis y diseño orientado a objetos.

Capítulo 2: Todo es un objeto Este capítulo te lleva al punto donde tú puedas crear el primer programa en Java, por lo que debe dar un repaso a lo esencial, incluyendo el concepto de referencia a un objeto; cómo crear un objeto; una introducción de los tipos primitivos y arrays; el alcance y la forma en que destruye los objetos el recolector de basura; cómo en Java todo es un nuevo tipo de datos (clase) y cómo crear cada uno sus propias clases; funciones, argumentos y valores de retorno; visibilidad de nombres y el uso de componentes de otras bibliotecas; la palabra clave static;y los comentarios y documentación embebida.

Capítulo 3: Controlando el flujo de los programas Este capítulo comienza con todos los operadores que provienen de C y C++. Además, se descubrirán los fallos de los operadores comunes, la conversión de tipos, la promoción y la precedencia. Des-


Introducción

xxxix

pués se presentan las operaciones básicas de control de flujo y selección existentes en casi todos los lenguajes de programación: la opción con if-else;los bucles con while y for; cómo salir de un bucle con break y continue, además de sus versiones etiquetadas en Java (que vienen a sustituir al "goto perdido" en Java); la selección con switch. Aunque gran parte de este material tiene puntos comunes con el código de C y C++,hay algunas diferencias. Además, todos los ejemplos estarán hechos completamente en Java por lo que el lector podrá estar más a gusto con la apariencia de Java.

Capítulo 4: Inicialización y limpieza Este capítulo comienza presentando el constructor, que garantiza una inicialización adecuada. La definición de constructor conduce al concepto de sobrecarga de funciones (puesto que puede haber varios constructores). Éste viene seguido de una discusión del proceso de limpieza, que no siempre es tan simple como parece. Normalmente, simplemente se desecha un objeto cuando se ha acabado con él y el recolector de basura suele aparecer para liberar la memoria. Este apartado explora el recolector de basura y algunas de sus idiosincrasias. El capítulo concluye con un vistazo más cercano a cómo se inicializan las cosas: inicialización automática de miembros, especificación de inicialización de miembros, el orden de inicialización, la inicialización static y la inicialización de arrays.

Capítulo

Ocultando

implementación

Este capítulo cubre la forma de empaquetar junto el código, y por qué algunas partes de una biblioteca están expuestas a la vez que otras partes están ocultas. Comienza repasando las palabras clave package e import, que llevan a cabo empaquetado a nivel de archivo y permiten construir bibliotecas de clases. Después examina el tema de las rutas de directorios y nombres de fichero. El resto del capítulo echa un vistazo a las palabras clave public, private y protected, el concepto de acceso "friendly", y qué significan los distintos niveles de control de acceso cuando se usan en los distintos conceptos.

Capítulo 6 :

clases

El concepto de herencia es estándar en casi todos los lenguajes de POO. Es una forma de tomar una clase existente y añadirla a su funcionalidad (además de cambiarla, que será tema del Capítulo 7). La herencia es a menudo una forma de reutilizar código dejando igual la "clase base", y simplemente parcheando los elementos aquí y allí hasta obtener lo deseado. Sin embargo, la herencia no es la única forma de construir clases nuevas a partir de las existentes. También se puede empotrar un objeto dentro de una clase nueva con la composición. En este capítulo, se aprenderán estas dos formas de reutilizar código en Java, y cómo aplicarlas.

Capítulo 7: Polimorfismo Cada uno por su cuenta, podría invertir varios meses para descubrir y entender el polimorfismo, claves en POO. A través de pequeños ejemplos simples, se verá cómo crear una familia de tipos con


xl

Piensa en Java

herencia y manipular objetos de esa familia a través de su clase base común. El polimorfismo de Java permite tratar los objetos de una misma familia de forma genérica, lo que significa que la mayoría del código no tiene por qué depender de un tipo de información específico. Esto hace que los programas sean extensibles, por lo que se facilita y simplifica la construcción de programas y el mantenimiento de código.

Capítulo 8: Interfaces y clases internas Java proporciona una tercera forma de establecer una relación de reutilización a través de la interfaz, que es una abstracción pura del interfaz de un objeto. La interfaz es más que una simple clase abstracta llevada al extremo, puesto que te permite hacer variaciones de la "herencia múltiple" de C++, creando una clase sobre la que se puede hacer una conversión hacia arriba a más de una clase base. A primera vista, las clases parecen un simple mecanismo de ocultación de código: se colocan clases dentro de otras clases. Se aprenderá, sin embargo, que la clase interna hace más que eso -conoce y puede comunicarse con la clase contenedora- y que el tipo de código que se puede escribir con clases internas es más elegante y limpio, aunque es un concepto nuevo para la mayoría de la gente y lleva tiempo llegar a estar cómodo utilizando el diseño clases internas.

Capítulo 9: Guardando tus objetos Es un programa bastante simple que sólo tiene una cantidad fija de objetos de tiempo de vida conocido. En general, todos los programas irán creando objetos nuevos en distintos momentos, conocidos sólo cuando se está ejecutando el programa. Además, no se sabrá hasta tiempo de ejecución la cantidad o incluso el tipo exacto de objetos que se necesitan. Para solucionar el problema de programación general, es necesario crear cualquier número de objetos, en cualquier momento y en cualquier lugar. Este capítulo explora en profundidad la biblioteca de contenedores que proporciona Java 2 para almacenar objetos mientras se está trabajando con ellos: los simples arrays y contenedores más sofisticados (estructuras de datos) como ArrayList y HashMap.

Capítulo 10: Manejo de errores con excepciones La filosofía básica de Java es que el código mal formado no se ejecutará. En la medida en que sea posible, el compilador detecta problemas, pero en ocasiones los problemas -debidos a errores del programador o a condiciones de error naturales que ocurren como parte de la ejecución normal del programa- pueden detectarse y ser gestionados sólo en tiempo de ejecución. Java tiene el manejo de excepciones para tratar todos los problemas que puedan surgir al ejecutar el programa. Este capítulo muestra cómo funcionan en Java las palabras clave try, catch, throw, throws y finally; cuándo se deberían lanzar excepciones y qué hacer al capturarlas. Además, se verán las excepciones estándar de Java, cómo crear las tuyas propias, qué ocurre con las excepciones en los constructores y cómo se ubican los gestores de excepciones.


Introducción

xli

Capítulo 11: El sistema de E/S de Java Teóricamente, se puede dividir cualquier programa en tres partes: entrada, proceso y salida. Esto implica que la E/S (entrada/salida) es una parte importante de la ecuación. En este capítulo se aprenderá las distintas clases que proporciona Java para leer y escribir ficheros, bloques de memoria y la consola. También se mostrará la distinción entre E/S "antigua" y "nueva". Además, este capítulo examina el proceso de tomar un objeto, pasarlo a una secuencia de bytes (de forma que pueda ser ubicado en el disco o enviado a través de una red) y reconstruirlo, lo que realiza automáticamente la serialización de objetos de Java. Además, se examinan las bibliotecas de compresión de Java, que se usan en el formato de archivos de Java CJAR).

Capítulo 12: Identificación de tipos en tiempo de ejecución La identificación de tipos en tiempo de ejecución (RTTI) te permite averiguar el tipo exacto de un objeto cuando se tiene sólo una referencia al tipo base. Normalmente, se deseará ignorar intencionadamente el tipo exacto de un objeto y dejar que sea el mecanismo de asignación dinámico de Java (polimorfismo) el que implemente el comportamiento correcto para ese tipo. A menudo, esta información te permite llevar a cabo operaciones de casos especiales, más eficientemente. Este capítulo explica para qué existe la RTTI, cómo usarlo, y cómo librarse de él cuando sobra. Además, este capítulo presenta el mecanismo de reflectividad de Java.

Capítulo 13: Creación de ventanas y applets Java viene con la biblioteca IGU Swing, que es un conjunto de clases que manejan las ventanas de forma portable. Estos programas con ventanas pueden o bien ser applets o bien aplicaciones independientes. Este capítulo es una introducción a Swing y a la creación de applets de World Wide Web. Se presenta la importante tecnología de los "JavaBeansn,fundamental para la creación de herramientas de construcción de programas de Desarrollo Rápido de Aplicaciones (RAD).

Capítulo 14: Hilos múltiples Java proporciona una utilidad preconstruida para el soporte de múltiples subtareas concurrentes denominadas hilos, que se ejecutan en un único programa. (A menos que se disponga de múltiples procesadores en la máquina, los múltiples hilos sólo son aparentes.) Aunque éstas pueden usarse en todas partes, los hilos son más lucidos cuando se intenta crear una interfaz de usuario con alto grado de respuesta, de forma que, por ejemplo, no se evita que un usuario pueda presionar un botón o introducir datos mientras se está llevando a cabo algún procesamiento. Este capítulo echa un vistazo a la sintaxis y la semántica del multihilo en Java.


xlii

Piensa en Java

Capítulo 15: Computación distribuida Todas las características y bibliotecas de Java aparecen realmente cuando se empieza a escribir programas que funcionen en red. Este capítulo explora la comunicación a través de redes e Internet, y las clases que proporciona Java para facilitar esta labor. Presenta los tan importantes conceptos de Serulets y JSP (para programación en el lado servidor), junto con Java DataBase Connectiuity CJDBC) y el Remote Method Inuocation (RMI). Finalmente, hay una introducción a las nuevas tecnologías de JINI, JauaSpaces, y Enterprise JavaBeans (EJBS) .

Apéndice A: Paso y retorno de objetos Puesto que la única forma de hablar con los objetos en Java es mediante referencias, los conceptos de paso de objetos a una función y de devolución de un objeto de una función tienen algunas consecuencias interesantes. Este apéndice explica lo que es necesario saber para gestionar objetos cuando se está entrando y saliendo de funciones, y también muestra la clase String, que usa un enfoque distinto al problema.

Apendice B: La Interfaz Nativa de Java (JNI) Un programa Java totalmente portable tiene importantes pegas: la velocidad y la incapacidad para acceder a servicios específicos de la plataforma. Cuando se conoce la plataforma sobre la que está ejecutando, es posible incrementar dramáticamente la velocidad de ciertas operaciones construyéndolas como métodos nativos, que son funciones escritas en otro lenguaje de programación (actualmente, sólo están soportados C/C++). Este apéndice da una introducción más que satisfactoria que debería ser capaz de crear ejemplos simples que sirvan de interfaz con código no Java.

Apendice C: Guías de programación Java Este apéndice contiene sugerencias para guiarle durante la realización del diseño de programas de bajo nivel y la escritura de código.

Apéndice D: Lecturas recomendadas Una lista de algunos libros sobre Java que he encontrado particularmente útil.

tjercicios He descubierto que los ejercicios simples son excepcionalmente útiles para completar el entendimiento de los estudiantes durante un seminario, por lo que se encontrará un conjunto de ellos al final de cada capítulo.


Introducción

xliii

La mayoría de ejercicios están diseñados para ser lo suficientemente sencillos como para poder ser resueltos en un tiempo razonable en una situación de clase mientras que observa el profesor, asegurándose de que todos los alumnos asimilen el material. Algunos ejercicios son más avanzados para evitar que los alumnos experimentados se aburran. La mayoría están diseñados para ser resueltos en poco tiempo y probar y pulir el conocimiento. Algunos suponen un reto, pero ninguno presenta excesivas dificultades. (Presumiblemente, cada uno podrá encontrarlos -o más probablemente te encontrarán ellos a ti.) En el documento electrónico The Thinking in Java Annotated Solution Guide pueden encontrarse soluciones a ejercicios seleccionados, disponibles por una pequeña tasa en http://www.BruceEckeI.com.

CD ROM Multimedia Hay dos CD multimedia asociados con este libro. El primero está en el propio libro: Thinking in C, descritos al final del prefacio. que te preparan para el libro aportando velocidad en la sintaxis de C necesaria para poder entender Java. Hay disponible un segundo CD ROM multimedia, basado en los contenidos del libro. Este CD ROM es un producto separado y contiene los contenidos enteros del seminario de formación "Hands-On Java" de una semana de duración. Esto son grabaciones de conferencias de más de 15 horas que he grabado, y sincronizado con cientos de diapositivas de información. Dado que el seminario se basa en este libro, es el acompañamiento ideal. El CD ROM contiene todas las conferencias (¡con la importante excepción de la atención personalizada!) de los seminarios de cinco días de inmersión total. Creemos que establece un nuevo estándar de calidad. El CD ROM "Hands-On Java" está disponible sólo bajo pedido, que se cursa directamente del sitio web http:llwww.BruceEckel.com.

Código fuente Todo el código fuente de este libro está disponible de modo gratuito sometido a copyright, distribuido como un paquete único, visitando el sitio web http://www.BruceEcke1.com. Para asegurarse de obtener la versión más actual, éste es el lugar oficial para distribución del código y de la versión electrónica del libro. Se pueden encontrar versiones espejo del código y del libro en otros sitios (algunos de éstos están referenciados en http://www. BruceEckel.com), pero habría que comprobar el sitio oficial para asegurarse de obtener la edición más reciente. El código puede distribuirse en clases y en otras situaciones con fines educativos.

La meta principal del copyright es asegurar que el código fuente se cite adecuadamente, y prevenir que el código se vuelva a publicar en medios impresos sin permiso. (Mientras se cite la fuente, utilizando los ejemplos del libro, no habrá problema en la mayoría de los medios.) En cada fichero de código fuente, se encontrará una referencia a la siguiente nota de copyright:


xliv

:

Piensa en Java

!

:CopyRght.txt Copyright (c)2000 Bruce Eckel Source code file from the 2nd edition of the book "Thinking in Java." Al1 rights reserved EXCEPT as allowed by the following statements: You can freely use this file for your own work (personal or commercial), including modifications and distribution in executable form only. Permission is granted to use this file in classroom situations, including its use in presentation materials, as long as the book

"Thinking in Java" i s c i t e d as t h e source. Except in classroom situations, you cannot copy and distribute this code; instead, the sole distribution point is http://www.BruceEckel.com (and official mirror sites) where it is freely available. You cannot remove this copyright and notice. You cannot distribute modified versions of the source code in this package. You cannot use this file in printed media without the express permission of the author. Bruce Eckel makes no representation about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty of any kind, including any implied warranty of merchantability, fitness for a particular purpose or non-infringement. The entire risk as to the quality and performance of the software is with you. Bruce Eckel and the publisher shall not be liable for any damages suffered by you or any third party as a result of using or distributing software. In no event will Bruce Eckel or the publisher be liable for any lost revenue, profit, or data, or for direct, indirect, special, consequential, incidental, or punitive darnages, however caused and reqardless of the theory of liability, arising out of the use of or inability to use software, even if Bruce Eckel and the publisher have been advised of the possibility of such damages. Should the software prove defective, you assume the cost of al1 necessary servicing, repair, or correction. If you think you've found an error, please submit the correction using the form you will find at www.BruceEckel.com. (Please use the same form for non-code errors found in the book. ) ///:-


Introducción

xlv

El código puede usarse en proyectos y en clases (incluyendo materiales de presentación) mientras se mantenga la observación de copyright que aparece en cada archivo fuente.

Estandares de codificación En el texto de este libro, los identificadores (nombres de fimciones, variables y clases) están en negrita. La mayoría de palabras clave también están en negrita, excepto en aquellos casos en que las palabras se usan tanto que ponerlas en negrita podría volverse tedioso, como es el caso de la palabra "clase". Para los ejemplos de este libro, uso un estilo de codificación bastante particular. Este estilo sigue al estilo que la propia Sun usa e n prácticamente todo el código d e sitio web (véase http://]ava.sun.com/docs/codeconv/index.html), y parece quc es1á supui-tado por la mayoría de entor-

nos de desarrollo Java. Si ha leído el resto de mis trabajos, también verá que el estilo de codificación de Sun coincide con el mío -esto me alegra, aunque no tenía nada que hacer con él. El aspecto del estilo de formato es bueno para lograr horas de tenso debate, por lo que simplemente diré que no pretendo dictar un estilo correcto mediante mis ejemplos; tengo mi propia motivación para usar el estilo que uso. Java es un lenguaje de programación de forma libre, se puede seguir usando cualquier estilo con el que uno esté a gusto. Los programas de este libro son archivos incluidos por el procesador de textos, directamente sacados de archivos compilados. Por tanto, los archivos de código impresos en este libro deberían funcionar sin errores de compilador. Los errores que deberían causar mensajes de error en tiempo de compilación están comentados o marcados mediante //!, por lo que pueden ser descubiertos fácilmente, y probados utilizando medios automáticos. Los errores descubiertos de los que ya se haya informado al autor, aparecerán primero en el código fuente distribuido y posteriormente en actualizaciones del libro (que también aparecerán en el sitio web http:llwww.BruceEckel.com).

Versiones de Java Generalmente confío en la implementación que Sun hace de Java como referencia para definir si un determinado comportamiento es o no correcto. Con el tiempo, Sun ha lanzado tres versiones principales de Java: la 1.0, la 1.1y la 2 (que se llama versión 2, incluso aunque las versiones del JDK de Sun siguen usando el esquema de numeración de 1.2, 1.3, 1.4, etc.). La versión 2 parece llevar finalmente a Java a la gloria, especialmente en lo que concierne a las herramientas de interfaces. Este libro se centra en, y está probado con, Java 2, aunque en ocasiones hago concesiones a las características anteriores de Java 2, de forma que el código pueda compilarse bajo Linux (vía el JDK de Linux que estaba disponible en el momento de escribir el libro). Si se necesita aprender versiones anteriores del lenguaje no cubiertas en esta edición, la primera edición del libro puede descargarse gratuitamente de http:llwww.BruceEckel.corn, y también está en el CD adjunto a este libro.


xlvi

Piensa en Java

Algo de lo que uno se dará cuenta es de que, cuando menciono versiones anteriores del lenguaje, no uso los números de sub-revisión. En este libro me referiré sólo a Java 1.0, 1.1 y 2, para protegerme de errores tipográficos producidos por sub-revisiones posteriores de estos productos.

Seminarios y m i papel como mentor Mi empresa proporciona seminarios de formación de cinco días, en máquina, públicos e in situ, basados en el material de este libro. Determinado material de cada capítulo representa una lección, seguida de un periodo de ejercicios guiados de forma que cada alumno recibe atención personal. Las conferencias y las diapositivas del seminario introductorio también están en el CD ROM para proporcional al menos alguna de la experiencia del seminario sin el viaje y el coste que conllevaría. Para más información, visitar http:llwww.BruceEckel.corn. Mi compañía también proporciona consultoría, servicios de orientación y acompañamiento para ayudar a guiar un proyecto a lo largo de su ciclo de desarrollo -especialmente indicado para el primer proyecto en Java de una empresa.

Errores Sin que importe cuántos trucos utiliza un escritor para detectar errores, siempre hay alguno que se queda ahí y que algún lector encontrará. Hay un formulario para remitir errores al principio de cada capítulo en la versión HTML del libro (y en el CD ROM unido al final de este libro, además de descargable de http:llwww.BruceEckel.corn) y también en el propio sitio web, en la página correspondiente a este libro. Si se descubre algo que uno piense que puede ser un error, por favor, utilice el formulario para remitir el error junto con la corrección sugerida. Si es necesario, incluya el archivo de código fuente original y cualquier modificación que se sugiera. Su ayuda será apreciada.

Nota sobre el diseño de la portada La portada de Piensa en Java está inspirada en el American Arts & Crafts Movement, que se fundó al cambiar de siglo y alcanzó su cenit entre los años 1900 y 1920. Empezó en Inglaterra como una reacción tanto a la producción de las máquinas de la Revolución Industrial y al estilo victoriano, excesivamente ornamental. Arts & Crafts hacía especial énfasis en el mero diseño, en las formas de la naturaleza tal y como se ven en el movimiento del Art Nouveau, las manualidades y la importancia del trabajo individual, y sin embargo sin renunciar al uso de herramientas modernas. Hay muchas réplicas con la situación de hoy en día: el cambio de siglo, la evolución de los principios puros de la revolución de los computadores a algo más refinado y más significativo para las personas individuales, y el énfasis en el arte individual que hay en el software, frente a su simple manufactura. Veo Java de esta misma forma: como un intento de elevar al programador más allá de la mecánica de un sistema operativo y hacia el "arte del software".


Introducción

xlvii

Tanto el autor como el diseñador del libro/portada (que han sido amigos desde la infancia) encuentran la inspiración en este movimiento, y ambos poseen muebles, lámparas y otros elementos que o bien son originales, o bien están inspirados en este periodo. El otro tema de la cubierta sugiere una caja de colecciones que podría usar un naturalista para mostrar los especímenes de insectos que ha guardado. Estos insectos son objetos, ubicados dentro de la caja de objetos. Los objetos caja están a su vez ubicados dentro del "objeto cubierta", que ilustra el concepto fundamental de la agregación en la programación orientada a objetos. Por supuesto, un programador no puede ayudar si no es produciendo "errores" en la asociación, y aquí los errores se han capturado siendo finalmente confinados en una pequeña caja de muestra, como tratando de mostrar la habilidad de Java para encontrar, mostrar y controlar los errores (lo cual es sin duda uno de sus más potentes atributos).

Agradecimientos En primer lugar, gracias a los asociados que han trabajado conmigo para dar seminarios, proporcionar consultoría y desarrollar productos de aprendizaje: Andrea Provaglio, Dave Bastlett (que también contribuyó significativamente al Capítulo 15), Bill Venners y Larry O'Brien. Aprecio vuestra paciencia a medida que sigo intentando desarrollar el mejor modelo para que tipos tan independientes como nosotros podamos trabajar juntos. Gracias a Rolf André Klaedtke (Suiza); Martin Vleck, Martin Byer, Vlada & Pavel Lahoda, Martin el Oso, y Hanka (Praga); y a Marco Cantu (Italia) por darme alojamiento durante mi primera gira seminario auto organizada por Europa. Gracias a la Doyle Street Cohousing Community por soportarme durante los dos años que me llevó escribir la primera edición de este libro (y por aguantarme en general). Muchas gracias a Kevin y Sonda Donovan por subarrendarme su magnífico lugar en Creste Butte, Colorado, durante el verano mientras trabajaba en la primera edición del libro. Gracias también a los amigables residentes de Crested Butte y al Rocky Mountain Biologial Laboratory que me hizo sentir tan acogido. Gracias a Claudette Moore de la Moore Literary Agency por su tremenda paciencia y perseverancia a la hora de lograr que yo hiciera exactamente lo que yo quería hacer. Mis dos primeros libros se publicaron con Jeff Pepper como editor de Osborne/McGraw-Hill. Jeff apareció en el lugar oportuno y en la hora oportuna en Prentice-Hall y me ha allanado el camino y ha hecho que ocurra todo lo que tenía que ocurrir para que ésta se convirtiera en una experiencia de publicación agradable. Gracias, Jeff -significa mucho para mí. Estoy especialmente en deuda con Gen Kiyooka y su compañía, Digigami, que me proporcionó gentilmente mi primer servidor web durante los muchos años iniciales de presencia en la Web. Esto constituyó una ayuda de valor incalculable. Gracias a Cay Hostmann (coautor de Core Java, Prentice-Hall, 2000), D'Arcy Smith (Symantec) y Paul Tyma (coautor de Java Primer Plus, The Waite Group, 1996), por ayudarme a aclarar conceptos sobre el lenguaje.


xlviii

Piensa en Java

Gracias a la gente que ha hablado en mi curso de Java en la Software Development Conference, y a los alumnos de mis cursos, que realizan las preguntas que necesito oír para poder hacer un material más claro. Gracias espaciales a Larry y Tina O'Brien, que me ayudaron a volcar mis seminarios en el CD ROM original Hands-On Java. (Puede encontrarse más información en http:llwww.BruceEckel.com.) Mucha gente me envió correcciones y estoy en deuda con todos ellos, pero envío gracias en particular a (por la primera edición): Kevin Raulerson (encontró cientos de errores enormes), Bob Resendes (simplemente increíble), John Pinto, Joe Dante, Jose Sharp (los tres son fabulosos), David Coms (muchas correcciones gramaticales y aclaraciones), Dr. Robert Stephenson, John Cook, Franklin Chen, Zev Griner, David Karr, Leander A. Stroschein, Steve Clark, Charles A. Lee, Austin Maher, Dennos P. Roth, Roque Oliveira, Douglas Dunn, Dejan Ristic, Neil Galarneau, David B. Malkovsky, Steve Wilkinson, y otros muchos. El profesor Marc Meurrens puso gran cantidad de esfuerzo en publicitar y hacer disponible la versión electrónica de la primera edición del libro en toda Europa. Ha habido muchísimos técnicos en mi vida que se han convertido en amigos y que también han sido, tanto influyentes, como inusuales por el hecho de que hacen yoga y practican otras formas de ejercicio espiritual, que yo también encuentro muy instructivo e inspirador. Son Karig Borckschmidt, Gen Kiyooka y Andrea Provaglio, (que ayuda en el entendimiento de Java y en la programación general en Italia, y ahora en los Estados Unidos como un asociado del equipo MindView). No es que me haya sorprendido mucho que entender Delphi me ayudara a entender Java, pues tienen muchas decisiones de diseño del lenguaje en común. Mis amigos de Delphi me proporcionaron ayuda facilitándome a alcanzar profundidad en este entorno de programación tan maravilloso. Son Marco Cantu (otro italiano -¿quizás aprender Latín es una ayuda para entender los lenguajes de programación?), Neil Rubenking (que solía hacer yoga, era vegetariano,. .. hasta que descubrió los computadores) y por supuesto, Zack Urlocker, un colega de hace tiempo con el que me he movido por todo el mundo. Las opiniones y el soporte de mi amigo Richard Hale Shaw han sido de mucha ayuda (y la de Kim también). Richard y yo pasamos muchos meses dando seminarios juntos e intentando averiguar cuál era la experiencia de aprendizaje perfecta desde el punto de vista de los asistentes. Gracias a KoAnn Vikoren, Eric Faurot, Marco Pardi, y el resto de equipo y tripulación de MFI. Gracias especialmente a Tara Arrowood, que me volvió a inspirar en las posibilidades de las conferencias. El diseño del libro, de la portada, y la foto de ésta fueron creadas por mi amigo Daniel Hill-Harris, que solía jugar con letras de goma en autor y diseñador de renombre (http:llwww.Wil-Harris.com), el colegio mientras esperaba a que se inventaran los computadores y los ordenadores personales, y se quejaba de que yo siempre estuviera enmarañado con mis problemas de álgebra. Sin embargo, he producido páginas listas para la cámara por mí mismo, por lo que los errores de tipografía son míos. Para escribir el libro se us6 Microsoft 8Word 97 for Windows, y para crear páginas listas para fotografiar en Adobe Acrobat; el libro se creó directamente a partir de los ficheros Acrobat PDE (Como un tributo a la edad electrónica, estuve fuera en las dos ocasiones en que se produjo la versión final del libro -la primera edición se envío desde Capetown, Sudáfrica, y la segunda edición se


Introducción

xlix

envío desde Praga.) La tipología del cuerpo es Georgia y los títulos están en Vérdana. La tipografía de la portada es ITC Rennie Mackintosch. Gracias a los vendedores que crearon los compiladores. Borland, el Blackdown Group (para Linux), y por supuesto, Sun.

Gracias especiales a todos mis profesores y alumnos (que son a su vez mis profesores). La persona que me enseñó a escribir fue Gabrielle Rico (autora de Writing the Natural Way, Putnam, 1985). Siempre guardaré como un tesoro aquella terrorífica semana en Esalen. El conjunto de amigos que me han ayudado incluyen, sin ser los únicos a: Andrew Binstock, Steve Sinofsky, JD Hildebrandt, Tom Keffer, Brian McElhinney, Brinckely Barr, Hill Gates de Midnight Engineering Magazine, Larry Constantine y Lucy Lockwood, Grez Perry, Dan Putterman, Christi Westphal, GeneWang, Dave Mayer, David Intersiomne, Andrea Rosenfield, Claire Sawyers, más italianos (Laura Fallai, Corrado, ILSA, y Cristina Guistozzi). Chris y Laura Strand, los Alrnquists, Brad Jerbic, Marilyn Cvitanic, los Mabrys, los Haflingers, los Pollocks, Peter Vinci, las familias Rohhins, las familias Moelter (y los McMillans), Michael Wilk, Dave Stoner, Laurie Adams, los Cranstons,

Larry Fogg, Mike y Karen Sequeiro, Gary Entsminger y Allison Brody, Kevin Donovan y Sonda Eastlack, Chester y Shannon Andersen, Joe Lordy, Dave y Brenda Bartlett, David Lee, los Rentschlers, los Sudeks, Dick, Patty y Lee Eckel, Lynn y Todd y sus familias. Y por supuesto, papá y mamá.

Colaboradores I n t e r n e t Gracias a aquellos que me han ayudado a reescribir los ejemplos para usar la biblioteca Swing, y por cualquier otra ayuda: Jon Shvarts, Thomas Kirsch, Rahim Adatia, Rajes Jain, Ravi Manthena, Banu Rajarnani, Jens Brandt, Mitin Shivaram, Malcolm Davis y todo el mundo que mostró su apoyo. Verdaderamente, esto me ayudó a dar el primer salto.


1: Introducción a los objetos La génesis de la revolución de los computadores se encontraba en una máquina, y por ello, la génesis de nuestros lenguajes de programación tiende a parecerse a esa máquina. Pero los computadores, más que máquinas, pueden considerarse como herramientas que permiten ampliar la mente ("bicicletas para la mente", como se enorgullece de decir Steve Jobs), además de un medio de expresión inherentemente diferente. Como resultado, las herramientas empiezan a parecerse menos a máquinas y más a partes de nuestra mente, al igual que ocurre con otros medios de expresión como la escritura, la pintura, la escultura, la animación o la filmación de películas. La

programación orientada a objetos (POO) es una parte de este movimiento dirigido a utilizar los computadores como si de un medio de expresión s e tratara.

Este capítulo introducirá al lector en los conceptos básicos de la POO, incluyendo un repaso a los métodos de desarrollo. Este capítulo y todo el libro, toman como premisa que el lector ha tenido experiencia en algún lenguaje de programación procedural (por procedimientos), sea C u otro lenguaje. Si el lector considera que necesita una preparación mayor en programación y/o en la sintaxis de C antes de enfrentarse al presente libro, se recomienda hacer uso del CD ROM formativo Thinking in C: Foundations for C++ and Java, que se adjunta con el presente libro, y que puede encontrarse también la URL, http://www.BruceEckel.com. Este capítulo contiene material suplementario, o de trasfondo (background). Mucha gente no se siente cómoda cuando se enfrenta a la programación orientada a objetos si no entiende su contexto, a grandes rasgos, previamente. Por ello, se presentan aquí numerosos conceptos con la intención de proporcionar un repaso sólido a la POO. No obstante, también es frecuente encontrar a gente que no acaba de comprender los conceptos hasta que tiene acceso a los mecanismos; estas personas suelen perderse si no se les ofrece algo de código que puedan manipular. Si el lector se siente identificado con este último grupo, estará ansioso por tomar contacto con el lenguaje en sí, por lo que debe sentirse libre de saltarse este capítulo -lo cual no tiene por qué influir en la comprensión que finalmente se adquiera del lenguaje o en la capacidad de escribir programas en él mismo. Sin embargo, tarde o temprano tendrá necesidades ocasionales de volver aquí, para completar sus nociones en aras de lograr una mejor comprensión de la importancia de los objetos y de la necesidad de comprender cómo acometer dise��os haciendo uso de ellos.

El progreso d e la abstracción Todos los lenguajes de programación proporcionan abstracciones. Puede incluso afirmarse que la complejidad de los problemas a resolver es directamente proporcional a la clase (tipo) y calidad de las abstracciones a utilizar, entendiendo por tipo "clase", aquello que se desea abstraer. El lenguaje


2

Piensa en Java

ensamblador es una pequeña abstracción de la máquina subyacente. Muchos de los lenguajes denominados "imperativos" desarrollados a continuación del antes mencionado ensamblador (como Fortran, BASIC y C) eran abstracciones a su vez del lenguaje citado. Estos lenguajes supusieron una gran mejora sobre el lenguaje ensamblador, pero su abstracción principal aún exigía pensar en términos de la estructura del computador más que en la del problema en sí a resolver. El programador que haga uso de estos lenguajes debe establecer una asociación entre el modelo de la máquina (dentro del "espacio de la solución", que es donde se modela el problema, por ejemplo, un computador) y el modelo del problema que de hecho trata de resolver (en el "espacio del problema", que es donde de hecho el problema existe). El esfuerzo necesario para establecer esta correspondencia, y el hecho de que éste no es intrínseco al lenguaje de programación, es causa directa de que sea difícil escribir programas, y de que éstos sean caros de mantener, además de fomentar, como efecto colateral (lateral), toda una la industria de "métodos de programación".

La alternativa al modelado de la máquina es modelar el problema que se trata de resolver. Lenguajes primitivos como LISP o APL eligen vistas parciales o particulares del mundo (considerando respectivamente que los problemas siempre se reducen a "listas" o a "algoritmos"). PROLOG convierte todos los problemas en cadenas de decisiones. Los lenguajes se han creado para programación basada en limitaciones o para programar exclusivamente mediante la manipulación de símbolos gráficos (aunque este último caso resultó ser excesivamente restrictivo). Cada uno de estos enfoques constituyc una solución buena para determinadas clases (tipos) de problemas (aquéllos para cuya snlii-

ción fueron diseñados), pero cuando uno trata de sacarlos de su dominio resultan casi impracticables. El enfoque orientado a objetos trata de ir más allá, proporcionando herramientas que permitan al programador representar los elementos en el espacio del problema. Esta representación suele ser lo suficientemente general como para evitar al programador limitarse a ningún tipo de problema específico. Nos referiremos a elementos del espacio del problema, denominando "objetos" a sus representaciones dentro del espacio de la solución (por supuesto, también serán necesarios otros objetos que no tienen su análogo dentro del espacio del problema). La idea es que el programa pueda autoadaptarse al lingo del problema simplemente añadiendo nuevos tipos de objetos, de manera que la mera lectura del código que describa la solución constituya la lectura de palabras que expresan el problema. Se trata, en definitiva, de una abstracción del lenguaje mucho más flexible y potente que cualquiera que haya existido previamente. Por consiguiente, la PO0 permite al lector describir el problema en términos del propio problema, en vez de en términos del sistema en el que se ejecutará el programa final. Sin embargo, sigue existiendo una conexión con el computador, pues cada objeto puede parecer en sí un pequeño computador; tiene un estado, y se le puede pedir que lleve a cabo determinadas operaciones. No obstante, esto no quiere decir que nos encontremos ante una mala analogía del mundo real, al contrario, los objetos del mundo real también tienen características y comportamientos. Algunos diseñadores de lenguajes han dado por sentado que la programación orientada a objetos, de por sí, no es adecuada para resolver de manera sencilla todos los problemas de programación, y hacen referencia al uso de lenguajes de programación multiparadigmal.

' N. del autor: Ver Multiparadigrn Programming in Leda, por Timothy Budd (Addison-Wesley, 1995).


1: Introducción a los objetos

3

Alan Kay resumió las cinco características básicas de Smalltalk, el primer lenguaje de programación orientado a objetos que tuvo éxito, además de uno de los lenguajes en los que se basa Java. Estas características constituyen un enfoque puro a la programación orientada a objetos: Todo es un objeto. Piense en cualquier objeto como una variable: almacena datos, permite

que se le "hagan peticiones", pidiéndole que desempeñe por sí mismo determinadas operaciones, etc. En teoría, puede acogerse cualquier componente conceptual del problema a resolver (bien sean perros, edificios, servicios, etc.) y representarlos como objetos dentro de un programa. Un programa es un cúmulo de objetos que se dicen entre sí lo que tienen que hacer mediante el envío de mensajes. Para hacer una petición a un objeto, basta con "enviarle un

mensaje". Más concretamente, puede considerarse que un mensaje en sí es una petición para solicitar una llamada a una función que pertenece a un objeto en particular. Cada objeto tiene su propia memoria, constituida por otros objetos. Dicho de otra manera, uno crea una nueva clase de objeto construyendo un paquete que contiene objetos ya existentes. Por consiguiente, uno puede incrementar la complejidad de un programa, ocultándola tras la simplicidad de los propios objetos. Todo objeto es de algún tipo. Cada objeto es un elemento de una clase, entendiendo por "clase" un sinónimo de "tipo". La característica más relevante de una clase la constituyen "el conjunto de mensajes que se le pueden enviar". Todos los objetos de determinado tipo pueden recibir los mismos mensajes. Ésta es una afirmación de enorme trascendencia como se verá más tarde. Dado que un objeto de tipo "círculo" es también un objeto de tipo "polígono", se garantiza que todos los objetos "círculo" acepten mensajes propios de "polígono". Esto permite la escritura de código que haga referencia a polígonos, y que de manera automática pueda manejar cualquier elemento que encaje con la descripción de "polígono". Esta capacidad de suplantación es uno de los conceptos más potentes de la POO.

Todo objeto tiene una interfaz Aristóteles fue probablemente el primero en estudiar cuidadosamente el concepto de tipo; hablaba de "la clase de los pescados o la clase de los peces". La idea de que todos los objetos, aún siendo únicos, son también parte de una clase de objetos, todos ellos con características y comportamientos en común, ya fue usada en el primer lenguaje orientado a objetos, Simula-67, que ya incluye la palabra clave clase, que permite la introducción de un nuevo tipo dentro de un programa. Simula, como su propio nombre indica, se creó para el desarrollo de simulaciones, como el clásico del cajero de un banco, en el que hay cajero, clientes, cuentas, transacciones y unidades monetarias -un montón de "objetos". Todos los objetos que, con excepción de su estado, son idénticos durante la ejecución de un programa se agrupan en "clases de objetos", que es precisamente de donde proviene la palabra clave clase. La creación de tipos abstractos de datos (clases) es un concepto fun-


4

Piensa en Java

damental en la programación orientada a objetos. Los tipos abstractos de datos funcionan casi como los tipos de datos propios del lenguaje: es posible la creación de variables de un tipo (que se denominan objetos o instancias en el dialecto propio de la orientación a objetos) y manipular estas variables (mediante el envío o recepción de mensajes; se envía un mensaje a un objeto y éste averigua que debe hacer con él). Los miembros (elementos) de cada clase comparten algunos rasgos comunes: toda cuenta tiene un saldo, todo cajero puede aceptar un ingreso, etc. Al mismo tiempo, cada miembro tiene su propio estado, cada cuenta tiene un saldo distinto, cada cajero tiene un nombre. Por consiguiente, los cajeros, clientes, cuentas, transacciones, etc. también pueden ser representados mediante una entidad única en el programa del computador. Esta entidad es el objeto, y cada objeto pertenece a una clase particular que define sus características y comportamientos. Por tanto, aunque en la programación orientada a objetos se crean nuevos tipos de datos, virtual-

mente todos los lenguajes de programación orientada a objetos hacen uso de la palabra clave "clase". Siempre que aparezca la palabra clave "tipo" (type) puede sustituirse por "clase" (class) y viceversa2. Dado que una clase describe a un conjunto de objetos con características (datos) y comportamientos (funcionalidad) idénticos, una clase es realmente un tipo de datos, porque un número en coma flotante, por ejemplo, también tiene un conjunto de características y comportamientos. La diferencia radica en que un programador define una clase para que encaje dentro de un problema en vez de verse forzado a utilizar un tipo de datos existente que fue diseñado para representar una unidad de almacenamiento en una máquina. Es posible extender el lenguaje de programación añadiendo nuevos tipos de datos específicos de las necesidades de cada problema. El sistema de programación acepta las nuevas clases y las cuida, y' asigna las comprobaciones que da a los tipos de datos predefinidos. El enfoque orientado a objetos no se limita a la construcción de simulaciones. Uno puede estar de acuerdo o no con la afirmación de que todo programa es una simulación del sistema a diseñar, mientras que las técnicas de PO0 pueden reducir de manera sencilla un gran conjunto de problemas a una solución simple. Una vez que se establece una clase, pueden construirse tantos objetos de esa clase como se desee, y manipularlos como si fueran elementos que existen en el problema que se trata de resolver. Sin

duda, 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 en el espacio de la solución. Pero, ¿cómo se consigue que un objeto haga un trabajo útil para el programador? Debe de haber una forma de hacer peticiones al objeto, de manera que éste desempeñe alguna tarea, como completar una transacción, dibujar algo en la pantalla o encender un interruptor. Además, cada objeto sólo puede satisfacer determinadas peticiones. Las peticiones que se pueden hacer a un objeto se encuentran definidas en su interfaz, y es el tipo de objeto el que determina la interfaz. Un ejemplo sencillo sería la representación de una bombilla: W g u n a s personas establecen una distinción entre ambos, remarcando que un tipo determina la interfaz, mientras que una clase e s una implementación particular de una interfaz.


1: Introducción a los objetos

Tipo

1

Luz

5

l

lnterfaz

Luz lz = new L u z lz e n c e n d e r ( ) ;

.

() ;

La interfaz establece qm6 peticiones pueden hacerse a un objeto particular Sin embargo, debe hacer

código en algún lugar que permita satisfacer esas peticiones. Este, junto con los datos ocultos, constituye la implementación. Desde el punto de vista de un lenguaje de programación procedural, esto no es tan complicado. Un tipo tiene una función asociada a cada posible petición, de manera que cuando se hace una petición particular a un objeto, se invoca a esa función. Este proceso se suele simplificar diciendo que se ha "enviado un mensaje" (hecho una petición) a un objeto, y el objeto averigua qué debe hacer con el mensaje (ejecuta el código). Aquí, el nombre del tipo o clase es Luz, el nombre del objeto Luz particular es lz, y las peticiones que pueden hacerse a una Luz son encender, apagar, brillar o atenuar. Es posible crear una Luz definiendo una "referencia" (12) a ese objeto e invocando a new para pedir un nuevo objeto de ese tipo. Para enviar un mensaje al objeto, se menta el nombre del objeto y se conecta al mensaje de petición mediante un punto. Desde el punto de vista de un usuario de una clase predefinida, éste es el no va más de la programación con objetos.

El diagrama anteriormente mostrado sigue el formato del Lenguaje de Modelado Unificado o Un@ed Modeling Language (UML). Cada clase se representa mediante una caja, en la que el nombre del tipo se ubica en la parte superior, cualquier dato necesario para describirlo se coloca en la parte central, y lasfknciones miembro (las funciones que pertenecen al objeto) en la parte inferior de la caja. A menudo, solamente se muestran el nombre de la clase y las funciones miembro públicas en los diagramas de diseño UML, ocultando la parte central. Si únicamente interesan los nombres de las clases, tampoco es necesario mostrar la parte inferior de la caja.

La implementación oculta Suele ser de gran utilidad descomponer el tablero de juego en creadores de clases (elementos que crean nuevos tipos de datos) y programadores clientes3 (consumidores de clases que hacen uso de los tipos de datos en sus aplicaciones). La meta del programador cliente es hacer uso de un gran repertorio de clases que le permitan acometer el desarrollo de aplicaciones de manera rápida. La

Nota del autor: Término acuñado por mi amigo Scott Meyers.


6

Piensa en Java

meta del creador de clases es construir una clase que únicamente exponga lo que es necesario al programador cliente, manteniendo oculto todo lo demás. ¿Por qué? Porque aquello que esté oculto no puede ser utilizado por el programador cliente, lo que significa que el creador de la clase puede modificar la parte oculta a su voluntad, sin tener que preocuparse de cualquier impacto que esta modificación pueda implicar. La parte oculta suele representar las interioridades de un objeto que podrían ser corrompidas por un programador cliente poco cuidadoso o ignorante, de manera que mientras se mantenga oculta su implementación se reducen los errores en los programas. En cualquier relación es importante determinar los límites relativos a todos los elementos involucrados. Al crear una biblioteca, se establece una relación con el programador cliente, que es tam-

bién un programador, además de alguien que está construyendo una aplicación con las piezas que se encueriLrari e11esta biblioteca, quizás

con la intención de construir una biblioteca aún mayor.

Si todos los miembros de una clase están disponibles para todo el mundo, el programador cliente puede hacer cualquier cosa con esa clase y no hay forma de imponer reglas. Incluso aunque prefiera que el programador cliente no pueda manipular alguno de los miembros de su clase, sin control de accesos, no hay manera de evitarlo. Todo se presenta desnudo al mundo. Por ello, la primera razón que justifica el control de accesos es mantener las manos del programador cliente apartadas de las porciones que no deba manipular -partes que son necesarias para las maquinaciones internas de los tipos de datos pero que no forman parte de la interfaz que los usuarios necesitan en aras de resolver sus problemas particulares. De hecho, éste es un servicio a los usuarios que pueden así ver de manera sencilla aquello que es sencillo para ellos, y qué es lo que pueden ignorar.

La segunda razón para un control de accesos es permitir al diseñador de bibliotecas cambiar el funcionamiento interno de la clase sin tener que preocuparse sobre cómo afectará al programador cliente. Por ejemplo, uno puede implementar una clase particular de manera sencilla para simplificar el desarrollo y posteriormente descubrir que tiene la necesidad de reescribirla para que se ejecute más rápidamente. Si tanto la interfaz como la implementación están claramente separadas y protegidas, esto puede ser acometido de manera sencilla. Java usa tres palabras clave explícitas para establecer los límites en una clase: public, private y protected. Su uso y significado son bastante evidentes. Estos mod$cadores de acceso determinan quién puede usar las definiciones a las que preceden. La palabra public significa que las definiciones siguientes están disponibles para todo el mundo. El término private, por otro lado, significa que nadie excepto el creador del tipo puede acceder a esas definiciones. Así, private es un muro de ladrillos entre el creador y el programador cliente. Si alguien trata de 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 miembros protected pero no a los private. La herencia será definida algo más adelante. Java también tiene un "acceso por defecto", que se utiliza cuando no se especifica ninguna de las palabras clave descritas en el párrafo anterior. Este modo de acceso se suele denominar "amistoso" o friendly porque implica que las clases pueden acceder a los miembros amigos de otras clases que


1: Introducción a los objetos

7

estén en el mismo package o paquete, sin embargo, fuera del paquete, estos miembros amigos se convierten en private.

Una vez que se ha creado y probado una clase, debería (idealmente) representar una unidad útil de código. Resulta que esta reutilización no es siempre tan fácil de lograr como a muchos les gustaría; producir un buen diseño suele exigir experiencia y una visión profunda de la problemática. Pero si se logra un diseño bueno, parece suplicar ser reutilizado. La reutilización de código es una de las mayores ventajas que proporcionan los lenguajes de programación orientados a objetos.

La manera más simple de reutilizar una clase es simplemente usar un objeto de esa clase directamente, pero también es posible ubicar un objeto de esa clase dentro de otra clase. Esto es lo que se denomina la "creación de un objeto miembro ". La nueva clase puede construirse a partir de un número indefinido de otros objetos, de igual o distinto tipo, en cualquier combinación necesaria para lograr la funcionalidad deseada dentro de la nueva clase. Dado que uno está construyendo una nueva clase a partir de otras ya existentes, a este concepto se le denomina composición (o, de forma más general, agregación). La composición se suele representar mediante una relación "es-parte-de", como en "el motor es una parte de un coche" ( "carro" en Latinoamérica).

(Este diagrama UML indica la composición, y el rombo relleno indica que hay un coche. Normalmente, lo representaré simplemente mediante una línea, sin el diamante, para indicar una asociación4.)

La composición conlleva una gran carga de flexibilidad. Los objetos miembros de la nueva clase suelen ser privados, haciéndolos inaccesibles a los programadores cliente que hagan uso de la clase. Esto permite cambiar los miembros sin que ello afecte al código cliente ya existente. También es posible cambiar los objetos miembros en tiempo de ejecución, para así cambiar de manera dinámica el comportamiento de un programa. La herencia, que se describe a continuación, no tiene esta flexibilidad, pues el compilador debe emplazar restricciones de tiempo de compilación en las clases que se creen mediante la 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 un programador novato puede llegar a pensar que hay que ha"ste ya suele ser un nivel de detalle suficiente para la gran mayoría de diagramas, de manera que no e s necesario indicar de manera explícita si se está utilizando agregación o composición.


8

Piensa en Java

cer uso de la misma en todas partes. Este pensamiento puede provocar que se elaboren diseños poco elegantes y desmesuradamente complicados. Por el contrario, primero sería recomendable intentar hacer uso de la composición, mucho más simple y sencilla. Siguiendo esta filosofía se lograrán diseños mucho más limpios. Una vez que se tiene cierto nivel de experiencia, la detección de los casos que precisan de la herencia se convierte en obvia.

Herencia: reutilizar

interfaz

Por sí misma, la idea de objeto es una herramienta más que buena. Permite empaquetar datos y funcionalidad juntos por concepto, de manera que es posible representar cualquier idea del espacio del problema en vez de verse forzado a utilizar idiomas propios de la máquina subyacente. Estos conceptos se expresan como las unidades fundamentales del lenguaje de programación haciendo uso de la palabra clave class. Parece una pena, sin embargo, acometer todo el problema para crear una clase y posteriormente verse forzado a crear una nueva que podría tener una funcionalidad similar. Sería mejor si pudiéramos hacer uso de una clase ya existente, clonarla, y después hacer al "clon" las adiciones y modificaciones que sean necesarias. Efectivamente, esto se logra mediante la herencia, con la excepción de que si se cambia la clase original (denominada la clase base, clase super o clase padre), el "clon" modificado (denominado clase derivada, clase heredada, subclase o clase hijo) también reflejaría esos cambios.

E Derivada

(La flecha de este diagrama UML apunta de la clase derivada a la clase base. Como se verá, puede haber más de una clase derivada.) Un tipo hace más que definir los limites de un conjunto de objetos; también tiene relaciones con otros tipos. Dos tipos pueden tener características y comportamientos en común, pero un tipo puede contener más características que otro y también puede manipular más mensajes (o gestionarlos de manera distinta). La herencia expresa esta semejanza entre tipos haciendo uso del concepto de tipos base y tipos derivados. Un tipo base contiene todas las características y comportamientos que comparten los tipos que de él se derivan. A partir del tipo base, es posible derivar otros tipos para expresar las distintas maneras de llevar a cabo esta idea.


1: Introducción a los objetos

9

Por ejemplo, una máquina de reciclaje de basura clasifica los desperdicios. El tipo base es "basura", y cada desperdicio tiene su propio peso, valor, etc. y puede ser fragmentado, derretido o descompuesto. Así, se derivan tipos de basura más específicos que pueden tener características adicionales (una botella tiene un color), o comportamientos (el aluminio se puede modelar, una lata de acero tiene capacidades magnéticas). Además, algunos comportamientos pueden ser distintos (el valor del papel depende de su tipo y condición). El uso de la herencia permite construir una jerarquía de tipos que expresa el problema que se trata de resolver en términos de los propios tipos. Un segundo ejemplo es el clásico de la "figura geométrica" utilizada generalmente en sistemas de diseño por computador o en simulaciones de juegos. El tipo base es "figura" y cada una de ellas tiene un tamaño, color, posición, etc. Cada figura puede dibujarse, borrarse, moverse, colorearse, etc. A partir de ésta, se pueden derivar (heredar) figuras específicas: círculos, cuadrados, triángulos, etc., pudiendo tener cada uno de los cuales características y comportamientos adicionales. Algunos comportamientos pueden ser distintos, como pudiera ser el cálculo del área de los distintos tipos de figuras. La jerarquía de tipos engloba tanto las similitudes como las diferencias entre las figuras.

1

1

Círculo

11

Figura

Cuadrado

l

"

Triánguio

1

Representar la solución en los mismos términos que el problema es tremendamente beneficioso puesto que no es necesario hacer uso de innumerables modelos intermedios para transformar una descripción del problema en una descripción de la solución. Con los objetos, el modelo principal lo constituye la jerarquía de tipos, de manera que se puede ir directamente de la descripción del sistema en el mundo real a la descripción del sistema en código. Sin duda, una de las dificultades que tiene la gente con el diseño orientado a objetos es la facilidad con que se llega desde el principio al final: las mentes entrenadas para buscar soluciones completas suelen verse aturdidas inicialmente por esta simplicidad.

Al heredar a partir de un tipo existente, se crea un nuevo tipo. Este nuevo tipo contiene no sólo los miembros del tipo existente (aunque los datos privados "private"están ocultos e inaccesibles) sino lo que es más importante, duplica la interfaz de la clase base. Es decir, todos los mensajes que pueden ser enviados a objetos de la clase base también pueden enviarse a los objetos de la clase derivada. Dado que sabemos el tipo de una clase en base a los mensajes que se le pueden enviar, la cla-


10

Piensa en Java

se derivada es del mismo tipo que la clase base. Así, en el ejemplo anterior, "un círculo en una figura". Esta equivalencia de tipos vía herencia es una de las pasarelas fundamentales que conducen al entendimiento del significado de la programación orientada a objetos. Dado que, tanto la clase base como la derivada tienen la misma interfaz, debe haber alguna implementación que vaya junto con la interfaz. Es decir, debe haber algún código a ejecutar cuando un objeto recibe un mensaje en particular. Si simplemente se hereda la clase sin hacer nada más, los métodos de la interfaz de la clase base se trasladan directamente a la clase derivada. Esto significa que los objetos de la clase derivada no sólo tienen el mismo tipo sino que tienen el mismo comportamiento, aunque este hecho no es particularmente interesante. Hay dos formas de diferenciar una clase nueva derivada de la clase base original. El primero es bas-

tante evidente: se añaden nuevas funciones a la clase derivada. Estas funciones nuevas no forman parte de la interfaz de la clase base, lo que significa que la clase base simplemente no hacía todo lo que ahora se deseaba, por lo que fue necesario añadir nuevas funciones. Ese uso simple y primitivo rle la herencia es, en ocasiones, la solución perfecta a los problemas. Sin embargo, debería considerarse detenidamente la posibilidad de que la clase base llegue también a necesitar estas funciones adicionales. Este proceso iterativo y de descubrimiento de su diseño es bastante frecuente en la programación orientada a objetos.

r-7 Figura

dibujar () borrar () getColor () setColor ()

E

Círculo

1 l

Cuadrado

1m Triángulo

Girarvertical () GirarHorizontal ()

Aunque la herencia puede implicar en ocasiones (especialmente en Java, donde la palabra clave que hace referencia a la misma es extends) que se van a añadir funcionalidades a una interfaz, esto no tiene por qué ser siempre así. La segunda y probablemente más importante manera de diferenciar una nueva clase es variar el comportamiento de una función ya existente en la clase base. A esto se le llama redefinición5 (anulación o superposición) de la función. Para redefinir una función simplemente se crea una nueva definición de la función dentro de la clase derivada. De esta manera puede decirse que "se está utilizando la misma función de la interfaz pero se desea que se comporte de manera distinta dentro del nuevo tipo". ' En el original ouerriding

(N. del T.).


1: Introducción a los objetos

11

Figura dibujar () borrar () getColor () setColor ()

m1 Círculo

dibujar () borrar ()

dibujar () borrar ()

I1 I1

Triángulo dibujar ( )

1 1 borrar ()

1

La relación es-un frente a la relación es-como-un Es habitual que la herencia suscite un pequeño debate: ¿debería la herencia superponer sólo las funciones de la clase base (sin añadir nuevas funciones miembro que no se encuentren en ésta)? Esto significaría que el tipo derivado sea exactamente el mismo tipo que el de la clase base, puesto que tendría exactamente la misma interfaz. Consecuentemente, es posible sustituir un objeto de la clase derivada por otro de la clase base. A esto se le puede considerar sustitución pura, y a menudo se le llama el principio de sustitución. De cierta forma, ésta es la manera ideal de tratar la herencia. Habitualmente, a la relación entre la clase base y sus derivadas que sigue esta filosofía se le denomina relación es-un, pues es posible decir que "un círculo es un polígono". Una manera de probar la herencia es determinar si es posible aplicar la relación es-un a las clases en liza, y tiene sentido. Hay veces en las que es necesario añadir nuevos elementos a la interfaz del tipo derivado, extendiendo así la interfaz y creando un nuevo tipo. Éste puede ser también sustituido por el tipo base, pero la sustitución no es perfecta pues las nuevas funciones no serían accesibles desde el tipo base. Esta relación puede describirse como la relación es-como-un" el nuevo tipo tiene la interfaz del viejo pero además contiene otras funciones, así que no se puede decir que sean exactamente iguales. Considérese por ejemplo un acondicionador de aire. Supongamos que una casa está cableada y tiene las botoneras para refrescarla, es decir, tiene una interfaz que permite controlar la temperatura. Imagínese que se estropea el acondicionador de aire y se reemplaza por una bomba de calor que puede tanto enfriar como calentar. La bomba de calor es-como-un acondicionador de aire, pero puede hacer más funciones. Dado que el sistema de corilrol de la casa está diseñado exclusivamenle para controlar el enfriado, se encuentra restringido a la comunicación con la parte "enfriadora" del nuevo objeto. Es necesario cxtender la interfaz del nuevo objeto, y el sistema existente únicamente conoce la interfaz original. Término acuñado por el autor.


12

Piensa en Java

1

Termostato

1

~ontrola

I

Sistema de enfriado

1 enfriar O

1

Acondicionador de aire

1

/ 1

Bomba de Calor

enfriar ()

Por supuesto, una vez que uno ve este diseño, está claro que la clase base "sistema de enfriado" no es lo suficientemente general, y debería renombrarse a "sistema de control de temperatura" de manera que también pueda incluir calentamiento -punto en el que el principio de sustitución funcionará. Sin embargo, este diagrama es un ejemplo de lo que puede ocurrir en el diseño y en el mundo real. Cuando se ve el principio de sustitución es fácil sentir que este principio (la sustitución pura) es la única manera de hacer las cosas, y de hecho, es bueno para los diseños que funcionen así. Pero hay veces que está claro que hay que añadir nuevas funciones a la interfaz de la clase derivada. Con la experiencia, ambos casos irán pareciendo obvios.

Objetos intercambiables con polimorfismo Al tratar con las jerarquías de tipos, a menudo se desea tratar un objeto no como el tipo específico del que es, sino como su tipo base. Esto permite escribir código que no depende de tipos específicos. En el ejemplo de los polígonos, las funciones manipulan polígonos genéricos sin que importe si son círculos, cuadrados, triángulos o cualquier otro polígono que no haya sido aún definido. Todos los polígono~pueden dibujarse, borrarse y moverse, por lo que estas funciones simplemente envían un mensaje a un objeto polígono; no se preocupan de qué base hace este objeto con el mensaje. Este tipo de código no se ve afectado por la adición de nuevos tipos, y esta adición es la manera más común de extender un programa orientado a objetos para que sea capaz de manejar nuevas situaciones. Por ejemplo, es posible derivar un nuevo subtipo de un polígono denominado pentágono sin tener que modificar las funciones que solamente manipulan polígonos genéricos. Esta capacidad para extender un programa de manera sencilla derivando nuevos subtipos es importante, ya que mejora considerablemente los diseños a la vez que reduce el coste del mantenimiento de software. Sin embargo, hay un problema a la hora dc tratar los objetos de tipos derivados como sus tipos base genéricos (los círculos como polígonos, las bicicletas como vehículos, los cormoranes como pájaros, etc.). Si una función va a decir a un polígono genérico que la dibuje, o a un vehículo genérico que se engrane, o a un pájaro genérico que se mueva, el compilador no puede determinar en tiempo de


1: Introducción a los objetos

13

compilación con exactitud qué fragmento de código se ejecutará. Éste es el punto clave -cuando se envía el mensaje, el programador no quiere saber qué fragmento de código se ejecutará; la función dibujar se puede aplicar de manera idéntica a un círculo, un cuadrado o un triángulo, y el objeto ejecutará el código correcto en función de su tipo específico. Si no es necesario saber qué fragmento de código se ejecutará, al añadir un nuevo subtipo, el código que ejecute puede ser diferente sin necesidad de modificar la llamada a la función. Por consiguiente, el compilador no puede saber exactamente qué fragmento de código se está ejecutando, ¿y qué es entonces lo que hace? Por ejemplo, en el diagrama siguiente, el objeto ControlaPájaros trabaja con objetos Pájaro genéricos, y no sabe exactamente de qué tipo son. Esto es conveniente para la perspectiva de ControlaPájaros pues no tiene que escribir código especial para determinar el tipo exacto de Pájaro con el que está trabajando, ni el comportamiento de ese Pájaro. Entonces, ¿cómo es que cuando se invoca a mover() ignorando el tipo específico de Pájaro se dará el comportamiento correcto (un Ganso corre, vuela o nada, y un Pingüino corre o nada)?

7 ?,Qué pasa al invocar a

F mover ()

l

Ganso

1 1 I

mover ()

I

Pingüino

1 1 mover ()

l

La respuesta es una de las principales novedades en la programación orientada a objetos: el compilador iiu p u d c liaccr uria llarriada a furiciúri eri el serilido lradicio~ial.La llarriada a íurici0ri genera-

da por un compilador no-PO0 hace lo que se denomina una ligadura temprana, un término que puede que el lector no haya visto antes porque nunca pensó que se pudiera hacer de otra forma. Esto significa que el compilador genera una llamada a una función con nombre específico, y el montador resuelve esta llamada a la dirección absoluta del código a ejecutar. En POO, el programa no puede determinar la dirección del código hasta tiempo de ejecución, por lo que es necesario otro esquema cuando se envía un mensaje a un objeto genérico. Para resolver el problema, los lenguajes orientados a objetos usan el concepto de ligadura tardía. Al enviar un mensaje a un objeto, no se determina el código invocado hasta tiempo de ejecución. El cornpilador se asegura de que la luriciím exisla y hace la comprobación de tipos de los argumentos y del valor de retorno (un lenguaje en el que esto no se haga así se dice que es débilmente tipificado), pero se desconoce el código exacto a ejecutar. Para llevar a cabo la ligadura tardía, Java utiliza un fragmento de código especial en vez de la llamada absoluta. Este código calcula la dirección del cuerpo de la función utilizando información almacenada en el objeto (este proceso se relata con detalle en el Capítulo 7). Por consiguiente, cada objeto puede comportarse de manera distinta en función de los contenidos de ese fragmento de có-


14

Piensa en Java

digo especial. Cuando se envía un mensaje a un objeto, éste, de hecho, averigua qué es lo que debe hacer con ese mensaje. En algunos lenguajes (C++ en particular) debe establecerse explícitamente que se desea que una función tenga la flexibilidad de las propiedades de la ligadura tardía. En estos lenguajes, por defecto, la correspondencia con las funciones miembro no se establece dinámicamente, por lo que es necesario recordar que hay que añadir ciertas palabras clave extra para lograr el polimorfismo. Considérese el ejemplo de los polígonos. La familia de clases (todas ellas basadas en la misma interfaz uniforme) ya fue representada anteriormente en este capítulo. Para demostrar el polimorfismo, se escribirá un único fragmento de código que ignora los detalles específicos de tipo y solamente hace referencia a la clase base. El código está desvinculado de información específica del tipo, y por consiguiente es más fácil de escribir y entender. Y si se añade un nuevo tipo -por ejemplo un Hexágono- mediante herencia, el código que se escriba trabajará tan perfectamente con el nuevo Polígono como lo hacia con los tipos ya exisle~iles,y por- curisiguiente, el programa es arrzpliable. Si se escribe un método en Java (y pronto aprenderá el lector a hacerlo): void hacerAlgo (Poligono p) p.borrar ( ) ; // ... p.dibujar ( ) ;

{

1 Esta función se entiende con cualquier Polígono, y por tanto, es independiente del tipo específico de objeto que esté dibujando y borrando. En cualquier otro fragmento del programa se puede usar la función hacerAlgo( ) : Circulo c = new Circulo() ; Triangulo t = new Triangulo Linea 1 = new Linea ( ) ; hacerAlgo (c); hacerAlgo (t); hacerAlgo (1);

() ;

Las llamadas a hacerAlgo( ) trabajan correctamente, independientemente del tipo de objeto. De hecho, éste es un truco bastante divertido. Considérese la línea:

Lo que está ocurriendo es que se está pasando un Círculo a una función que espera un Polígono. Como un Círculo es un Polígono, puede ser tratado como tal por hacerAlgo(). Es decir, cualquier mensaje que hacerAigo() pueda enviar a un Polígono, también podrá aceptarlo un Círculo. Por tanto, obrar así es algo totalmente seguro y lógico.

A este proceso de tratar un tipo derivado como si fuera el tipo base se le llama conversión de tipos (moldeado) hacia arriba7.El nombre moldear (cast) se utiliza en el sentído de moldear (convertir) un ' En el original inglés, casting.


1: Introducción a los objetos

15

molde, y es hacia arriba siguiendo la manera en que se representa en los diagramas la herencia, con el tipo base en la parte superior y las derivadas colgando hacia abajo. Por consiguiente, hacer un moldeado (casting)a la clase base es moverse hacia arriba por el diagrama de herencias: moldeado hacia arriba.

Un programa orientado a objetos siempre tiene algún moldeado hacia arriba pues ésta es la manera de desvincularse de tener que conocer el tipo exacto con que se trabaja en cada instante. Si se echa un vistazo al código de hacerAlgo() :

Obsérvese que no se dice "caso de ser un Círculo, hacer esto; caso de ser un Cuadrado, hacer esto otro, etc.". Si se escribe código que compruebe todos los tipos posibles que puede ser un Polígono, el tipo de código se complica, además de hacerse necesario modificarlo cada vez que se añade un nuevo tipo de Polígono. Aquí, simplemente se dice que "en el caso de los polígonos, se sabe que es posible aplicarles las operaciones de borrar() y dibujar(), eso sí, teniendo en cuenta todos los detalles de manera correcta".

Lo que más llama la atención del código de hacerAlgo() es que, de alguna manera, se hace lo correcto. Invocar a dibujar() para Círculo hace algo distinto que invocar a dibujar() para Cuadrado o Iínea, pero cuando se envía el mensaje dibujar() a un Polígono anónimo, se da el comportamiento correcto basándose en el tipo actual de Polígono. Esto es sorprendente porque, como se mencionó anteriormente, cuando el compilador de Java está compilando el código de hacerAlgo(), no puede saber exactamente qué tipos está manipulando. Por ello, generalmente, se espera que acabe invocando a la versión de borrar() y dibujar() de la clase base Polígono y no a las específicas de Círculo, Cuadrado y Iánea. Y sigue ocurriendo lo correcto por el polimorfismo. El compilador y el sistema de tiempo real se hacen cargo de los detalles; todo lo que hace falta saber es qué ocurre, y lo que es más importante, cómo diseñar haciendo uso de ello. Al enviar un mensaje a un objeto, el objeto hará lo correcto, incluso cuando se vea involucrado el moldeado hacia arriba.

Clases base abstractas e interfaces A menudo es deseable que la clase base únicamente presente una interfaz para sus clases derivadas. Es decir, no se desea que nadie cree objetos de la clase base, sino que sólo se hagan moldeados ha-


16

Piensa en Java

cia arriba de la misma de manera que se pueda usar su interfaz. Esto se logra convirtiendo esa clase en abstracta usando la palabra clave abstract. Si alguien trata de construir un objeto de una clase abstracta el compilador lo evita. Esto es una herramienta para fortalecer determinados diseños. También es posible utilizar la palabra clave abstract para describir un método que no ha sido aún implementado -indicando "he aquí una función interfaz para todos los tipos que se hereden de esta clase, pero hasta la fecha no existe una implementación de la misma". Se puede crear un método abstracto sólo dentro de una clase abstracta. Cuando se hereda la clase, debe implementarse el método o de lo contrario también la clase heredada se convierte en abstracta. La creación de métodos abstractos permite poner un método en una interfaz sin verse forzado a proporcionar un fragmento de código, posiblemente sin significado, para ese método.

La palabra clave interface toma el concepto de clase abstracta un paso más allá, evitando totalmente las definiciones de funciones. La interfaz es una herramienta muy útil y utilizada, ya que proporcionar la separación perfecta entre interfaz e implementación. Además. si se desea. es posible combinar muchos elementos juntos mientras que no es posible heredar de múltiples clases normales o abstractas.

Localización de objetos y longevidad Técnicamente, la PO0 consiste simplemente en tipos de datos abstractos, herencia y polimorfismo, aunque también hay otros aspectos no menos importantes. El resto de esta sección trata de analizar esos aspectos. Uno de los factores más importantes es la manera de crear y destruir objetos. ¿Dónde están los datos de un objeto y cómo se controla su longevidad (tiempo de vida)? En este punto hay varias filosofías de trabajo. En C++ el enfoque principal es el control de la eficiencia, proporcionando una alternativa al programador. Para lograr un tiempo de ejecución óptimo, es posible determinar el espacio de almacenamiento y la longevidad en tiempo de programación, ubicando los objetos en la pila (creando las variables scoped o automatic) o en el área de almacenamiento estático. De esta manera se prioriza la velocidad de la asignación y liberación de espacio de almacenamiento, cuyo control puede ser de gran valor en determinadas situaciones. Sin embargo, se sacrifica en flexibilidad puesto que es necesario conocer la cantidad exacta de objetos, además de su longevidad y su tipo, mientras se escribe el programa. Si se está tratando de resolver un problema más general como un diseño asistido por computador, la gestión de un almacén o el control de tráfico aéreo, este enfoque resulta demasiado restrictivo. El segundo enfoque es crear objetos dinámicamente en un espacio de memoria denominado el montículo o montón (heap). En este enfoque, no es necesario conocer hasta tiempo de ejecución el numero de objetos necesario, cuál es su longevidad o a qué tipo exacto pertenecen. Estos aspectos se deter-minar-ánjusto en el preciso momento en que se ejecute el programa. Si se necesita un nuevo o b jeto, simplemente se construye en el montículo en el instante en que sea necesario. Dado que el almacenamiento se gestiona dinámicamente, en tiempo de ejecución, la cantidad de tiempo necesaria


1: Introducción a los objetos

17

para asignar espacio de almacenamiento en el montículo es bastante mayor que el tiempo necesario para asignar espacio a la pila. (La creación de espacio en la pila suele consistir simplemente en una instrucción al ensamblador que mueve hacia abajo el puntero de pila, y otra para moverlo de nuevo hacia arriba.) El enfoque dinámico provoca generalmente el pensamiento lógico de que los objetos tienden a ser complicados, por lo que la sobrecarga debida a la localización de espacio de almacenamiento y su liberación no deberían tener un impacto significativo en la creación del objeto. Es más, esta mayor flexibilidad es esencial para resolver en general el problema de programación. Java utiliza exclusivamente el segundo enfoque8.Cada vez que se desea crear un objeto se usa la palabra clave new para construir una instancia dinámica de ese objeto. Hay otro aspecto, sin embargo, a considerar: la longevidad de un objeto. Con los lenguajes que permiten la creación de objetos en la pila, el compilador determina cuánto dura cada objeto y puede destruirlo cuando no es necesario. Sin embargo, si se crea en el montículo, el compilador no tiene conocimiento alguno sobre su longevidad. En un lenguaje como C++ hay que determinar en tiempo de programación cuándo destruir el objeto, lo cual puede conducir a fallos de memoria si no se hace de manera correcta & este problema es bastante común en los programas en C++).Java proporciona un recolector de basura que descubre automáticamente cuándo se ha dejado de utilizar un objeto, que puede, por consiguiente, ser destruido. Un recolector de basura es muy conveniente, al reducir el número de aspectos a tener en cuenta, así como la cantidad de código a escribir. Y lo que es más importante, el recolector de basura proporciona un nivel de seguridad mucho mayor contra el problema de los fallos de memoria (que ha hecho abandonar más de un proyecto en Ctt). El resto de esta sección se centra en factores adicionales relativos a la longevidad de los objetos y su localización.

Colecciones e iteradores Si se desconoce el número de objetos necesarios para resolver un problema en concreto o cuánto deben durar, también se desconocerá cómo almacenar esos objetos. ¿Cómo se puede saber el espacio a reservar para los mismos? De hecho, no se puede, pues esa información se desconocerá hasta tiempo de ejecución.

La solución a la mayoría de problemas de diseño en la orientación a objetos parece sorprendente: se crea otro tipo de objeto. El nuevo tipo de objeto que resuelve este problema particular tiene referencias a otros objetos. Por supuesto, es posible hacer lo mismo con un array, disponible en la mayoría de lenguajes. Pero hay más. Este nuevo objeto, generalmente llamado contenedor (llamado también colección, pero la biblioteca de Java usa este término con un sentido distinto así que este libro empleará la palabra "contenedor"), se expandirá a sí mismo cuando sea necesario para albergar cuanto se coloque dentro del contenedor. Simplemente se crea el objeto contenedor, y él se encarga de los detalles. Afortunadamente, un buen lenguaje PO0 viene con un conjunto de contenedores como parte del propio lenguaje. En C++,es parte de la Biblioteca Estándar C++ (Standard C++Library), que en oca-

' Los tipos primitivos, de los que se hablará más adelante, son un caso especial.


18

Piensa en Java

siones se denomina la Standard Template Library, Biblioteca de plantillas estándar, (STL). Object Pascal tiene contenedores en su Visual Component Library (VCL). Smalltalk tiene un conjunto de contenedores muy completo, y Java también tiene contenedores en su biblioteca estándar. En algunas bibliotecas, se considera que un contenedor genérico es lo suficientemente bueno para todas las necesidades, mientras que en otras (como en Java, por ejemplo) la biblioteca tiene distintos tipos de contenedores para distintas necesidades: un vector (denominado en Java ArrayList) para acceso consistente a todos los elementos, y una lista enlazada para inserciones consistentes en todos los elementos, por ejemplo, con lo que es posible elegir el tipo particular que satisface las necesidades de cada momento. Las bibliotecas de contenedores también suelen incluir conjuntos, colas, tablas de hasing, árboles, pilas, etc. Todos los contenedores tienen alguna manera de introducir y extraer cosas; suele haber funciones para añadir elementos a un contenedor, y otras para extraer de nuevo esos elementos. Pero sacar los elementos puede ser más problemático porque una función de selección única suele ser restrictiva. ¿Qué ocurre si se desea manipular o comparar un conjunto de eleinerilos del co~ite~iedor y no u ~ i osdo?

La solución es un iterador, que es un objeto cuyo trabajo es seleccionar los elementos de dentro de un contenedor y presentárselos al usuario del iterador. Como clase, también proporciona cierto nivel de abstracción. Esta abstracción puede ser usada para separar los detalles del contenedor del código al que éste está accediendo. El contenedor, a través del iterador, se llega a abstraer hasta convertirse en una simple secuencia, que puede ser recorrida gracias al iterador sin tener que preocuparse de la estructura subyacente - e s decir, sin preocuparse de si es un ArrayLBst (lista de arrays), un LinkedList (lista enlazado), un Stack, (pila) u otra cosa. Esto proporciona la flexibilidad de cambiar fácilmente la estructura de datos subyacente sin que el código de un programa se vea afectado. Java comenzó (en las versiones 1.0 y 1.1) con un iterador estándar denominado Enumeration,para todas sus clases contenedor. Java 2 ha añadido una biblioteca mucho más completa de contenedores que contiene un iterador denominado Iterator mucho más potente que el antiguo Enumeration. Desde el punto de vista del diseño, todo lo realmente necesario es una secuencia que puede ser manipulada en aras de resolver un problema. Si una secuencia de un sólo tipo satisface todas las necesidades de un problema, entonces no es necesario hacer uso de distintos tipos. Hay dos razones por las que es necesaria una selección de contenedores. En primer lugar, los contenedores proporcionan distintos tipos de interfaces y comportamientos externos. Una pila tiene una interfaz y un comportamiento distintos del de las colas, que son a su vez distintas de los conjuntos y las listas. Cualquiera de éstos podría proporcionar una solución mucho más flexible a un problema. En segundo lugar, distintos contenedores tienen distintas eficiencias en función de las operaciones. El mejor ejemplo está en ArrayList y LinkedList. Ambos son secuencias sencillas que pueden tener interfaces y comportamientos externos idénticos. Pero determinadas operaciones pueden tener costes radicalmente distintos. Los accesos aleatorios a ArrayList tienen tiempos de acceso constante; se invierte el mismo tiempo independientemente del elemento seleccionado. Sin embargo, en una LinkedList moverse de elemento en elemento a lo largo de la lista para seleccionar uno al azar es altamente costoso, y es necesario muchísimo más tiempo para localizar un elemento cuanto más adelante se encuentre. Por otro lado, si se desea insertar un elemento en el medio de una secuencia, es mucho menos costoso hacerlo en un LinkedList quc cn un ArrayList. Esta y otras opera ciones tienen eficiencias distintas en función de la estructura de la secuencia subyacente. En la fase de diseño, podría comenzarse con una LinkedList y, al primar el rendimiento, cambiar a un


1: Introducción a los objetos

19

ArrayList. Dado que la abstracción se lleva a cabo a través de iteradores, es posible cambiar de uno a otro con un impacto mínimo en el código. Finalmente, debe recordarse que un contenedor es sólo un espacio de almacenamiento en el que colocar objetos. Si este espacio resuelve todas las necesidades, no importa realmente cómo está implementado (concepto compartido con la mayoría de tipos de objetos). Si se está trabajando en un entorno de programación que tiene una sobrecarga inherente debido a otros factores, la diferencia de costes entre ArrayList y LinkedList podría no importar. Con un único tipo de secuencia podría valer. Incluso es posible imaginar la abstracción contenedora "perfecta", que pueda cambiar su implementación subyacente automáticamente en función de su uso.

La jerarquía de raíz Única Uno de los aspectos de la P O 0 que se ha convertido especialmente prominente desde la irrupción de C++ es si todas las clases en última instancia deberían ser heredadas de una única clase base. En Java (como virtualmente en todos los lenguajes P O 0 ) la respuesta es "sí" y el nombre de esta última clase base es simplemente Object. Resulta que los beneficios de una jerarquía de raíz única son enormes. Todos los objetos en una jerarquía de raíz única tienen una interfaz en común, por lo que en última instancia son del mismo tipo. La alternativa (proporcionada por C++) es el desconocimiento de que todo pertenece al mismo tipo fundamental. Desde el punto de vista de la retrocompatibilidad, esto encaja en el modelo de C mejor, y puede pensarse que es menos restrictivo, pero cuando se desea hacer programación orientada a objetos pura, es necesario proporcionar una jerarquía completa para lograr el mismo nivel de conveniencia intrínseco a otros lenguajes POO. Y en cualquier nueva biblioteca de clases que se adquiera, se utilizará alguna interfaz incompatible. Hacer funcionar esta nueva interfaz en un diseño lleva un gran esfuerzo (y posiblemente herencia múltiple). Así que ¿merece la pena la "flexibilidad" extra de C++?Si se necesita -si se dispone de una gran cantidad de código en C- es más que valiosa. Si se empieza de cero, otras alternativas, como Java, resultarán mucho más productivas. Puede garantizarse que todos los objetos de una jerarquía de raíz única (como la proporcionada por Java) tienen cierta funcionalidad. Se sabe que es posible llevar a cabo ciertas operaciones básicas con todos los objetos del sistema. Una jerarquía de raíz única, junto con la creación de todos los objetos en el montículo, simplifica enormemente el paso de argumentos (uno de los temas más complicados de C++). Una jerarquía de raíz única simplifica muchísimo la implementación de un recolector de basura (incluido en Java). El soporte necesario para el mismo puede instalarse en la clase base, y el recolector de basura podrá así enviar los mensajes apropiados a todos los objetos del sistema. Si no existiera este tipo de jerarquía rii la posibilidad de rriariipular un objeto a través de reierencias, sería muy dificil implementar un recolector de basura. Dado que está garantizado que en tiempo de ejecución la información de tipos está en todos los objetos, jamás será posible encontrar un objeto cuyo tipo no pueda ser determinado. Esto es especial-


20

Piensa en Java

mente importante con operaciones a nivel de sistema, tales como el manejo de excepciones, además de proporcionar una gran flexibilidad a la hora de programar.

Bibliotecas de colecciones y soporte al fácil manejo de coIecciones Dado que un contenedor es una herramienta de uso frecuente, tiene sentido tener una biblioteca de contenedores construidos para ser reutilizados, de manera que se puede elegir uno de la estantería y enchufarlo en un programa determinado. Java proporciona una biblioteca de este tipo, que satisface la gran mayoría de necesidades.

Moldeado hacia abajo frente a plantillas/genéricos Yara lograr que estos contenedores sean reutilizables, guardan un tipo universal en Java ya mencionado anteriormente: Object (Objeto). La jerarquía de raíz única implica que todo sea un Object, de forma que un contenedor que tiene objetos de tipo Object puede contener de todo, logrando así que los contenedores sean fácil de reutilizar. Para utilizar uno de estos contenedores, basta con añadirle referencias a objetos y finalmente preguntar por ellas. Pero dado que el contenedor sólo guarda objetos de tipo Object, al añadir una referencia al contenedor, se hace un moldeado hacia arriba a Object, perdiendo por consiguiente su identidad. Al recuperarlo, se obtiene una referencia a Object y no una referencia al tipo que se había introducido. ¿Y cómo se convierte de nuevo en algo útil con la interfaz del objeto que se introdujo en el contenedor? En este caso, también se hace uso del moldeado, pero en esta ocasión en vez de hacerlo hacia arriba siguiendo la jerarquía de las herencias hacia un tipo más general, se hace hacia abajo, hacia un tipo más específico. Esta forma de moldeado se denomina moldeado hacia abajo. Con el moldeado hacia arriba, como se sabe, un Círculo, por ejemplo, es un tipo de Polígono, con lo que este tipo de moldeado es seguro, pero lo que no se sabe es si un Object es un Círculo o un Polígono, por lo que no es muy seguro hacer moldeado hacia abajo a menos que se sepa exactamente qué es lo que se está manipulando. Esto no es completamente peligroso, sin embargo, dado que si se hace un moldeado hacia abajo, a un tipo erróneo, se mostrará un error de tiempo de ejecución denominado excepción, que se describirá en breve. Sin embargo, al recuperar referencias a objetos de un contenedor, es necesario tener alguna manera de recordar exactamente lo que son para poder llevar a cabo correctamente un moldeado hacia abajo. El moldeado hacia abajo y las comprobaciones en tiempo de ejecución requieren un tiempo extra durante la ejecución del programa, además de un esfuerzo extra por parte del programador. 2No tendría sentido crear, de alguna manera, el contenedor de manera que conozca los tipos que guarda, eliminando la necesidad de hacer moldeado hacia abajo y por tanto, de que aparezca algún error? La solución la constituyen los tipos parametrizados, que son clases que el compilador puede adaptar automáticamente para que trabajen con tipos determinados. Por ejemplo, con un contenedor parametrizado, el compilador podría adaptar el propio contenedor para que solamente aceptara y per-


1: Introducción a los objetos

21

mitiera la recuperación de Polígonos. Los tipos parametrizados son un elemento importante en C++,en parte porque este lenguaje no tiene una jerarquía de raíz única. En C++, la palabra clave que implementa los tipos parametrizados es "template". Java actualmente no tiene tipos parametrizados pues se puede lograr lo mismo -aunque de manera complicada- explotando la unicidad de raíz de su jerarquía. Sin embargo, una propuesta actualmente en curso para implementar tipos parametrizados utiliza una sintaxis muy semejante a las plantillas (templates) de C++.

El dilema de las labores del hogar: ¿quién limpia la casa? Cada objeto requiere recursos simplemente para poder existir, fundamentalmente memoria. Cuando un objeto deja de ser necesario debe ser eliminado de manera que estos recursos queden disponibles para poder reutilizarse. En situaciones de programación sencillas la cuestión de cuándo eliminar un objeto no se antoja complicada: se crea el objeto, se utiliza mientras es necesario y posteriormente debe ser destruido. Sin embargo, no es difícil encontrar s i t u ~ i o n e en s las que esto se corriplica.

Supóngase, por ejemplo, que se está diseñando un sistema para gestionar el tráfico aéreo de un aeropuerto. (El mismo modelo podría funcionar también para gestionar paquetes en un almacén, o un sistema de alquiler de vídeos, o una residencia canina.) A primera vista, parece simple: construir un contenedor para albergar aviones, crear a continuación un nuevo avión y ubicarlo en el contenedor (para cada avión que aparezca en la zona a controlar). En el momento de eliminación, se borra (suprime) simplemente el objeto aeroplano correcto cuando un avión sale de la zona barrida. Pero quizás, se tiene otro sistema para guardar los datos de los aviones, datos que no requieren atención inmediata, como la función de control principal. Quizás, se trata de un registro del plan de viaje de todos los pequeños aviones que abandonan el aeropuerto. Es decir, se dispone de un segundo contenedor de aviones pequeños, y siempre que se crea un objeto avión también se introduce en este segundo contenedor si se trata de un avión pequeño. Posteriormente, algún proceso en segundo plano lleva a cabo operaciones con los objetos de este segundo contenedor cada vez que el sistema está ocioso. Ahora el problema se complica: jcómo se sabe cuándo destruir los objetos? Cuando el programa principal (el controlador) ha acabado de usar el objeto, puede que otra parte del sistema lo esté usando (o lo vaya a usar en un futuro). Este problema surge en numerosísimas ocasiones, y los sistemas de programación (como C++) en los que los objetos deben de borrarse explícitamente cuando acaba de usarlos, pueden volverse bastante complejos. Con Java, el problema de vigilar que se libere la memoria se ha implementado en el rccolector de basura (aunque no incluye otros aspectos de la supresión de objetos). El recolector "sabe" cuándo se ha dejado de utilizar un objeto y libera la memoria que ocupaba automáticamente. Esto (combinado con el hecho de que todos los objetos son heredados de la clase raíz única Object y con la existencia de una única forma de crear objetos, en el montículo) hace que el proceso de programar en Java sea mucho más sencillo que el hacerlo en C++.Hay muchas menos decisiones que tomar y menos obstáculos que sortear.


22

Piensa en Java

Los recolectores de basura frente a la eficiencia y flexibilidad Si todo esto es tan buena idea, ¿por qué no se hizo lo mismo en C++?Bien, por supuesto, hay un precio que pagar por todas estas comodidades de programación, y este precio consiste en sobrecarga en tiempo de ejecución. Como se mencionó anteriormente, en C++es posible crear objetos en la pila, y en este caso, éstos se eliminan automáticamente (pero no se dispone de la flexibilidad de crear tantos como se desee en tiempo de ejecución). La creación de objetos en la pila es la manera más eficiente de asignar espacio a los objetos y de liberarlo. La creación de objetos en el montículo puede ser mucho más costosa. Heredar siempre de una clase base y hacer que todas las llamadas a función sean polimórficas también conlleva un pequeño peaje. Pero el recolector de basura es un problema concreto pues nunca se sabe cuándo se va a poner en marcha y cuánto tiempo conllevará su ejecución. Esto significa que hay inconsistencias en los ratios (velocidad) de ejecución de los programas escritos en Java, por lo que éstos no pueden ser utilizados en determinadas situaciones, como por ejemplo, cuando el tiempo de ejecución de un programa e s uniformemente crítico. (Se tra-

ta de los programas denominados generalmente de tiempo real, aunque no todos los problemas de programación en tiempo real son tan rígidos.) Los diseñadores del lenguaje C++,trataron de ganarse a los programadores de C (en lo cual tuvieron bastante éxito), no quisieron añadir nuevas características al lenguaje que pudiesen influir en la velocidad o el uso de C++ en cualquier situación en la que los programadores pudiesen decantarse por C. Se logró la meta, pero a cambio de una mayor complejidad cuando se programa en C++.Java es más simple que C++,pero a cambio es menos eficiente y en ocasiones ni siquiera aplicable. Sin embargo, para un porcentaje elevado de problemas de programación, Java es la mejor elección.

Manejo d e excepciones: t r a t a r c o n errores El manejo de errores ha sido, desde el principio de la existencia de los lenguajes de programación, uno de los aspectos más difíciles de abordar. Dado que es muy complicado diseñar un buen esquema de manejo de errores, muchos lenguajes simplemente ignoran este aspecto, pasando el problema a los diseñadores de bibliotecas que suelen contestar con soluciones a medias que funcionan en la mayoría de situaciones, pero que pueden ser burladas de manera sencilla; es decir, simplemente ignorándolas. Un problema importante con la mayoría de los esquemas de tratamiento de errores es que dependen de la vigilancia del programador de cara al seguimiento de una convención preestablecida no especialmente promovida por el propio lenguaje. Si el programador no está atento -cosa que ocurre muchas veces, especialmente si se tiene prisa- es posible olvidar estos esquemas con relativa facilidad. El manejo de excepciones está íntimamente relacionado con el lenguaje de programación y a veces incluso con el sistema operativo. Una excepción es un objeto que es "lanzado", "arrojadowqdesde el lugar en que se produce el error, y que puede ser "capturado" por el gestor de excepción apropiado

Y

N. del traductor: en inglés se emplea el verbo throw.


1: Introducción a los objetos

23

diseñado para manejar ese tipo de error en concreto. Es como si la gestión de excepciones constituyera un cauce de ejecución diferente, paralelo, que puede tomarse cuando algo va mal. Y dado que usa un cauce de ejecución distinto, no tiene por qué interferir con el código de ejecución normal. De esta manera el código es más simple de escribir puesto que no hay que estar comprobando los errores continuamente. Además, una excepción lanzada no es como un valor de error devuelto por una función, o un indicador (bandera) que una función pone a uno para indicar que se ha dado cierta condición de error (éstos podrían ser ignorados). Una excepción no se puede ignorar, por lo que se garantiza que será tratada antes o después. Finalmente, las excepciones proporcionan una manera de recuperarse de manera segura de una situación anormal. En vez de simplemente salir, muchas veces es posible volver a poner las cosas en su sitio y reestablecer la ejecución del programa, logrando así que éstos sean mucho más robustos.

El manejo de excepciones de Java destaca entre los lenguajes de programación pues en Java, éste se encuentra imbuido desde el principio, y es obligatorio utilizarlo. Si no se escribe un código de manera que maneje excepciones correctamente, se obtendrá un mensaje de error en tiempo de compilación. Esta garantía de consistencia hace que la gestión de errores sea mucho más sencilla. Es importante destacar el hecho de que el manejo de excepciones no es una característica orientada a objetos, aunque en los lenguajes orientados a objetos las excepciones se suelan representar m e diante un objeto. El manejo de excepciones existe desde antes de los lenguajes orientados a objetos.

Multihilo Un concepto fundamental en la programación de computadores es la idea de manejar más de una tarea en cada instante. Muchos problemas de programación requieren que el programa sea capaz de detener lo que esté haciendo, llevar a cabo algún otro problema, y volver a continuación al proceso principal. Se han buscando múltiples soluciones a este problema. Inicialmente, los programadores que tenían un conocimiento de bajo nivel de la máquina, escribían rutinas de servicio de interrupciones, logrando la suspensión del proceso principal mediante interrupciones hardware. Aunque este enfoque funcionaba bastante bien, era dificultoso y no portable, por lo que causaba que transportar un programa a una plataforma distinta de la original fuera lento y caro. A veces, es necesario hacer uso de las interrupciones para el manejo de tareas críticas en el tiempo, pero hay una gran cantidad de problemas en los que simplemente se intenta dividir un problema en fragmentos de código que pueden ser ejecutados por separado, de manera que se logra un menor tiempo de respuesta para todo el programa en general. Dentro de un programa, estos fragmentos de código que pueden ser ejecutados por separado, se denominan hilos, y el concepto general se denomina multihilos. Un ejemplo común de aplicación multihilo es la interfaz de usuario. Gracias al uso de hilos, un usuario puede presionar un botón y lograr una respuesta rápida en vez de verse forzado a esperar a que el programa acabe su tarea actual. Normalmente, los hilos no son más que una herramienta para facilitar la planificación en un monoprocesador. Pero si el sistema operativo soporta múltiples procesadores, es posible asignar cada hilo a un procesador distinto d e manera que los hilos se ejecuten verdaderamente en paralelo. Uno de los aspectos más destacables de la programación multihilo es que el programador no tiene que pre-


24

Piensa en Java

ocuparse de si hay uno o varios procesadores. El programa se divide de forma lógica en hilos y si hay más de un procesador, se ejecuta más rápidamente, sin que sea necesario llevar a cabo ningún ajuste adicional sobre el código. Todo esto hace que el manejo de hilos parezca muy sencillo. Hay un inconveniente: los recursos compartidos. Si se tiene más de un hilo en ejecución tratando de acceder al mismo recurso, se plantea un problema. Por ejemplo, dos procesos no pueden enviar simultáneamente información a una impresora. Para resolver el problema, los recursos que pueden ser compartidos como la impresora, deben bloquearse mientras se están usando. Por tanto, un hilo bloquea un recurso, completa su tarea y después libera el bloqueo de manera que alguien más pueda usar ese recurso. El hilo de Java está incluido en el propio lenguaje, lo que hace que un tema de por sí complicado se presente de forma muy sencilla. El manejo de hilos se soporta a nivel de objeto, de manera que un hilo de ejecución se representa por un objeto. Java también proporciona bloqueo dc rccursos limi-

tados; puede bloquear la memoria de cualquier objeto (que en el fondo, no deja de ser un tipo de recurso compartido) de manera que sólo un objeto pueda usarlo en un instante dado. Esto se logra mediante la palabra clave synchronized. Otros tipos de recursos deben ser explícitamente bloqueados por el programador, generalmente, creando un objeto que represente el bloqueo que todos los hilos deben comprobar antes de acceder al recurso.

Persistencia Al crear un objeto, existe tanto tiempo como sea necesario, pero bajo ninguna circunstancia sigue existiendo una vez que el programa acaba. Si bien esta circunstancia parece tener sentido a primera vista, hay situaciones en las que sería increíblemente útil el que un objeto pudiera existir y mantener su información incluso cuando el programa ya no esté en ejecución. De esta forma, la siguiente vez que se lance el programa, el objeto estará ahí y seguirá teniendo la misma información que tenía la última vez que se ejecutó el programa. Por supuesto, es posible lograr un efecto similar escribiendo la información en un archivo o en una base de datos, pero con la idea de hacer que todo sea un objeto, sería deseable poder declarar un objeto como persistente y hacer que alguien o algo se encargue de todos los detalles, sin tener que hacerlo uno mismo. Java proporciona soporte para "persistencia ligera", lo que significa que es posible almacenar objetos de manera sencilla en un disco para más tarde recuperarlos. La razón de que sea "ligera" es que e s necesario hacer llamadas explícitas a este almacenamiento y recuperación. Además, los JavaSpaces (descritos en el Capítulo 15) proporcionan cierto tipo de almacenamiento persistente de los objetos. En alguna versión futura, podría aparecer un soporte completo para la persistencia.

Java

Internet

Si Java es, de hecho, otro lenguaje de programación de computadores entonces uno podría preguntarse por qué es tan importante y por qué debería promocionarse como un paso revolucionario en la programación de computadores. La respuesta no es obvia ni inmediata si se procede de la perspectiva de programación tradicional. Aunque Java es muy útil de cara a la solución de problemas de


1: Introducción a los objetos

25

programación tradicionales autónomos, también es importante por resolver problemas de programación en la World Wide Web.

¿Qué es la Web? La Web puede parecer un gran misterio a primera vista, especialmente cuando se oye hablar de "navegar", "presencia" y "páginas iniciales" (home pages). Ha habido incluso una reacción creciente contra la "Internet-manía",cuestionando el valor económico y el beneficio de un movimiento tan radical. Es útil dar un paso atrás y ver lo que es realmente, pero para hacer esto es necesario entender los sistemas cliente/servidor, otro elemento de la computación lleno de aspectos que causan también confusión.

Computación cliente/servidor La idea principal de un sistema cliente/servidor es que se dispone de un depósito (repositorio) central de información -cierto tipo de datos, generalmente en una base de datos- que se desea distribuir bajo demanda a cierto conjunto de máquinas o personas. Una clave para comprender el concepto de cliente/servidor es que el depósito de información está ubicado centralmente, de manera que puede ser modificado y de forma que los cambios se propaguen a los consumidores de la información. A la(s) máquina(s) en las que se ubican conjuntamente el depósito de información y el software que la distribuye se la denomina el servidor. El software que reside en la máquina remota se comunica con el servidor, toma la información, la procesa y después la muestra en la máquina remota, denominada el cliente. El concepto básico de la computación cliente/servidor, por tanto, no es tan complicado. Aparecen problemas porque se tiene un único servidor que trata de dar servicio a múltiples clientes simultáneamente. Generalmente, está involucrado algún sistema gestor de base de datos de manera que el diseñador "reparte" la capa de datos entre distintas tablas para lograr un uso óptimo de los mismos. Además, estos sistemas suelen admitir que un cliente inserte nueva información en el servidor. Esto significa que es necesario asegurarse de que el nuevo dato de un cliente no machaque los nuevos datos de otro cliente, o que no se pierda este dato en el proceso de su adición a la base de datos (a esto se le denomina procesamiento de la transacción). Al cambiar el software cliente, debe ser construido, depurado e instalado en las máquinas cliente, lo cual se vuelve bastante más complicado y caro de lo que pudiera parecer. Es especialmente problemático dar soporte a varios tipos de computadores y sistemas operativos. Finalmente, hay un aspecto de rendimiento muy importante: es posible tener cientos de clientes haciendo peticiones simultáneas a un mismo servidor, de forma que un mínimo retraso sea crucial. Para minimizar la latencia, los programadores deben empeñarse a fondo para disminuir las cargas de las tareas en proceso, generalmente repartiéndolas con las máquinas cliente, pero en ocasiones, se dirige la carga hacia otras máquinas ubicadas junto con el servidor, denominadas intermediarios "middleware"(que también se utiliza para mejorar la mantenibilidad del sistema global).

La simple idea de distribuir la información a la gente, tiene muchas capas de complejidad en la fase de implementación, y el problema como un todo puede parecer desesperanzador. E incluso puede ser crucial: la computación cliente/servidor se lleva prácticamente la mitad de todas las actividades de programación. Es responsable de todo, desde recibir las órdenes y transacciones de tarjetas de


26

Piensa en Java

crédito hasta la distribución de cualquier tipo de datos -mercado de valores, datos científicos, del gobierno,. . . Hasta la fecha, en el pasado, se han intentado y desarrollado soluciones individuales para problemas específicos, inventando una solución nueva cada vez. Estas soluciones eran difíciles de crear y utilizar, y el usuario debía aprenderse nuevas interfaces para cada una de ellas. El problema cliente/servidor completo debe resolverse con un enfoque global.

La Web como un servidor gigante La Web es, de hecho, un sistema cliente/servidor gigante. Es un poco peor que eso, puesto que todos los servidores y clientes coexisten en una única red a la vez. No es necesario, sin embargo, ser conscientes de este hecho, puesto que simplemente es necesario preocuparse de saber cómo conectarse y cómo interactuar con un servidor en un momento dado (incluso aunque sea necesario merodear por todo el mundo para encontrar el servidor correcto). Inicialmente, este proceso era unidireccional. Se hacía una petición de un servidor y éste te proporcionaba un archivo que el software navegador (por ejemplo, el cliente) de tu máquina podía interpretar dándole el formato adecuado en la máquina local. Pero en poco tiempo, la gente empezó a demandar más servicios que simplemente recibir páginas de un servidor. Se pedían capacidades cliente/servidor completas, de manera que el cliente pudiera retroalimentar de información al servidor; por ejemplo, para hacer búsquedas en base de datos en el servidor, añadir nueva información al mismo, o para ubicar una orden (lo que requería un nivel de seguridad mucho mayor que el que ofrecían los sistemas originales). Éstos son los cambios de los que hemos sido testigos a lo largo del desarrollo de la Web. El navegador de la Web fue un gran paso hacia delante: el concepto de que un fragmento de información pudiera ser mostrado en cualquier tipo de computador sin necesidad de modificarlo. Sin embargo, los navegadores seguían siendo bastante primitivos y pronto se pasaron de moda debido a las demandas que se les hacían. No eran especialmente interactivos, y tendían a saturar tanto el servidor como Internet puesto que cada vez que requería hacer algo que exigiera programación había que enviar información de vuelta al servidor para que fuera procesada. Encontrar algo que por ejemplo, se había tecleado incorrectamente en una solicitud, podía llevar muchos minutos o segundos. Dado que el navegador era únicamente un visor no podía desempeñar ni siquiera las tareas de computación más simples. (Por otro lado, era seguro, puesto que no podía ejecutar programas en la máquina local que pudiera contener errores (bugs) o virus.) Para resolver el problema, se han intentado distintos enfoques. El primero de ellos consistió en mejorar los estándares gráficos para permitir mejores animaciones y vídeos dentro de los navegadores. El resto del problema se puede resolver incorporando simplemente la capacidad de ejecutar programas en el cliente final, bajo el navegador. Esto se denomina programación en la parte cliente.

Programación en el lado del cliente El diseño original servidor-navegador de la Web proporcionaba contenidos interactivos, pero la capacidad de interacción la proporcionaba completamente el servidor. Éste producía páginas estáticas para el navegador del cliente, que simplemente las interpretaba y visualizaba. El HTML básico


1: Introducción a los objetos

27

contiene mecanismos simples para la recopilación de datos: cajas de entrada de textos, cajas de prueba, cajas de radio, listas y listas desplegables, además de un botón que sólo podía programarse para borrar los datos del formulario o "enviar" los datos del formulario de vuelta al servidor. Este envío de datos se lleva a cabo a través del Common Gateway Interface (CGI), proporcionado por todos los servidores web. El texto del envío transmite a CGI qué debe hacer con él. La acción más común es ejecutar un programa localizado en el servidor en un directorio denominado generalmente "cgi-bin". (Si se echa un vistazo a la ventana de direcciones de la parte superior del navegador al presionar un botón de una página Web, es posible ver en ocasiones la cadena "cgi-bin" entre otras cosas.) Estos programas pueden escribirse en la mayoría de los lenguajes. Perl es una elección bastante frecuente pues fue diseñado para la manipulación de textos, y es interpretado, lo que permite que pueda ser instalado en cualquier servidor sin que importe el procesador o sistema operativo instalado. Muchos de los sitios web importantes de hoy en día se siguen construyendo estrictamente con CGI, y es posible, de hecho, hacer casi cualquier cosa con él. Sin embargo, los sitios web cuyo funcionamiento se basa en programas CGI se suelen volver difíciles de mantener, y presentan además problemas de tiempo de respuesta. (Además, poner en marcha programas CGI suele ser bastante lento.) Los diseñadores iniciales de la Web no previeron la rapidez con que se agotaría el ancho de banda para los tipos de aplicaciones que se desarrollaron. Por ejemplo, es imposible llevar a cabo cualquier tipo de generación dinámica de gráficos con consistencia, pues es necesario crear un archivo GIF que pueda ser después trasladado del servidor al cliente para cada versión del gráfico. Y seguro que todo el mundo ha tenido alguna experiencia con algo tan simple como validar datos en un formulario de entrada. Se presiona el botón de enviar de una página; los datos se envían de vuelta al servidor; el servidor pone en marcha un programa CGI y descubre un error, da formato a una página H'I'ML inIormando del error y después vuelve a mandar la pagina de vuelta; entonces es necesario recuperar el formulario y volver a empezar. Esto no es solamente lento, sino que es además poco elegante.

La solución es la programación en el lado del cliente. La mayoria de las máquinas que ejecutan navegadores Web son motores potentes capaces de llevar a cabo grandes cantidades de trabajo, y con el enfoque HTML estático original, simplemente estaban allí "sentadas", esperando ociosas a que el servidor se encargara de la página siguiente. La programación en el lado del cliente quiere decir que el servidor web tiene permiso para hacer cualquier trabajo del que sea capaz, y el resultado para el usuario es una experiencia mucho más rápida e interactiva en el sitio web. El problema con las discusiones sobre la programación en el lado cliente es que no son muy distintas de las discusiones de programación en general. Los parámetros son casi los mismos, pero la plataforma es distinta: un navegador web es como un sistema operativo limitado. Al final, uno debe seguir programando, y esto hace que siga existiendo el clásico conjunto de problemas y soluciones, producidos en este caso por la programación en el lado del cliente. El resto de esta sección proporciona un repaso de los aspectos y enfoques en la programación en el lado del cliente.

Conecta bles (plug-ins) Uno de los mayores avances en la programación en la parte cliente es el desarrollo de los conectables (plug-ins). Éstos son modos en los que un programador puede añadir nueva funcionalidad al


28

Piensa en Java

navegador descargando fragmentos de código que se conecta en el punto adecuado del navegador. Le dice al navegador "de ahora en adelante eres capaz de llevar a cabo esta nueva actividad". (Es necesario descargar cada conectable únicamente una vez.) A través de los conectables, se añade comportamiento rápido y potente al navegador, pero la escritura de un conectable no es trivial y desde luego no es una parte deseable para hacer como parte de un proceso de construcción de un sitio web. El valor del conectable para la programación en el lado cliente es tal que permite a un programador experto desarrollar un nuevo lenguaje y añadirlo a un navegador sin permiso de la parte que desarrolló el propio navegador. Por consiguiente, los navegadores proporcionan una "puerta trasera que permite la creación de nuevos lenguajes de programación en el lado cliente (aunque no todos los lenguajes se encuentren implementados como conectables).

Lenguajes de guiones Los conectables condujeron a la explosión de los lenguajes de guiones (scripting). Con uno de estos lenguajes se integra el código fuente del programa de la parte cliente directamente en la página HTML, y el conectable que interpreta ese lenguaje se activa automáticamente a medida que se muestra la página HTML. Estos lenguajes tienden a ser bastante sencillos de entender y, dado que son simplemente texto que forma parte de una página HTML, se cargan muy rápidamente como parte del único acceso al servidor mediante el que se accede a esa página. El sacrificio es que todo el mundo puede ver (y robar) el código así transportado. Sin embargo, generalmente, si no se pretende hacer cosas excesivamente complicadas, los lenguajes de guiones constituyen una buena herramienta, al no ser complicados. Esto muestra que los lenguajes de guiones utilizados dentro de los navegadores web se desarrollaron verdaderamente para resolver tipos de problemas específicos, en particular la creación de interfaces gráficos de usuario (IGUs) más interactivos y ricos. Sin embargo, uno de estos lenguajes puede resolver el 80%de los problemas que se presentan en la programación en el lado cliente. Este 80%podría además abarcar todos los problemas de muchos programadores, y dado que los lenguajes de programación permiten desarrollos mucho más sencillos y rápidos, es útil pensar en utilizar uno de estos lenguajes antes de buscar soluciones más complicadas, como la programación en Java o ActiveX. Los lenguajes de guiones de navegador más comunes son JavaScript (que no tiene nada que ver con Java; se denominó así simplemente para aprovechar el buen momento de marketing de Java), VBScript (que se parece bastante a Visual Basic), y Tcl/Tk, que proviene del popular lenguaje de construcción de IGU (Interfaz Gráfica de Usuario) de plataformas cruzadas. Hay otros más, y seguro que se desarrollarán muchos más. JavaScript es probablemente el que recibe más apoyo. Viene incorporado tanto en el navegador Netscape Navigator como en el Microsoft Internet Explorer (IE). Además, hay probablemente más libros de JavaScritpt que de otros lenguajes de navegador, y algunas herramientas crean páginas automáticamente haciendo uso de JavaScript. Sin embargo, si se tiene habilidad en el manejo de Visual Basic o Tcl/Tk, será más productivo hacer uso de esos lenguajes de guiones en vez de aprender uno nuevo. (De hecho, ya se habrá hecho uso de aspectos web para estas alturas.)

Java Si un lenguaje de programación puede resolver el 80 por ciento de los problemas de programación en el lado cliente, ¿qué ocurre con el 20 por ciento restante -que constituyen de hecho "la


1: Introducción a los objetos

29

parte seria del problema"? La solución más popular hoy en día es Java. No sólo se trata de un lenguaje de programación potente, construido para ser seguro, de plataforma cruzada (multiplataforma) e internacional, sino que se está extendiendo continuamente para proporcionar aspectos de lenguaje y bibliotecas que manejan de manera elegante problemas que son complicados para los lenguajes de programación tradicionales, como la ejecución multihilo, el acceso a base de datos, la programación en red, y la computación distribuida. Java permite programación en el lado cliente a través del applet. Un applet es un miniprograma que se ejecutará únicamente bajo un navegador web. El applet se descarga automáticamente como parte de una página web (igual que, por ejemplo, se descarga un gráfico, de manera automática). Cuando se activa un applet, ejecuta un programa. Ésta es parte de su belleza -proporciona una manera de distribuir automáticamente software cliente desde el servidor justo cuando el usuario necesita software cliente, y no antes. El usuario se hace con la última versión del software cliente, sin posibilidad de fallo, y sin tener que llevar a cabo reinstalaciones complicadas. Gracias a cómo se ha diseñado Java, el programado simplemente tiene que crear un único programa, y este programa trabaja automiticamente en todos los computa-

dores que tengan navegadores que incluyan intérpretes de Java. (Esto incluye seguramente a la gran mayoría de plataformas.) Dado que Java es un lenguaje de programación para novatos, es posible hacer tanto trabajo como sea posible en el cliente, antes y después de hacer peticiones al servidor. Por ejemplo, no se deseará enviar un formulario de petición a través de Internet para descubrir que se tiene una fecha o algún otro parámetro erróneo, y el computador cliente puede llevar a cabo rápidamente la labor de registrar información en vez de tener que esperar a que lo haga el servidor para enviar después una imagen gráfica de vuelta. No sólo se consigue un incremento de velocidad y capacidad de respuesta inmediatas, sino que el tráfico en general de la red y la carga en los servidores se reduce considerablemente, evitando que toda Internet se vaya ralentizando. Una ventaja que tienen los applets de Java sobre los lenguajes de guiones es que se encuentra en formato compilado, de forma que el código fuente no está disponible para el cliente. Por otro lado, es posible descompilar un applet Java sin excesivo trabajo, pero esconder un código no es un problema generalmente importante. Hay otros dos factores que pueden ser importantes. Como se verá más tarde en este libro, un applet Java compilado puede incluir varios módulos e implicar varios accesos al servidor para su descarga (en Java 1.1y superiores, esto se minimiza mediante archivos Java, denominados archivos JAR, que permiten que los módulos se empaqueten todos juntos y se compriman después para que baste con una única descarga). Un programa de guiones se integrará simplemente en una página web como parte de su texto (y generalmente será más pequeño reduciendo los accesos al servidor). Esto podría ser importante de cara al tiempo de respuesta del sitio web. Otro factor importante es la curva de aprendizaje. A pesar de lo que haya podido oírse, Java no es un lenguaje cuyo aprendizaje resulte trivial. Para los programadores en Visual Basic, moverse a VBScript siempre será la solución más rápida, y dado que probablemente este lenguaje resolverá los problemas cliente/servidor más típicos, puede resultar bastante complicado justificar la necesidad de aprender Java. Si uno ya tiene experiencia con un lenguaje de guiones, seguro que se obtendrán beneficios simplemente haciendo uso de JavaScript o VBScript antes de lanzarse a utilizar Java, ya que estos lenguajes pueden resolver todos los problemas de manera sencilla, y se logrará un nivel de productividad elevado en un tiempo menor.


30

Piensa en Java

ActiveX Hasta cierto grado, el competidor principal de Java es el ActiveX de Microsoft, aunque se base en un enfoque totalmente diferente. ActiveX era originalmente una solución válida exclusivamente para Windows, aunque ahora se está desarrollando mediante un consorcio independiente de manera que acabará siendo multiplataforma (plataforma cruzada). Efectivamente, ActiveX se basa en que "si un programa se conecta a su entorno de manera que puede ser depositado en una página web y ejecutado en un navegador, entonces soporta ActiveX. (IE soporta ActiveX directamente, y Netscape también, haciendo uso de un conectable.) Por consiguiente, ActiveX no se limita a un lenguaje particular. Si, por ejemplo, uno es un programador Windows experimentado, haciendo uso de un lenguaje como C++,Visual Basic o en Delphi de Borland, es posible crear componentes ActiveX sin casi tener que hacer ningún cambio a los conocimientos de programación que ya se tengan. Además,

ActiveX proporciona un modo de usar código antiguo (base dado) en páginas web. Seguridad

La capacidad para descargar y ejecutar programas a través de Internet puede parecer el sueño de un constructor de virus. ActiveX atrae especialmente el espinoso tema de la seguridad en la programación en la parte cliente. Si se hace clic en el sitio web, es posible descargar automáticamente cualquier número de cosas junto con la página HTML: archivos GIF, código de guiones, código Java compilado, y componentes ActiveX. Algunos de estos elementos son benignos: los archivos GIF no pueden hacer ningún daño, y los lenguajes de guiones se encuentran generalmente limitados en lo que pueden hacer. Java también fue diseñado para ejecutar sus applets dentro de un "envoltorio" de seguridad, lo que evita que escriba en el disco o acceda a la memoria externa a ese envoltorio. ActiveX está en el rango opuesto del espectro. Programar con ActiveX es como programar Windows -es posible hacer cualquier cosa. De esta manera, si se hace clic en una página web que descarga un componente ActiveX, ese componente podría llegar a dañar los archivos de un disco. Por supuesto, los programas que uno carga en un computador, y que no están restringidos a ejecutarse dentro del navegador web podrían hacer lo mismo. Los virus que se descargaban desde una BBS (Bulletin-Board Systems) hace ya tiempo que son un problema, pero la velocidad de Internet amplifica su gravedad.

La solución parecen aportarla las "firmas digitales", que permiten la verificación de la autoría del código. Este sistema se basa en la idea de que un virus funciona porque su creador puede ser anónimo, de manera que si se evita la ejecución de programas anónimos, se obligará a cada persona a ser responsable de sus actos. Esto parece una buena idea, pues permite a los programas ser mucho más funcionales, y sospecho que eliminará las diabluras maliciosas. Si, sin embargo, un programa tiene un error (bug) inintencionadamente destructivo, seguirá causando problemas. El enfoque de Java es prevenir que estos problemas ocurran, a través del envoltorio. El intérprete de Java que reside en el navegador web local examina el applet buscando instrucciones adversas a medida que se carga el applet. Más en concreto, el applet no puede escribir ficheros en el disco o borrar ficheros (una de las principales vías de ataque de los virus). Los applets se consideran generalmente seguros, y dado que esto es esencial para lograr sistemas cliente/servidor de confianza, cualquier error (bug) que produzca virus en lenguaje Java será rápidamente reparado. (Merece la


1: Introducción a los objetos

31

pena destacar que el software navegador, de hecho, refuerza estas restricciones de seguridad, y algunos navegadores permiten seleccionar distintos niveles de seguridad para proporcionar distintos grados de acceso a un sistema.) También podría uno ser escéptico sobre esta restricción tan draconiana en contra de la escritura de ficheros en un disco local. Por ejemplo, uno puede desear construir una base de datos o almacenar datos para utilizarlos posteriormente, finalizada la conexión. La visión inicial parecía ser tal que eventualmente todo el mundo podría conseguir hacer cualquier cosa importante estando conectado, pero pronto se vio que esta visión no era práctica (aunque los "elementos Internet" de bajo coste puedan satisfacer algún día las necesidades de un segmento de usuarios significativo). La solución es el "applet firmado" que utiliza cifrado de clave pública para verificar que un applet viene efectivamente de donde dice venir. Un applet firmado puede seguir destrozando un disco local, pero la teoría es que dado que ahora es posible localizar al creador del applet, éstos no actuarán de manera perniciosa. Java proporciona un marco de trabajo para las firmas digitales, de forma que será posible permitir q u e un applet llegue a salir fuera del envoltorio si es necesario. Las firmas digitales han olvidado un aspecto importante, que es la velocidad con la que la gente se mueve por Internet. Si se descarga un programa con errores (bugs) que hace algo dañino, ¿cuánto tiempo se tardará en descubrir el daño? Podrían pasar días o incluso semanas. Para entonces, ¿cómo localizar el programa que lo ha causado? ¿Y será todo el mundo capaz de hacerlo?

Internet frente a Intranet La Web es la solución más general al problema cliente/servidor, de forma que tiene sentido que se pueda utilizar la misma tecnología para resolver un subconjunto del problema, en particular, el clásico problema cliente/servidor dentro de una compañía. Con los enfoques cliente/servidor tradicionales, se tiene el problema de la multiplicidad de tipos de máquinas cliente, además de la dificultad de instalar un nuevo software cliente, si bien ambos problemas pueden resolverse sencillamente con navegadores web y programación en el lado cliente. Cuando se utiliza tecnología web para una red de información restringida a una compañía en particular, se la denomina una Intranet. Las Intranets proporcionan un nivel de seguridad mucho mayor que el de Internet, puesto que se puede controlar físicamente el acceso a los servidores dentro de la propia compañía. En términos de formación, parece que una vez que la gente entiende el concepto general de navegador es mucho más sencillo que se enfrenten a distintas páginas y applets, de manera que la curva de aprendizaje para nuevos tipos de sistemas parece reducirse. El problema de la seguridad nos conduce ante una de las divisiones que parece estar formándose automáticamente en el mundo de la programación en el lado del cliente. Si un programa se ejecuta en Internet, no se sabe bajo qué plataforma estará funcionando, y si se desea ser extremadamente cauto, no se diseminará código con error. Es necesario algo multiplataforma y seguro, como un lenguaje de guiones o Java. Si se está ejecutando código en una Intranet, es posible tener un conjunto de limitaciones distinto. No es extraño que las máquinas de una red puedan ser todas plataformas Intel/Windows. En una Intranet, uno es responsable de la calidad de su propio código y puede reparar errores en el iriomenlo en que se descubren. Además, se podría tener cierta cantidad de código antiguo (heredado, legacy) que se ha


32

Piensa en Java

estado utilizando en un enfoque cliente/servidor más tradicional, en cuyo caso sería necesario instalar físicamente programas cliente cada vez que se construya una versión más moderna. El tiempo malgastado en instalar actualizaciones (upgrades) es la razón más apabullante para comenzar a usar navegadores, en los que estas actualizaciones son invisibles y automáticas. Para aquéllos que tengan intranets, el enfoque más sensato es tomar el camino más corto que permita usar el código base existente, en vez de volver a codificar todos los programas en un nuevo lenguaje. Se ha hecho frente a este problema presentando un conjunto desconcertante de soluciones al problema de programación en el lado cliente, y la mejor determinación para cada caso es la que determine un análisis coste-beneficio. Deben considerarse las restricciones de cada problema y cuál sería el camino más corto para encontrar la solución en cada caso. Dado que la programación en la parte cliente sigue siendo programación, suele ser buena idea tomar el enfoque de desarrollo más rápido para cada situación. Ésta es una postura agresiva para prepararse de cara a inevitables enfrentamiento~con los problemas del desarrollo de programas.

Programación en el lado del servidor Hasta la fecha toda discusión ha ignorado el problema de la programación en el lado del servidor. ¿Qué ocurre cuando se hace una petición a un servidor? La mayoría de las veces la petición es simplemente "envíame este archivo". A continuación, el navegador interpreta el archivo de la manera adecuada: como una página HTML, como una imagen gráfica, un applet Java, un programa de guiones, etc. Una petición más complicada hecha a un servidor puede involucrar una transacción de base de datos. Un escenario común involucra una petición para una búsqueda compleja en una base de datos, que el servidor formatea en una página HTML para enviarla a modo de resultado (por supuesto, si el cliente tiene una inteligencia mayor vía Java o un lenguaje de guiones, pueden enviarse los datos simplemente, sin formato, y será el extremo cliente el que les dé el formato adecuado, lo cual es más rápido, además de implicar una carga menor para el servidor). Un usuario también podría querer registrar su nombre en una base de datos al incorporarse a un grupo o presentar una orden, lo cual implica cambios a esa base de datos. Estas peticiones deben procesarse vía algún código en el lado servidor, que se denomina generalmente programación en el lado servidor. Tradicionalmente, esta programación se ha desempeñado mediante Perl y guiones CGI, pero han ido apareciendo sistemas más sofisticados. Entre éstos se encuentran los servidores web basados en Java que permiten llevar a cabo toda la programación del lado servidor en Java escribiendo lo que se denominan servlets. Éstos y sus descendientes, los JSP, son las dos razones principales por las que las compañías que desarrollan sitios web se están pasando a Java, especialmente porque eliminan los problemas de tener que tratar con navegadores de distintas características.

Un ruedo separado: las aplicaciones Muchos de los comentarios en torno a Java se referían a los applets. Java es actualmente un lenguaje de programación de propósito general que puede resolver cualquier tipo de problema -al menos en teoría. Y como se ha señalado anteriormente, cuando uno se sale del ruedo de los applets (y simultáneamente se salta las restricciones, como la contraria a la escritura en el disco) se entra en el mundo de las aplicaciones de propósito general que se ejecutan independientemente, sin un nave-


1: Introducción a los objetos

33

gador web, al igual que hace cualquier programa ordinario. Aquí, la fuerza de Java no es sólo su portabilidad, sino también su programabilidad (facilidad de programación). Como se verá a lo largo del presente libro, Java tiene muchos aspectos que permiten la creación de programas robustos en un período de tiempo menor al que requerían los lenguajes de programación anteriores. Uno debe ser consciente de que esta bendición no lo es del todo. El precio a pagar por todas estas mejoras es una velocidad de ejecución menor (aunque se está haciendo bastante trabajo en este área -JDK 1.3, en particular, presenta las mejoras de rendimiento denominadas "hotspot"). Como cualquier lenguaje, Java tiene limitaciones intrínsecas que podrían hacerlo inadecuado para resolver cierto tipo de problemas de programación. Java es un lenguaje que evoluciona rápidamente, no obstante, y cada vez que aparece una nueva versión, se presenta más y más atractivo de cara a la solución de conjuntos mayores de problemas.

Análisis y diseño El paradigma de la orientación a obietos es una nueva manera de enfocar la programación. Son muchos los que tienen problemas a primera vista para enfrentarse a un proyecto de POO. Dado que se supone que todo es un objeto, y a medida que se aprende a pensar de forma orientada a objetos, es posible empezar a crear "buenos" diseños y sacar ventaja de todos los beneficios que la PO0 puede ofrecer. Una metodología es un conjunto de procesos y heurísticas utilizadas para descomponer la complejidad de un problema de programación. Se han formulado muchos métodos de PO0 desde que enunció la programación orientada a objetos. Esta sección presenta una idea de lo que se trata de lograr al utilizar un método. Especialmente en la POO, la metodología es un área de intensa experimentación, por lo que es importante entender qué problema está intentando resolver el método antes de considerar la adopción de uno de ellos. Esto es particularmente cierto con Java, donde el lenguaje de programación se ha desarrollado para reducir la complejidad (en comparación con C) necesaria para expresar un programa. Esto puede, de hecho, aliviar la necesidad de metodologías cada vez más complejas. En vez de esto, puede que las metodologías simples sean suficientes en Java para conjuntos de problemas mucho mayores que los que se podrían manipular utilizando metodologías simples con lenguajes procedimentales. También es importante darse cuenta de que el término "metodología" es a menudo muy general y promete demasiado. Se haga lo que se haga al diseñar y escribir un programa, se sigue un método. Puede que sea el método propio de uno, e incluso puede que uno no sea consciente de utilizarlo, pero es un proceso que se sigue al crear un programa. Si el proceso es efectivo, puede que simplemente sea necesario afinarlo ligeramente para poder trabajar con Java. Si no se está satisfecho con el nivel de productividad y la manera en que se comportan los programas, puede ser buena idea co~isiderar la adopción de un método formal, o la selección dc fragmentos de entre los muchos métodos formales existentes. Mientras se está en el propio proceso de desarrollo, el aspecto más importante es no perderse, aunque puede resultar fácil. Muchos de los métodos de análisis y desarrollo fueron concebidos para re-


34

Piensa en Java

solver los problemas más grandes. Hay que recordar que la mayoría de proyectos no encajan en esta categoría, siendo posible muchas veces lograr un análisis y un diseño con éxito con sólo un pequeño subconjunto de los que el método recomienda"'. Pero algunos tipos de procesos, sin importar lo limitados que puedan ser, le permitirán encontrar el camino de manera más sencilla que si simplemente se empieza a codificar. También es fácil desesperarse, caer en "parálisis de análisis", cuando se siente que no se puede avanzar porque no se han cubierto todos los detalles en la etapa actual. Debe recordarse que, independientemente de cuánto análisis lleve a cabo, hay cosas de un sistema que no aparecerán hasta la fase de diseño, y otras no aflorarán incluso hasta la fase de codificación o en un extremo, hasta que el programa esté acabado y en ejecución. Debido a esto, e s crucial moverse lo suficientemente rápido a través de las etapas de análisis y diseño e implementar un prototipo del sistema propuesto. Debe prestarse un especial énfasis a este punto. Dado que ya se conoce la misma historia con los lenguajes procedimentales, es recomendable que el equipo proceda de manera cuidadosa y comprenda cada detalle antes de pasar del diseño a la implementación. Ciertamente, al crear un SGBD, esto pasa por comprender completamente la necesidad del cliente. Pero un SGBD es la clase de problema bien formulada y bien entendida; en muchos programas, es la estructura de la base de datos la que debe ser desmenuzada. La clase de problema de programación examinada en el presente capítulo es un "juego de azar"", en el que la solución no es simplemente la formulación de una solución bien conocida, sino que involucra además a uno o más "factores de azar" -elementos para los que no existe una solución previa bien entendida, y para los cuales es necesario algún tipo de proceso de investigaciónl7.Intentar analizar completamente un problema al azar antes de pasar al diseño e implementación conduce a una parálisis en el análisis, al no tener suficiente información para resolver este tipo de problemas durante la fase de análisis. Resolver un problema así, requiere iterar todo el ciclo, y precisa de un comportamiento que asuma riesgos (lo cual tiene sentido, pues está intentando hacer algo nuevo y las recompensas potenciales crecen). Puede parecer que el riesgo se agrava al precipitarse hacia una implementación preliminar, pero ésta puede reducir el riesgo en los problemas al azar porque se está averiguando muy pronto si un enfoque particular al problema es o no viable. El desarrollo de productos conlleva una gestión del riesgo.

A menudo, se propone "construir uno para desecharlo". Con PO0 es posible tirar parte, pero dado que el código está encapsulado en clases, durante la primera pasada siempre se producirá algún diseño de clases útil y se desarrollarán ideas que merezcan la pena para el diseño del sistema de las que no habrá que deshacerse. Por tanto, la primera pasada rápida por un problema no sólo suministra información crítica para las ulteriores pasadas por análisis, diseño e implementación, sino que también crea la base del código.

Un ejemplo excelente d r esto es UML Uistilled,2." edición, de Martin Fowler (Addison-Wesley 2000). que reducc cl proccso, en ocasiones aplastante, a un subconjunto rriariejable (existe vei-sión española con el título UMI, gota a gota). " N. del traductor: Término wild-card, acunado por el autor original.

lo

" Regla del pulgar -acuñada por el autor- para estimar este tipo de proyectos: si hay más de un factor al azar, ni siquiera debe intentarse planificar la duración o el coste del proyecto hasta que no s e ha creado un prototipo que funcione. Existen demasiados grados de libertad.


1: Introducción a los objetos

35

Dicho esto, si se está buscando una metodología que contenga un nivel de detalle tremendo, y sugiera muchos pasos y documentos, puede seguir siendo difícil saber cuándo parar. Debe recomendarse lo que se está intentando descubrir.

1. ¿Cuáles son los objetos? (¿Cómo se descompone su proyecto en sus componentes?) 2. ¿Cuáles son las interfaces? (¿Qué mensajes es necesario enviar a cada objeto?) Si se delimitan los objetos y sus interfaces, ya es posible escribir un programa. Por diversas razones, puede que sean necesarias más descripciones y documentos que éste, pero no es posible avanzar con menos. El proceso puede descomponerse en cinco fases, y la Fase O no es más que la adopción de un com-

promiso para utilizar algún tipo de estructura.

Fase O: Elaborar un plan En primer lugar, debe decidirse qué pasos debe haber en un determinado proceso. Suena bastante simple (de hecho, todo esto suena simple) y la gente no obstante, suele seguir sin tomar esta decisión antes de empezar a codificar. Si el plan consiste en "empecemos codificando", entonces, perfecto (en ocasiones, esto es apropiado, si uno se está enfrentando a un problema que conoce perfectamente). Al menos, hay que estar de acuerdo en que eso también es tener un plan. También podría decidirse en esta fase que es necesaria alguna estructura adicional de proceso, pero no toda una metodología completa. Para que nos entendamos, a algunos programadores les gusta trabajar en "modo vacación", en el que no se imponga ninguna estructura en el proceso de desarrollar de su trabajo; "se hará cuando se haga". Esto puede resultar atractivo a primera vista, pero a medida que se tiene algo de experiencia uno se da cuenta de que es mejor ordenar y distribuir el esfuerzo en distintas etapas en vez de lanzarse directamente a "finalizar el proyecto". Además, de esta manera se divide el proyecto en fragmentos más asequibles, y se resta miedo a la tarea de enfrentarse al mismo (además, las distintas fases o hitos proporcionan más motivos de celebración). Cuando empecé a estudiar la estructura de la historia (con el propósito de acabar escribiendo algún día una novela), inicialmente, la idea que más me disgustaba era la de la estructura, pues parecía que uno escribe mejor si simplemente se dedica a rellenar páginas. Pero más tarde descubrí que al escribir sobre computadores, tenía la estructura tan clara que no había que pensar demasiado en ella. Pero aún así, el trabajo se estructuraba, aunque sólo fuera semiconscientemente en mi cabeza. Incluso cuando uno piensa que el plan consiste simplemente en empezar a codificar, todavía se atraviesan algunas fases al plantear y contestar ciertas preguntas.

El enunciado de la misión Cualquier sistema que uno construya, independientemente de lo complicado que sea, tiene un propósito fundamental: el negocio intrínseco en el mismo, la necesidad básica que cumple. Si uno puede mirar a través de la interfaz de usuario, a los detalles específicos del hardware o del sistema, los algoritinos de codificación y los problemas de eficiencia, entonces se encuentra el centro de su existencia -simple y directo. Como el denominado alto concepto (high concept) en las películas de


36

Piensa en Java

Hollywood, uno puede describir el propósito de un programa en dos o tres fases. Esta descripción, pura, es el punto de partida. El alto concepto es bastante importante porque establece el tono del proyecto; es el enunciado de su misión. Uno no tiene por qué acertar necesariamente a la primera (puede ser que uno esté en una fase posterior del problema cuando se le ocurra el enunciado completamente correcto) pero hay que se guir intentándolo hasta tener la certeza de que está bien. Por ejemplo, en un sistema de control de trá. fico aéreo, uno puede comenzar con un alto concepto centrado en el sistema que se está construyendo: "El programa de la torre hace un seguimiento del avión". Pero considérese qué ocurre cuando se introduce el sistema en un pequeño aeródromo; quizás sólo hay un controlador humano, o incluso ninguno. Un modelo más usual no abordará la solución que se está creando como describe el problema: "Los aviones llegan, descargan, son mantenidos y recargan, a continuación, salen".

Fase 1: ¿Qué estamos construyendo? En la generación previa del diseño del programa (denominada diseño procedural) a esta fase se le de nominaba "creación del análisis de requisitos y especificación del sistema". Éstas, por supuesto, eran fases en las que uno se perdía; documentos con nombres intimidadores que podían de por sí convertirse en grandes proyectos. Sin embargo, su intención era buena. El análisis de requisitos dice: "Construya una lista de directrices que se utilizarán para saber cuándo se ha acabado el trabajo y cuándo el cliente está satisfecho". La especificación del sistema dice: "He aquí una descripción de lo que el programa hará (pero no cómo) para satisfacer los requisitos hallados". El análisis de requisitos es verdaderamente un contrato entre usted y el cliente (incluso si el cliente trabaja en la misma compañía o es cualquier otro objeto o sistema). La especificación del sistema es una exploración de alto nivel en el problema, y en cierta medida, un descubrimiento de si puede hacerse y cuánto tiempo llevará. Dado que ambos requieren de consenso entre la gente (y dado que generalmente variarán a lo largo del tiempo) lo mejor es mantenerlos lo más desnudos posible -idealmente, tratará de listas y diagrarnas básicos para ahorrar tiempo. Se podría tener otras limitaciones que exijan expandirlos en documentos de mayor tamaño, pero si se mantiene que el documento inicial sea pequeño y conciso, es posible cre arlo en unas pocas sesiones de tormenta de ideas (brainstorming) en grupo, con un líder que dinámicamente va creando la descripción. Este proceso no sólo exige que todos aporten sus ideas sino que fomenta el que todos los miembros del equipo lleguen a un acuerdo inicial. Quizás lo más importante es que puede incluso ayudar a que se acometa el proyecto con una gran dosis de entusiasmo. Es necesario mantenerse centrado en el corazón de lo que se está intentando acometer en esta fase: determinar qué es lo que se supone que debe hacer el sistema. La herramienta más valiosa para esto es una colección de lo que se denomina "casos de uso". Los casos de uso identifican los aspectos claves del sistema, que acabarán por revelar las clases fundamentales que se usarán en éste. De hecho, los casos de uso son esencialmente soluciones descriptivas a preguntas como1": ''¿Quién usará el sistema?" ''¿Qué pueden hacer esos actores con el sistema?"

"

Agradecemos la ayuda de James H. Jarrett


1: Introducción a los objetos

37

¿Cómo se las ingenia cada actor para hacer eso con este sistema?" "¿De qué otra forma podría funcionar esto si alguien más lo estuviera haciendo, o si el mismo actor tuviera un objetivo distinto?" (Para encontrar posibles variaciones.) ''¿Qué problemas podrían surgir mientras se hace esto con el sistema?" (Para localizar posibles excepciones.) Si se está diseñando, por ejemplo, un cajero automático, el caso de uso para un aspecto particular de la funcionalidad del sistema debe ser capaz de describir qué hace el cajero en cada situación posible. Cada una de estas "situaciones" se denomina un escenario, y un caso de uso puede considerarse como una colección de escenarios. Uno puede pensar que un escenario es como una pregunta que empieza por: ''¿Qué hace el sistema si...?". Por ejemplo: ''¿Qué hace el cajero si un cliente acaba de depositar durante las últimas 24 horas un cheque y no hay dinero suficiente en la cuenta, sin haber procesado el cheque, para proporcionarle la retirada el efectivo que ha solicitado?" Deben utilizarse diagramas de caso de uso intencionadamente simples para evitar ahogarse prematuramente en detalles de implementación del sistema :

Cada uno de los monigotes representa a un "actor", que suele ser generalmente un humano o cualquier otro tipo de agente (por ejemplo, otro sistema de computación, como "ATM")14. La caja representa los límites de nuestro sistema. Las elipses representan los casos de uso, que son descripciones del trabajo útil que puede hacerse dentro del sistema. Las líneas entre los actores y los casos de uso representan las interacciones. De hecho no importa cómo esté el sistema implementado, siempre y cuando tenga una apariencia como ésta para el usuario. l4

ATM, siglas en inglés de cajero automático. (N. del T. )


38

Piensa en Java

Un caso de uso no tiene por qué ser terriblemente complejo, aunque el sistema subyacente sea complejo. Solamente se pretende que muestre el sistema tal y como éste se muestra al usuario. Por ejemplo:

o Jardinero

~

~

l nvernadero

Temperatura

Los casos de uso proporcionan las especificaciones de requisitos determinando todas las interacciones que el usuario podría tener con el sistema. Se trata de descubrir un conjunto completo de casos de uso para su sistema, y una vez hecho esto, se tiene el núcleo de lo que el sistema se supone que hará. Lo mejor de centrarse en los casos de uso es que siempre permiten volver a la esencia manteniéndose alejado de aspectos que no son críticos para lograr culminar el trabajo. Es decir, si se tiene un conjunto completo de casos de uso, es posible describir el sistema y pasar a la siguiente fase. Posiblemente no se podrá configurar todo a la primera, pero no pasa nada. Todo irá surgiendo a su tiempo, y si se demanda una especificación perfecta del sistema en este punto, uno se quedará parado. Cuando uno se quede bloqueado, es posible comenzar esta fase utilizando una extensa herramienta de aproximación: describir el sistema en unos pocos párrafos y después localizar los sustantivos y los verbos. Los sustantivos pueden sugerir quiénes son los actores, el contexto del caso de uso (por ejemplo, "corredor"), o artefactos manipulados en el caso de uso. Los verbos pueden sugerir interacciones entre los actores y los casos de uso, y especificar los pasos dentro del caso de uso. También será posible descubrir que los sustantivos y los verbos producen objetos y mensajes durante la fase de diseño (y debe tenerse en cuenta que los casos de uso describen interacciones entre subsistemas, de forma que la técnica de "el sustantivo y el verbo" puede usarse sólo como una herramienta de tormenta de ideas, pues no genera casos de uso)15.

La frontera entre un caso de uso y un actor puede señalar la existencia de una interfaz de usuario, pero no lo define. Para ver el proceso de cómo definir y crear interfaces de usuario, véase Softwarefor Use de Larry Constantine y Lucy Lockwood, (Addison-Wesley Longman, 1999) o ir a http://www.forUse.com. Aunque parezca magia negra, en este punto es necesario algún tipo de planificación. Ahora se tiene una visión de lo que se está construyendo, por lo que probablemente se pueda tener una idea de cuánto tiempo le llevará. En este momento intervienen muchos factores. Si se estima una planificación larga, la compañía puede decidir no construirlo (y por consiguiente usar sus recursos en algo más razonable -esto es bueno). Pero un director podría tener decidido de antemano cuánto tiempo debería llevar el proyecto y podría tratar de influir en la estimación. Pero lo mejor es tener una estimación honesta desde el principio y tratar las decisiones duras al principio. Ha habido muchos inPuede encontrarse más información sobre casos de uso en Applying Use Cases, de Schneider & Winters (Addison-Weley 1998) y Use Case Driven Object modeling with UML de Rosenberg (Addison-Welsey 1999).

l5


1: Introducción a los objetos

39

tentos de desarrollar técnicas de planificación exactas (muy parecidas a las técnicas de predicción del mercado de valores), pero probablemente el mejor enfoque es confiar en la experiencia e intuición. Debería empezarse por una primera estimación del tiempo que llevaría, para posteriormente multiplicarla por dos y añadirle el 10 por ciento. La estimación inicial puede que sea correcta; a lo mejor se puede hacer que algo funcione en ese tiempo. Al "doblarlo" resultará que se consigue algo decente, y en el 10 por ciento añadido se puede acabar de pulir y tratar los detalles finalesl6.Sin embargo, es necesario explicarlo, y dejando de lado las manipulaciones y quejas que surgen al presentar una planificación de este tipo, normalmente funcionará.

Fase 2: ¿Cómo construirlo? En esta fase debe lograrse un diseño que describe cómo son las clases y cómo interactuarán. Una técnica excelente para determinar las clases e interacciones es la tarjeta Clase-ResponsabilidadColaboración (CRC)17.Parte del valor de esta herramienta se basa en que es de muy baja tecnología: se comienza con un conjunto de tarjetas de 3 x 5, y se escribe en ellas. Cada tarjeta representa una única clase, y en ella se escribe: 1.

El nombre de la clase. Es importante que este nombre capture la esencia de lo que hace la clase, de manera que tenga sentido a primera vista.

2.

Las "responsabilidades" de la clase: qué debería hacer. Esto puede resumirse típicamente escribiendo simplemente los nombres de las funciones miembros (dado que esas funciones deberían ser descriptivas en un buen diseño), pero no excluye otras anotaciones. Si se necesita ayuda, basta con mirar el problema desde el punto de vista de un programador holgazán: ¿qué objetos te gustaría que apareciesen por arte de magia para resolver el problema?

3.

Las "colaboraciones" de la clase: ¿con qué otras clases interactúa? "Interactuar" es un término amplio intencionadamente; vendría a significar agregación, o simplemente que cualquier otro objeto existente ejecutara servicios para un objeto de la clase. Las colaboraciones deberían considerar también la audiencia de esa clase. Por ejemplo, si se crea una clase Petardo, ¿quién la va a observar, un Químico o un Observador? En el primer caso estamos hablando de punto de vista del químico que va a construirlo, mientras que en el segundo se hace referencia a los colores y las formas que libere al explotar.

Uno puede pensar que las tarjetas deberían ser más grandes para que cupiera en ellas toda la información que se deseara escribir, pero son pequeñas a propósito, no sólo para mantener pequeño el tamaño de las clases, sino también para evitar que se caiga en demasiado nivel de detalle muy pronto. Si uno no puede encajar todo lo que necesita saber de una clase en una pequeña tarjeta, la clase es demasiado compleja (o se está entrando en demasiado nivel de detalle, o se debería crear más de una clase). La clase ideal debería ser comprensible a primera vista. La idea de las tarjetas CRC es ayudar a obtener un primer diseño de manera que se tenga un dibujo a grandes rasgos que pueda ser después refinado. Mi opinión en este sentido ha cambiado últimamente. Al doblar y añadir el 10 por ciento se obtiene una estimación razonablemente exacta (asumiendo que no hay demasiados factores al azar) pero todavía hay que trabajar con bastante diligencia para finaliar en ese tiempo. Si se desea tener tiempo suficiente para lograr un producto verdaderamente elegante y disfrutar durante el proceso, el multiplicador correcto, en mi opinión, puede ser por tres o por cuatro. l7 En inglés, Class-Responsibility-Collaboration. (N. del R.T.)


40

Piensa en Java

Una de las mayores ventajas de las tarjetas CRC se logra en la comunicación. Cuando mejor se hace es en tiempo real, en grupo y sin computadores. Cada persona se considera responsable de varias clases (que al principio no tienen ni nombres ni otras informaciones). Se ejecuta una simulación en directo resolviendo cada vez un escenario, decidiendo qué mensajes se mandan a los distintos objetos para satisfacer cada escenario. A medida que se averiguan las responsabilidades y colaboraciones de cada una, se van rellenando las tarjetas correspondientes. Cuando se han recorrido todos los casos de uso, se debería tener un diseño bastante completo. Antes de empezar a usar tarjetas CRC, tuve una experiencia de consultoría de gran éxito, que me permitió presentar un diseño inicial a todo el equipo, que jamás había participado en un proyecto de POO, y que consistió en ir dibujando objetos en una pizarra, después de hablar sobre cómo se deberían comunicar los objetos entre sí, y tras borrar algunos y reemplazar otros. Efectivamente, estaban haciendo uso de "tarjetas CRC" en la propia pizarra. El equipo (que sabía que el proyecto se iba a hacer) creó, de hecho, el diseño; ellos eran los "propietarios" del diseño, más que recibirlo hecho directamente. Todo lo que yo hacía era guiar el proceso haciendo en cada momento las preguntas adecuadas, poniendo a prueba los distintos supuestos, y tomando la realimentación del equipo para ir modificando los supuestos. La verdadera belleza del proyecto es que el equipo aprendió cómo hacer diseño orientado a objetos no repasando ejemplos o resúmenes de ejemplos, sino trabajando en el diseño que les pareció más interesante en ese momento: el de ellos mismos. Una vez que se tiene un conjunto de tarjetas CRC se desea crear una descripción más formal del diseño haciendo uso de UML18. No es necesario utilizar UML, pero puede ser de gran ayuda, especialmente si se desea poner un diagrama en la pared para que todo el mundo pueda ponderarlo, lo cual es una gran idea. Una alternativa a UML es una descripción textual de los objetos y sus interfaces, o, dependiendo del lenguaje de programación, el propio cÓdigol9. UML también proporciona una notación para diagramas que permiten describir el modelo dinámico del sistema. Esto es útil en situaciones en las que las transiciones de estado de un sistema o subsistema son lo suficientemente dominantes como para necesitar sus propios diagramas (como ocurre en un sistema de control). También puede ser necesario describir las estructuras de datos, en sistemas o subsistemas en los que los datos sean un factor dominante (como una base de datos). Sabemos que la Fase 2 ha acabado cuando se han descrito los objetos y sus interfaces. Bueno, la mayoría -hay generalmente unos pocos que quedan ocultos y que no se dan a conocer hasta la Fase 3. Pero esto es correcto. En lo que a uno respecta, esto es todo lo que se ha podido descubrir de los objetos a manipular. Es bonito descubrirlos en las primeras etapas del proceso pero la PO0 proporciona una estructura tal, que no presenta problema si se descubren más tarde. De hecho, el diseño de un objeto tiende a darse en cinco etapas, a través del proceso completo de desarrollo de un programa.

' V a r a los principiantes, recomiendo UML Distilled, 2." edición.

'"han

(http://www.Python.orgJ suele utilizarse como "pseudocódigo ejecutable".


1: Introducción a los objetos

41

Las cinco etapas del diseño de un objeto La duración del diseño de un objeto no se limita al tiempo empleado en la escritura del programa, sino que el diseño de un objeto conlleva una serie de etapas. Es útil tener esta perspectiva porque se deja de esperar la perfección; por el contrario, uno comprende lo que hace un objeto y el nombre que debería tener surge con el tiempo. Esta visión también se aplica al diseño de varios tipos de programas; el patrón para un tipo de programa particular emerge al enfrentarse una y otra vez con el problema (esto se encuentra descrito en el libro Thinking in Patterns with Java, descargable de http://www.BruceEckel.com). Los objetos, también tienen su patrón, que emerge a través de su entendimiento, uso y reutiliiación. 1. Descubrimiento de los objetos. Esta etapa ocurre durante el análisis inicial del programa. Se descubren los objetos al buscar factores externos y limitaciones, elementos duplicados en el sistema, y las unidades conceptuales más pequeñas. Algunos objetos son obvios si ya se tiene un conjunto de bibliotecas de clases. La comunidad entre clases que sugieren clases bases y herencia, puede aparecer también en este momento, o más tarde dentro del proceso de diseño. 2 . Ensamblaje de objetos. Al construir un objeto se descubre la necesidad de nuevos miembros

que no aparecieron durante el descubrimiento. Las necesidades internas del objeto pueden requerir de otras clases que lo soporten. 3 . Construcción del sistema. De nuevo, pueden aparecer en esta etapa más tardía nuevos requi-

sitos para el objeto. Así se aprende que los objetos van evolucionando. La necesidad de un objeto de comunicarse e interconectarse con otros del sistema puede hacer que las necesidades de las clases existentes cambien, e incluso hacer necesarias nuevas clases. Por ejemplo, se puede descubrir la necesidad de clases que faciliten o ayuden, como una lista enlazada, que contiene poca o ninguna información de estado y simplemente ayuda a la función de otras clases. 4. Aplicación del sistema. A medida que se añaden nuevos aspectos al sistema, puede que se descubra que el diseño previo no soporta una ampliación sencilla del sistema. Con esta nueva información, puede ser necesario reestructurar partes del sistema, generalmente añadiendo nuevas clases o nuevas jerarquías de clases.

5 . Reutilización de objetos. Ésta es la verdadera prueba de diseño para una clase. Si alguien trata de reutilizarla en una situación completamente nueva, puede que descubra pequeños inconvenientes. Al cambiar una clase para adaptarla a más programas nuevos, los principios generales de la clase se mostrarán más claros, hasta tener un tipo verdaderamente reutilizable. Sin embargo, no debe esperarse que la mayoría de objetos en un sistema se diseñen para ser reutilizados -es perfectamente aceptable que un porcentaje alto de los objetos sean específicos del sistema para el que fueron diseñados. Los tipos reutilizables tienden a ser menos comunes, y deben resolver problemas más generales para ser reutilizables.

Guías para el desarrollo de objetos Estas etapas sugieren algunas indicaciones que ayudarán a la hora de pensar en el desarrollo de clases: 1.

Debe permitirse que un problema específico genere una clase, y después dejar que la clase crezca y madure durante la solución de otros problemas.


Piensa en Java

Debe recordarse que descubrir las clases (y SUS interfaces) que uno necesita es la tarea principal del diseño del sistema. Si ya se disponía de esas clases, el proyecto será fácil. No hay que forzar a nadie a saber todo desde el principio; se aprende sobre la marcha.Y esto ocurrirá poco a poco. Hay que empezar programando; es bueno lograr algo que funcione de manera que se pueda probar la validez o no de un diseño. No hay que tener miedo a acabar con un código de estilo procedimental malo -las clases segmentan el problema y ayudan a controlar la anarquía y la entropía. Las clases malas no estropean las clases buenas. Hay que mantener todo lo más simple posible. Los objetos pequeños y limpios con utilidad obvia son mucho mejores que interfaces grandes y complicadas. Cuando aparecen puntos de diseño puede seguirse el enfoque de una afeitadora de Occam: se consideran las alternativas y se selecciona la más simple, porque las clases simples casi siempre resultan mejor. Hay que empezar con algo pequeño y sencillo, siendo posible ampliar la interfaz de la clase al entenderla mejor. A medida que avance el tiempo será difícil eliminar elementos de una clase.

Fase 3: Construir el núcleo Ésta es la conversión inicial de diseño pobre en un código compilable y ejecutable que pueda ser probado, y especialmente, que pueda probar la validez o no de la arquitectura diseñada. Este proceso no se puede hacer de una pasada, sino que consistirá más bien en una serie de pasos que permitirán construir el sistema de manera iterativa, como se verá en la Fase 4. Su objetivo es encontrar el núcleo de la arquitectura del sistema que necesita implementar para generar un sistema ejecutable, sin que importe lo incompleto que pueda estar este sistema en esta fase inicial. Está creando un armazón sobre el que construir en posteriores iteraciones. También se está llevando a cabo la primera de las muchas integraciones y pruebas del sistema, a la vez que proporcionando a los usuarios una realimentación sobre la apariencia que tendrá su sistema, y cómo va progresando. Idealmente, se están además asumiendo algunos riesgos críticos. De hecho, se descubrirán posibles cambios y mejoras que se pueden hacer sobre el diseño original -cosas que no se hubieran descubierto de no haber implementado el sistema. Una parte de la construcción del sistema es comprobar que realmente se cumple el análisis de requisitos y la especificación del sistema que realmente cumple el analisis de requisitos y la especificación del sistema (independientemente de la forma en que estén planteados). Debe asegurarse que las pruebas verifican los requerimientos y los casos de uso. Cuando el corazón del sistema sea estable, será posible pasar a la siguiente fase y añadir nuevas funcionalidades.

Fase 4: Iterar los casos de uso Una vez que el núcleo del sistema está en ejecución, cada característica que se añada es en sí misma un pequeño proyecto. Durante cada iteración,entendida como un periodo de desarrollo razonablemente pequeño, se añade un conjunto de características.


1: Introducción a los objetos

43

$tíal debe ser la duración de una iteración? Idealmente, cada iteración dura de una a tres semanas (la duración puede variar en función del lenguaje de implementación). Al final de ese periodo, se tiene un sistema integrado y probado con una funcionalidad mayor a la que tenía previamente. Pero lo particularmente interesante es la base de la iteración: un único caso de uso. Cada caso de uso es un paquete de funcionalidad relacionada que se construye en el sistema de un golpe, durante una iteración. Esto no sólo proporciona una mejor idea de lo que debería ser el ámbito de un caso de uso, sino que además proporciona una validación mayor de la idea del mismo, dado que el concepto no queda descartado hasta después del análisis y del diseño, pues éste es una unidad de desarrollo fundamental a lo largo de todo el proceso de construcción de software. Se deja de iterar al lograr la funcionalidad objetivo, o si llega un plazo y el cliente se encuentra satisfecho con la versión actual (debe recordarse que el software es un negocio de suscripción). Dado que el proceso es iterativo, uno puede tener muchas oportunidades de lanzar un producto, más que tener un único punto final; los proyectos abiertos trabajan exclusivamente en entornos iterativos de gran nivel de realimentación, que es precisamente lo que les permite acabar con éxito. Un proceso de desarrollo iterativo tiene gran valor por muchas razones. Si uno puede averiguar y resolver pronto los riesgos críticos, los clientes pueden tener muchas oportunidades de cambiar de idea, la satisfacción del programador es mayor, y el proyecto puede guiarse con mayor precisión. Pero otro beneficio adicional importante es la realimentación a los usuarios, que pueden ver a través del estado actual del producto cómo va todo. Así es posible reducir o eliminar la necesidad de reuniones de estado "entumece-mentes" e incrementar la confianza y el soporte de los usuarios.

Fase 5: Evolución Éste es el punto del ciclo de desarrollo que se ha denominado tradicionalmente "mantenimiento", un término global que quiere decir cualquier cosa, desde "hacer que funcione de la manera que se suponía que lo haría en primer lugar", hasta "añadir aspectos varios que el cliente olvidó mencionar", pasando por el tradicional "arreglar los errores que puedan aparecer" o "la adición de nuevas características a medida que aparecen nuevas necesidades". Por ello, al término "mantenimiento" se le han aplicado numerosos conceptos erróneos, lo que ha ocasionado un descenso progresivo de su calidad, en parte porque sugiere que se construyó una primera versión del programa en la cual hay que ir cambiando partes, además de engrasarlo para evitar que se oxide. Quizás haya un término mejor para describir lo que está pasando. Prefiero el término evolución2".De esta forma, "uno no acierta a la primera, por lo que debe concederse la libertad de aprender y volver a hacer nuevos cambios". Podríamos necesitar muchos cambios a medida que vamos aprendiendo y comprendiendo con más detenimiento el problema. A corto y largo plazo, será el propio programa el que se verá beneficiado de este proceso continuo de evolución. De hecho, ésta permitirá que el programa pase de bueno a genial, haciendo que se aclaren aquellos aspectos que no fueron verdaderamente entendidos en la primera pasada. También es El libro de Martin Fowler Refactoring: improuing the design of existing code (Addison-Wesley, 1999) cubre al menos un aspecto de la evolución, utilizando exclusivamente ejemplos en Java.

20


44

Piensa en Java

en este proceso en el que las clases se convierten en recursos reutilizables, en vez de clases diseñadas para su uso en un solo proyecto. "Hacer el proyecto bien" no sólo implica que el programa funcione de acuerdo con los requisitos y casos de uso. También quiere decir que la estructura interna del código tenga sentido, y que parezca que encaja bien, sin aparentar tener una sintaxis extraña, objetos de tamaño excesivo o con fragmentos inútiles de código. Además, uno debe tener la sensación de que la estructura del programa sobrevivirá a los cambios que inevitablemente irá sufriendo a lo largo de su vida, y de que esos cambios se podrán hacer de forma sencilla y limpia. Esto no es trivial. Uno no sólo debe entender qué es lo que está construyendo, sino también cómo evolucionará el programa (lo que yo denomino el vector del cambio). Afortunadamente, los lenguajes de programación orientada a objetos son especialmente propicios para soportar este tipo de modificación continua -los límites creados por los objetos son los que tienden a lograr una estructura sólida. También permiten hacer cambios -que en un programa procedural parecerían drásticos- sin causar terremotos a lo largo del código. De hecho, el soporte a la evolución podría ser el beneficio más importante de la POO. Con la evolución, se crea algo que al menos se aproxima a lo que se piensa que se-está construyendo, se compara con los requisitos, y se ve dónde se ha quedado corto. Después, se puede volver y ajustarlo diseñando y volviendo a implementar las porciones del programa que no funcionaron correctamente". De hecho, es posible que se necesite resolver un problema, o determinado aspecto de un problema, varias veces antes de dar con la solución correcta (suele ser bastante útil estudiar en este momento el Diseño de Patrones). También es posible encontrar información en Thinking in Patterns with Java, descargable de http://www.BruceEcke1.com).

La evolución también se da al construir un sistema, ver que éste se corresponda con los requisitos,

y descubrir después que no era, de hecho, lo que se pretendía. Al ver un sistema en funcionamiento, se puede dcscubrir que verdaderamente se pretendía que solucionase otro problema. Si uno espera que se dé este tipo de evolución, entonces se debe construir la primera versión lo más rápidamente posible con el propósito de averiguar sin lugar a dudas qué es exactamente lo que se desea.

Quizás lo más importante que se ha de recordar es que por defecto, si se modifica una clase, sus súper y subclases seguirán funcionando. Uno no debe tener miedo a la modificación (especialmente si se dispone de un conjunto de pruebas, o alguna prueba individual que permita verificar la corrección de las modificaciones). Los cambios no tienen por qué estropear el programa, sino que cualquiera de las consecuencias de un cambio se limitarán a las subclases y/o colaboradores específicos de la clase que se modifica.

Los planes merecen la pena Por supuesto, uno jamás construiría una casa sin unos planos cuidadosamente elaborados. Si construyéramos un hangar o la casa de un perro, los planes no tendrían tanto nivel de detalle, pero pro?' Esto es semejante a la elaboración de "prototipos rápidos", donde se supone que uno construyc una versión "rápida y sucia" que permite comprender mejor el sistema, pero que e s después desechada para construirlo correctamente. El problema con el prototipado rápido e s que los equipos de desarrollo no suelen desechar completamente el prototipo, sino que lo utilizan como base sobre la que construir. Si se combina, en la programación procedural, con la falta de estructura, se generan sistemas totalmente complicados, y difíciles de mantener.


1: Introducción a los objetos

45

bablemente comenzaríamos con una serie de esbozos que nos permitiesen guiar el proceso. El desarrollo de software ha llegado a extremos. Durante mucho tiempo, la gente llevaba a cabo desarrollos sin mucha estructura, pero después, comenzaron a fallar los grandes procesos. Como reacción, todos acabamos con metodologías que conllevan una cantidad considerable de estructura y detalle, eso sí, diseñadas, en principio, para estos grandes proyectos. Estas metodologías eran demasiado tediosas de usar -parecía que uno invertiría todo su tiempo en escribir documentos, y que no le quedaría tiempo para programar (y esto ocurría a menudo). Espero haber mostrado aquí una serie de sugerencias intermedias. Independientemente de lo pequeño que sea, es necesario algún tipo de plan, que redundará en una gran mejora en el proyecto, especialmente respecto del que se obtendría si no se hiciera ningún plan de ningún tipo. Es necesario recordar que en muchas estimaciones, falla más del 50 por ciento del proyecto (iincluso en ocasiones se llega al 70 por ciento!). Siguiendo un plan -preferentemente uno simple y breve- y siguiendo una estructura de diseño antes de la codificación, se descubre que los elementos encajan mejor, de modo más sencillo que si uno se zambulle y empieza a escribir código sin ton ni son. También se alcanzará un nivel de satisfacción elevado. La experiencia dice que al lograr una solución elegante uno acaba completamente satisfecho, a un nivel totalmente diferente; uno se siente más cercano al arte que a la tecnología. Y la elegancia siempre merece la pena; no se trata de una pcrsccución frívola. De hccho, no solamen-

te proporciona un programa más fácil de construir y depurar, sino que éste es mucho más fácil de entender y mantener, que es precisamente donde reside su valor financiero.

Programación extrema Una vez estudiadas las técnicas de análisis y diseño, por activa y por pasiva durante mucho tiempo, quizás el concepto de programación extrema (Extreme Programming, XP) sea el más radical y sorprendente que he visto. Es posible encontrar información sobre él mismo en Extreme Programming Explained, de Kent Beck (Addison-Wesley 2000), que puede encontrarse también en la Web en http://www.xprogramming.com.

XP es tanto una filosofía del trabajo de programación como un conjunto de guías para acometer esta tarea. Algunas de estas guías se reflejan en otras metodologías recientes, pero las dos contribuciones más distintivas e importantes en mi opinión son "escribir las pruebas en primer lugar" y "la programación a pares". Aunque Beck discute bastante todo el proceso en sí, señala que si se adoptan únicamente estas dos prácticas, uno mejorará enormemente su productividad y nivel de confianza.

Escritura de las pruebas en primer lugar El proceso de prueba casi siempre ha quedado relegado al final de un proyecto, una vez que "se tiene todo trabajando, pero hay que asegurarlo". Implícitamente, tenía una prioridad bastante baja, y la gente que se especializa en las pruebas nunca ha gozado de un gran estatus, e incluso suele estar ubicada en el sótano, lejos de los "programadores de verdad". Los equipos de pruebas se han amoldado tanto a esta consideración que incluso han llegado a vestir de negro, y han chismorreado alegremente cada vez que lograban encontrar algún fallo (para ser honestos, ésta es la misma sensación que yo tenía cada vez que lograba encontrar algún fallo en un compilador).


46

Piensa en Java

XP revoluciona completamente el concepto de prueba dándole una prioridad igual (o incluso mayor) que a la codificación. De hecho, se escriben los tests antes de escribir el código a probar, y los códigos se mantienen para siempre junto con su código destino. Es necesario ejecutar con éxito los tests cada vez que se lleva a cabo un proceso de integración del proyecto (lo cual ocurre a menudo, en ocasiones más de una vez al día).

Al principio la escritura de las pruebas tiene dos efectos extremadamente importantes. El primero es que fuerza una definición clara de la interfaz de cada clase. Yo, en numerosas ocasiones he sugerido que la gente "imagine la clase perfecta para resolver un problema particular" como una herramienta a utilizar a la hora de intentar diseñar el sistema. La estrategia de pruebas XP va más allá -especifica exactamente qué apariencia debe tener la clase para el consumidor de la clase, y cómo ésta debe comportarse exactamente. No puede haber nada sin concretar. Es posible escribir toda la prosa o crear todos los diagramas que se desee, describiendo cómo debería comportarse una clase, pero nada es igual que un conjunto de pruebas. Lo primero es una lista de deseos, pero las pruebas son un contrato reforzado por el compilador y el programa en ejecución. Cuesta imaginar una descripción más exacta de una clase que la de los tests.

Al crear los tests, uno se ve forzado a pensar completamente en la clase, y a menudo, descubre la funcionalidad deseada que podría haber quedado en el tintero durante las experiencias de pensamiento de los diagramas XML, las tarjetas CRC, los casos de uso, etc. El segundo efecto importante de escribir las pruebas en primer lugar, proviene de la ejecución de las pruebas cada vez que se construye un producto software. Esta actividad proporciona la otra mitad de las pruebas que lleva a cabo el compilador. Si se observa la evolución de los lenguajes de programación desde esta perspectiva, se llegará a la conclusión de que las verdaderas mejoras en lo que a tecnología se refiere han tenido que ver con las pruebas. El lenguaje ensamblador solamente comprobaba la sintaxis, pero C imponía algunas restricciones semánticas, que han evitado que se produzca cierto tipo de errores. Los lenguajes PO0 imponen incluso más restricciones semánticas, que miradas así no son, de hecho, sino métodos de prueba. "¿Se está utilizando correctamente este tipo de datos?", y "¿se está invocando correctamente a esta función?" son algunos de los tipos de preguntas que hace un compilador o un sistema en tiempo de ejecución. Se han visto los resultados de tener estas pruebas ya incluidas en el lenguaje: la gente parece ser capaz de escribir sistemas más completos y hacer que funcionen, con menos cantidad de tiempo y esfuerzo. He intentado siempre averiguar la razón, pero ahora lo tengo claro, son las pruebas: cada vez que se hace algo mal, la red de pruebas de seguridad integradas dice que hay un problema y determina dónde. Pero las pruebas integradas permitidas por el diseño del lenguaje no pueden ir mucho más allá. En cierto punto, cada uno debe continuar y añadir el resto de pruebas que producen una batería de pruebas completa (en cooperación con el compilador y el sistema en tiempo de ejecución) que verifique todo el programa. Y, exactamente igual que si se dispusiera de un compilador observando por encima del hombro, ¿no desearía uno que estas pruebas le ayudasen a hacer todo bien desde el principio? Por eso es necesario escribir las pruebas en primer lugar y ejecutarlas cada vez que se reconstruya el sistema. Las pruebas se convierten en una extensión de la red de seguridad proporcionada por el lenguaje.


1: Introducción a los objetos

47

Una de las cosas que he descubierto respecto del uso de lenguajes de programación cada vez más y más potentes es que conducen a la realización de experimentos cada vez más duros, pues se sabe a priori que el propio lenguaje evitará pérdidas innecesarias de tiempo en la localización de errores. El esquema de pruebas XP hace lo mismo para todo el proyecto. Dado que se sabe que las pruebas localizarán cualquier problema que pueda aparecer en la vida del proyecto (y cada vez que se nos ocurra alguno), simplemente se introducen nuevas pruebas), es posible hacer cambios, incluso grandes, cuando sea necesario sin preocuparse de que éstos puedan cargarse todo el proyecto. Esto es increíblemente potente.

Programación a pares La programación a pares (por parejas) va más allá del férreo individualismo al que hemos sido adoctrinados desde el principio, a través de las escuelas (donde es uno mismo el que fracasa o tiene éxito), de los medios de comunicación, especialmente las películas de Hollywood, en las que el héroe siempre lucha contra la conformidad sin sentido". Los programadores, también, suelen considerarse abanderados de la individualidad -"los

vaqueros codificadores" como suele llamarlos Larry

Constantine. Y por el contrario, XP, que trata, de por sí, de luchar contra el pensamiento convencional, enuncia lo contrario, afirmando que el código debería siempre escribirse entre dos personas por cada estación de trabajo. Y esto debería hacerse en áreas en las que haya grupos de estaciones de trabajo, sin las barreras de las que la gente de facilidades de diseno suelen estar tan orgullosos. De hecho, Beck dice que la primera tarea para convertirse a XP es aparecer con destornilladores y llaves Allen y desmontar todo aquello que parezca imponer barreras o separaciones'" (esto exige contar con un director capaz de hacer frente a todas las quejas del departamento de infraestructuras). El valor de la programación en pareja es que una persona puede estar, de hecho, codificando mientras la otra piensa en lo que se está haciendo. El pensador es el que tiene en la cabeza todo el esbozo -y no sólo una imagen del problema que se está tratando en ese momento, sino todas las guías del XP. Si son dos las personas que están trabajando, es menos probable que uno de ellos huya diciendo "No quiero escribir las pruebas lo primero", por ejemplo. Y si el codificador se queda clavado, pueden cambiar de sitio. Si los dos se quedan parados, puede que alguien más del área de trabajo pueda contribuir al oír sus meditaciones. Trabajar a pares hace que todo fluya mejor y a tiempo. Y lo que probablemente es más importante: convierte la programación en una tarea mucho más divertida y social. He comenzado a hacer uso de la programación en pareja durante los periodos de ejercitación en algunos de mis seminarios, llegando a la conclusión de que mejora significativamente la experiencia de todos.

" Aunque probablemente ésta sea mas una perspectiva americana, las historias de Hollywood llegan a todas partes. Incluido (especialmente) el sistema PA. Trabajé una vez en una compañia que insistía en difundir a todo el mundo cualquier Ilamada entrante que recibieran los ejecutivos, lo cual interrumpía continuamente la productividad del equipo (pero los directores no podían empezar siquiera a pensar en prescindir de un servicio tan importante como el PA). Al final, y cuando nadie me veía, me encargué de cortar los cables de los altavoces. 23


48

Piensa en Java

Por qué Java tiene éxito La razón por la que Java ha tenido tanto éxito es que su propósito era resolver muchos de los problemas a los que los desarrolladores se enfrentan hoy en día. El objetivo de Java es mejorar la productividad. Esta productividad se traduce en varios aspectos, pero el lenguaje fue diseñado para ayudar lo máximo posible, dejando en manos de cada uno la mínima cantidad posible, tanto de reglas arbitrarias, como de requisitos a usar en determinados conjuntos de aspectos. Java fue diseñado para ser práctico; las decisiones de diseño del lenguaje Java se basaban en proporcionar al programador la mayor cantidad de beneficios posibles.

Los sistemas son más fáciles d e expresar y entender Las clases diseñadas para encajar en el problema tienden a expresarlo mejor. Esto significa que al escribir el código uno está describiendo su solución en términos del espacio del problema, en vez

de en términos del computador, que es el espacio de la solución ("Pon el bit en el chip que indica que el relé se va cerrar"). Uno maneja conceptos de alto nivel y puede hacer mucho más con una única línea de código. El otro beneficio del uso de esta expresión es la mantenibilidad que (si pueden creerse los informes) se lleva una porción enorme del coste de un programa durante toda su vida. Si un programa es fácil de entender, entonces es fácil de mantener. Esto también puede reducir el coste de crear y mantener la documentación.

Ventajas máximas c o n las bibliotecas La manera más rápida de crear un programa es utilizar código que ya esté escrito: una biblioteca. Uno de los principales objetivos de Java es facilitar el uso de bibliotecas. Esta meta se logra convirtiendo las bibliotecas en nuevos tipos de datos (clases), de forma que la incorporación de una biblioteca equivale a la inserción de nuevos tipos al lenguaje. Dado que el compilador de Java se encarga del buen uso de las bibliotecas -garantizando una inicialización y eliminación completas, y asegurando que se invoca correctamente a las funciones- uno puede centrarse en lo que desea que haga la biblioteca en vez de cómo tiene que hacerlo.

Manejo de errores El manejo de errores en C es un importante problema, que suele ser frecuentemente ignorado o que se trata de evitar cruzando los dedos. Si se está construyendo un programa grande y complejo, no hay nada peor que tener un error enterrado en algún sitio sin tener ni siquiera una pista de dónde puede estar. El manejo de excepciones de Java es una forma de garantizar que se notifiquen los errores, y que todo ocurre como consecuencia de algo.


1: Introducción a los objetos

49

programación a lo grande Muchos lenguajes de programación "tradicionales" tenían limitaciones intrínsecas en lo que al tamaño y complejidad del programa se refiere. BASIC, por ejemplo, puede ser muy bueno para poner juntas soluciones rápidas para cierto tipo de problemas, pero si el programa se hace mayor de varias páginas, o se sale del dominio normal del problema, es como intentar nadar en un fluido cada vez más viscoso. No hay una línea clara que permita separar cuándo está fallando el lenguaje, y si la hubiera, la ignoraríamos. Uno no dice "Mi programa en BASIC simplemente creció demasiado; tendré que volver a escribirlo en C". Más bien se intenta meter con calzador unas pocas líneas para añadir alguna nueva característica. Por tanto, el coste extra viene dependiendo de uno mismo. Java está diseñado para ayudar a programar a lo grande -es decir, para borrar esos límites de complejidad entre un programa pequeño y uno grande. Uno no tiene por qué usar PO0 al escribir un programa de utilidad del estilo de "iHola, mundo!", pero estas características siempre están ahí cuando son necesarias. Y el compilador se muestra agresivo a la hora de descubrir las causas generadora~de errores, tanto eri el caso de programas g r arides, corrio pequeíius.

Estrategias para la t r a n s i c i ó n Si uno se introduce en la POO, la siguiente pregunta será probablemente "¿Cómo puedo hacer que mi director, mis colegas, mi departamento,. .. empiecen a usar objetos?". Uno debe pensar en cómo él mismo -un programador independiente- se sentiría a la hora de aprender un nuevo lenguaje y un nuevo paradigma de programación. A fin de cuentas, ya lo ha hecho antes. Lo primero es la educación y el uso de ejemplos, después viene un proyecto de prueba que proporcione una idea clara de los fundamentos sin hacer algo demasiado confuso. Después viene un proyecto "del mundo real" que, de hecho, haga algo útil. A lo largo de los primeros proyectos, uno sigue su educación leyendo y preguntando a los expertos, a la vez que solucionando pequeños inconvenientes con los colegas. Éste es el enfoque que muchos programadores experimentados sugieren de cara a migrar a Java. Cambiar una compañía entera, por supuesto implicaría la introducción de alguna dinámica de grupo, pero ayudará a recordar en cada paso cómo debería desenvolverse cada uno.

Guías He aquí algunas ideas o guías a tener en cuenta cuando se haga la transición a PO0 y Java:

1. Formación El primer paso es algún tipo dc educación. Hay que recordar la inversión en código de la compañía, e intentar no tirar todo a la basura durante los seis a nueve meses que lleve a todo el mundo enterarse de cómo funcionan las interfaces. Es mejor seleccionar un pequeño grupo para adoctrinarles, compuesto preferentemente por personas curiosas, y que trabajen bien en grupo, que pueda luego funcionar como una red de soporte propia mientras se esté aprendiendo Java.


50

Piensa en Java

Un enfoque alternativo recomendado en ocasiones, es formar a todos los niveles de la compañía a la vez, incluidos cursos muy por encima para los directores de estrategia, además de cursos de diseño y programación para los constructores de proyectos. Esto es especialmente bueno para las pequeñas compañías que cambian continuamente la manera de hacer las cosas, o a nivel de divisiones en aquellas compañías de gran tamaño. Dado que el coste es elevado, sin embargo, hay que elegir empezar de alguna manera con la formación a nivel de proyecto, llevar a cabo un proyecto piloto (posiblemente con un formador externo) y dejar que el equipo de proyecto se convierta en el grupo de profesores del resto de la compañía.

2. Proyecto de bajo riesgo Es necesario empezar con un proyecto de bajo riesgo y permitir los errores. Una vez que se ha adquirido cierta experiencia, uno puede alimentarse bien de proyectos de miembros del mismo equipo, o bien utilizar a los miembros del equipo como personal de soporte técnico para POO. Puede que el primer proyecto no funcione correctamente a la primera, por lo que no debería ser crítico con la misión de la compañía. Debería ser simple, independiente, e instructivo; esto significa que debe-

ría conllevar la creación de clases con significado para cuando les llegue el turno de aprender Java al resto de empleados de la compañía.

3. Modelo que ya ha tenido éxito Es necesario buscar ejemplos con un buen diseño orientado a objetos en vez de empezar de la nada. Hay muchas posibilidades de que exista alguien que ya haya solucionado el problema en cuestión, o que si no lo ha solucionado del todo pueda aplicar lo ya aprendido sobre la abstracción para modificar un diseño ya existente en aras de que se ajuste a tus necesidades. Éste es el concepto general de los patrones de diseño, cubiertos en Thinking in Patterns with Java, descargable de http://www. BruceEcke1.com.

4. Utilizar bibliotecas de clases existentes La motivación económica principal para cambiar a PO0 es la facilidad de usar código ya existente en forma de bibliotecas de clases (en particular las bibliotecas estándares de Java, cubiertas completamente a lo largo de este libro). Se obtendrá el ciclo de desarrollo más pequeño posible cuando se puedan crear y utilizar objetos de bibliotecas preconfeccionadas. Sin embargo, algunos programadores novatos no entienden este concepto, y son inconscientes de la existencia de bibliotecas de clases. El éxito con la PO0 y Java será óptimo si se hace un esfuerzo para buscar y reutilizar el código ya desarrollado cuanto antes en el proceso de transición.

5 . No reescribir en Java código ya existente No suele ser la mejor de las ideas tomar código ya existente y que funcione y reescribirlo en Java (si se convierten en objetos, es posible interactuar con código ya escrito en C o C++ haciendo uso de la Interfaz Nativa Java (lava Native Interface) descrito en el Apéndice B). Hay beneficios incrementales, especialmente si cl código va a ser reutilizado. Pero todas las opciones pasan porque no se darán los incrementos drásticos de productividad que uno pudiera esperar para su primer pro-


1: Introducción a los objetos

51

yecto a no ser que se acometa uno totalmente nuevo. Java y la P O 0 brillan mucho más cuando se pasa de un proyecto conceptual al real correspondiente.

Obstáculos de gestión Para aquél que sea director, su trabajo consiste en adquirir los recursos para el equipo, superar las barreras que puedan dificultar el éxito del equipo, y en general, intentar proporcionar el entorno más productivo que permita al equipo disfrutar y conseguir así, llevar a cabo esos milagros que siempre se exigen. Pasarse a Java implica estas tres características, y sería maravilloso que el coste fuera, además nulo. Aunque pasarse a Java puede ser más barato -en función de las limitaciones de cada uno- que las alternativas de la P O 0 para un equipo de programadores en C (y probablemente para los programadores de otros lenguajes procedurales) no es gratuito, y hay obstáculos de los que uno debería ser consciente a la hora de vender el pasarse a Java dentro de una compañía y quedar totalmente embarrancado.

Costes iniciales El coste de pasarse a Java es mayor que el de adquirir compiladores de Java (el compilador de Java de Sun es gratuito, así que éste difícilmente podría constituir un obstáculo). Los costes a medio y largo plazo se minimizan si se invierte en formación (y posiblemente si se utiliza un formador durante el primer proyecto) y también si se identifica y adquiere una biblioteca de clases que solucione el problema en vez de intentar construir esas bibliotecas uno mismo. Éstos son costes muy elevados que deben ser cuantificados en una propuesta realista. Además, están los costes ocultos de la pérdida de productividad implícita en el aprendizaje de un nuevo lenguaje, y probablemente un nuevo entorno de programación. La formación y la búsqueda de un formador pueden, a ciencia cierta, minimizar estos costes, pero los miembros del equipo deberán sobreponerse a sus propios problemas para comprender la nueva tecnología. Durante este proceso, ellos cometerán más fallos (éste es un aspecto importante, pues los errores reconocidos son la forma más rápida de aprender) y serán menos productivos. Incluso entonces, con algunos tipos de problemas de programación, las clases correctas y el entorno de desarrollo correcto, es posible ser más productivo mientras se está aprendiendo Java (incluso considerando que se están cometiendo más fallos y escribiendo menos 1íneas de código cada día) que si se continuara con C.

Aspectos de rendimiento Una pregunta frecuente es "¿La P O 0 hace que los programas se conviertan en más grandes y lentos automáticamente?".La respuesta es: "Depende". Los aspectos extra de seguridad de Java tradicionalmente han conllevado una penalización en el rendimiento, frente a lenguajes como C++.Las tecnologías como "hotspot" y las tecnologías de compilación han mejorado significativamente la velocidad en la mayoría dc los casos, y se continúan haciendo esfuerzos para lograr un rendimiento aún mayor. Cuando uno se centra en un prototipado rápido, es posible desechar conjuntamente componentes lo más rápido posible, a la vez que se ignoran ciertos aspectos de eficiencia. Si se utilizan bibliotecas de un tercero, éstas suelen estar optimizadas por el propio fabricante; en cualquier caso, esto no es un problema cuando uno está en modo de desarrollo rápido. Cuando se tiene el sistema deseado,


52

Piensa en Java

que sea lo suficientemente pequeño y rápido, entonces, ya está. Si no, hay que empezar a reescribir pequeñas porciones de código. Si a pesar de esto no se mejora, hay que pensar cómo hacer modificaciones en la implementacion subyacente, de forma que no haya ningún código que use una clase concreta que vaya a ser modificada. Sólo si no se encuentra ninguna otra solución al problema se acometerán posibles cambios en el diseño. El hecho de que el rendimiento sea crítico en esa porción del diseño es un indicador que debe formar parte del criterio de diseño principal. La utilización del desarrollo rápido ofrece la ventaja de poder averiguar esto muy pronto. Si se encuentra una función que constituya un cuello de botella, es posible reescribirla en C/C++ haciendo uso de los métodos nativos de Java, sobre los que versa el Apéndice B.

Errores de diseño comunes Cuando un equipo empieza a trabajar en PO0 y Java, los programadores cometerán una serie de errores de diseño comunes. Esto ocurre a menudo debido a que hay una realimentación insuficiente por parte de los expertos durante el discño e implementación de los primeros proyectos, puesto que

no han aparecido expertos dentro de la compañía y porque puede que haya cierta resistencia en la empresa para retener a los consultores. Es fácil que si alguien cree entender la PO0 desde las primeras etapas del ciclo trate de atajar a través de una tangente errónea. Algo que es obvio a los ojos de una persona experta en el lenguaje, puede llegar a constituir un gran problema o debate interno para un novato. Podría evitarse un porcentaje elevado de este trauma si se utilizara un experto externo experimentado como formador y consejero.

LJava frente a C + + ? Java se parece bastante a C++,y naturalmente podría parecer que C++está siendo reemplazado por Java. Pero me empiezo a cuestionar esta lógica. Para algunas cosas, C++ sigue teniendo una serie de características que Java no tiene, y aunque ha habido muchas promesas de que algún día Java llegará a ser tan o más rápido que C++, hasta la fecha solamente hemos sido testigos de ligeras mejoras, sin innovaciones drásticas. También parece que sigue habiendo un interés continuo en C++, por lo que es improbable que este lenguaje desaparezca con el tiempo. (Los lenguajes siempre merodean por ahí. En uno de los "Seminarios de Java Intermedio/Avanzado" del autor Allen Holub afirmó que los lenguajes más comúnmente utilizados son Rexx y COBOL, en ese orden.) Comienzo a pensar que la fuerza de Java reside en un ruedo ligeramente diferente al de C++. Éste es un lenguaje que no trata de encajar en un molde. Verdaderamente, se ha adaptado de distintas maneras para resolver problemas particulares. Algunas herramientas de C++ combinan bibliotecas, modelos de componente, y herramientas de generación de código para resolver el problema de desarrollar aplicaciones de ventanas para usuarios finales (para Microsoft Windows). Y sin embargo, ¿qué es lo que utilizan la gran mayoría de desarrolladores en Windows? Visual Basic de Microsoft. Y esto a pesar del hecho de que VB produce el tipo de código que se convierte en inmanejable en cuanto el programa tiene una ampliación de unas pocas páginas (además de proporcionar una sintaxis que puede ser incluso mística). VB es tan mal ejemplo de lenguaje de diseño como exitoso y popular. Por ello sería bueno disponer de la facilidad y potencia de VB sin que el resultado fuera código imposible de gestionar. Y es aquí donde Java debería destacar: como el "próxi-

m)


1: Introducción a los objetos

53

mo VB". Uno puede estremecerse al leer esto, o no, pero al menos debería pensar en ello: es tan grande la porción de Java diseñada para facilitar la tarea del programador a la hora de enfrenarse a problemas de nivel de aplicación como las redes o las interfaces de usuario multiplataforma, y además tiene un diseño de lenguaje que hace posible la creación de bloques de código flexibles y de gran tamaño. Si se añade a esto el hecho de que Java es el lenguaje con los sistemas de comprobación de tipos y manejo de errores más robustos jamás vistas en un lenguaje, se tienen las bases para dar un gran paso adelante en lo que se refiere a productividad de la programación. ¿Debería utilizarse Java en vez de C++para un proyecto determinado? En vez de applets de web, deben considerarse dos aspectos. El primero es que si se desea utilizar muchas de las bibliotecas de C++ ya existentes (logrando una considerable ganancia en productividad) o si se dispone de un código base ya existente en C o C++,Java podría ralentizar el desarrollo en vez de acelerarlo. Si se está desarrollando el código por primera vez desde la nada, la simplicidad de Java frente a C++

acortará significativamente el tiempo de desarrollo -la evidencia anecdótica (historias de equipos que desarrollan en C++y que siempre cuento a aquéllos que se pasan a Java) sugiere que se doble la velocidad de desarrollo frente a C++.Si el rendimiento de Java no importa o puede compensarse, los aspectos puramente de tiempo de lanzamiento hacen difícil justificar la elección de C++ frente a Java. El aspecto más importante es el rendimiento. El código Java interpretado siempre ha sido lento, incluso entre 20 y 50 veces más lento que C en el caso de los primeros intérpretes de Java. Este aspecto, no obstante, ha mejorado considerablemente a lo largo del tiempo, aunque sigue siendo del orden de varias veces superior. Los computadores se fundamentan en la velocidad; si hacer algo en un computador no es considerablemente más rápido, lo hacemos a mano. (Incluso se sugiere que se empiece con Java, para reducir el tiempo de desarrollo, para posteriormente utilizar una herramienta y bibliotecas de soporte que permitan traducir el código a C++,cuando se necesite una velocidad de ejecución más rápida.) La clave para hacer Java adecuado para la mayoría de proyectos de desarrollo es la aparición de mejoras en cuanto a velocidad, como los denominados compiladores just-in-time WIT), la tecnología "hotspot" de Sun, e incluso compiladores de código nativo. Por supuesto, estos últimos eliminan la ejecución multiplataforma de los programas compilados, pero también proporcionan una mejora de velocidad al ejecutable, que se acerca a la que se lograría con C y C++.Y compilar un programa multiplataforma en Java sería bastante más sencillo que hacerlo en C o C++. (En teoría, simplemente es necesario recompilar, pero esto ya se ha prometido también antes en otros lenguajes de programación). Es posible encontrar comparaciones entre Java y C++,y observaciones sobre las realidades de Java en los apéndices de la primera edición de este libro (disponible en el CD ROM que acompaña al presente texto, además de en http://www. BruceEcke1.com).

Resumen Este capítulo trata de dar un repaso a los aspectos más importantes de la programación orientada a objetos y Java, incluyendo el porqué la PO0 es diferente, y por qué Java en particular es diferente,


54

Piensa en Java

conceptos de metodologías de POO, y finalmente las situaciones que se dan al hacer que una compañía pase a PO0 y Java.

La PO0 y Java pueden no ser para todo el mundo. Es importante evaluar las propias necesidades y decidir si Java podría satisfacer completamente esas necesidades, o si no sería mejor hacer uso de otro sistema de programación (incluyendo el que se esté utilizando actualmente). Si se sabe que las necesidades serán muy especializadas en un futuro próximo y que se tienen limitaciones específicas, puede que Java no sea la solución más satisfactoria, por lo que uno debe investigar las posibles alternativasz4.Incluso si eventualmente se elige Java como lenguaje, uno debe al menos entender cuáles eran las opciones y tener una visión clara de por qué eligió dirigirse en esa dirección.

La apariencia de un lenguaje de programación procedural es conocida: definiciones de datos y llamadas a funciones. Para averiguar el significado de estos programas hay que invertir cierto tiempo, echando un vistazo a las llamadas a función y a conceptos de bajo nivel para crearse un modelo en la mente. Ésta es la razón por la que son necesarias representaciones intermedias al diseñar programas procedurales -por sí mismos, estos programas tienden a ser confusos porque los términos de expresión suelen estar más orientados hacia el computador que hacia el problema que se trata de resolver. Dado que Java añade muchos conceptos nuevos sobre lo que tienen los lenguajes procedurales, es algo natural pensar que el método main() de un programa en Java será bastante más complicado que su equivalente en un programa en C. Se verá que las definiciones de los objetos que representan conceptos en el espacio del problema (en vez de hacer uso de aspectos de representación del computador) además de los mensajes que se envían a los mismos, representan las actividades en ese mismo espacio. Una de las maravillas de la programación orientada a objetos es ésa: con un programa bien diseñado, es fácil entender el código simplemente leyéndolo. Generalmente hay también menos código, porque muchos de los problemas se resolverán reutilizando código de las bibliotecas ya existentes.

2Tecomiendo, en particular, echar un vistazo a Python (http:(//www.Python.org).


2: Todo es un objeto Aunque se basa en C++, Java es más un lenguaje orientado a objetos "puro". Tanto C++ como Java son lenguajes híbridos, pero en Java los diseñadores pensaban que esa "hibridación" no era tan importante como lo era en C++. Un lenguaje híbrido permite múltiples estilos de programación; la razón por la que C++ es híbrido es soportar la compatibilidad hacia atrás con el lenguaje C. Dado que C++ es un superconjunto del lenguaje C, incluye muchas de las características no deseables de ese lenguaje, lo que puede provocar que algunos aspectos de C++ sean demasiado complicados. El lenguaje Java asume que se desea llevar a cabo exclusivamente programación orientada a objetos. Esto significa que antes de empezar es necesario cambiar la forma de pensar hacia el mundo de la orientación a objetos (a menos que ya esté en él). El beneficio de este esfuerzo inicial es la habilidad para programar en un lenguaje que es más fácil de aprender y usar que otros muchos lenguajes de POO. En este capítulo, veremos los componentes básicos-de un programa Java y aprenderemos que todo en Java es un objeto, incluido un programa Java.

Los objetos se manipulan mediante referencias Cada lenguaje de programación tiene sus propios medios de manipular datos. Algunas veces, el programador debe ser consciente constantemente del tipo de manipulación que se está produciendo. ¿Se está manipulando directamente un objeto, o se está tratando con algún tipo de representación indirecta (un puntero en C o C++) que debe ser tratada con alguna sintaxis especial? Todo esto se simplifica en Java. Todo se trata como un objeto, de forma que hay una única sintaxis consistente que se utiliza en todas partes. Aunque se trata todo como un objeto, el identificador que se manipula es una "referencia" a un objeto1.Se podría imaginar esta escena como si se tratara de

' Esto puede suponer un tema de debate. Existe quien piensa que "claramente, e s un puntero", pero esto presupone una implementación subyacente. Además, las referencias de Java son mucho más parecidas en su sintaxis a las referencias de C++ que a punteros. En la primera edición del presente libro, el autor decidió inventar un nuevo término, "empuñadura" porque las referencias C++ y las referencias Java tienen algunas diferencias importantes. El autor provenía de C++ y no deseaba confundir a los programadores de C++ que supuestamente serían la mejor audiencia para Java. En la 2" edición, el autor decidió que el término más comúnmente usado era el término "referencia", y que cualquiera que proviniera de C++ tendría que lidiar con mucho más que con la terminología de las referencias, por lo que podrán incorporarse sin problemas. Sin embargo, hay personas que no están de acuerdo siquiera con el término "referencia". El autor leyó una vez un libro en el que "era incorrecto decir que Java soporta el paso por referencia", puesto que los identificadores de objetos en Java (en concordancia con el citado autor) son de hecho "referencias a objetos". 'Y (continúa el citado texto), todo se pasa de hecho por valor. Por tanto, si no se pasan parámetros por referencia, se está pasando una referencia a un objeto por valor". Se podría discutir la precisión de semejantes explicaciones, pero el autor considera que su enfoque simplifica el entendimiento del concepto sin herir a nadie (bueno, los abogados del lenguaje podrían decir que el autor miente, pero creo que la abstracción que se presenta es bastante apropiada).


56

Piensa en Java

una televisión (el objeto) con su mando a distancia (la referencia). A medida que se hace uso de la referencia, se está conectado a la televisión, pero cuando alguien dice "cambia de canal" o "baja el volumen", lo que se manipula es la referencia, que será la que manipule el objeto. Si desea moverse por la habitación y seguir controlando la televisión, se toma el mando a distancia (la referencia), en vez de la televisión. Además, el mando a distancia puede existir por sí mismo, aunque no haya televisión. Es decir, el mero hecho de tener una referencia no implica necesariamente la existencia de un objeto conectado al mismo. De esta forma si se desea tener una palabra o frase, se crea una referencia String: String S;

Pero esta sentencia solamente crea la referencia, y no el objeto. Si se decide enviar un mensaje a S en este momento, se obtendrá un error (en tiempo de ejecución) porque S no se encuentra, de hecho, vinculado a nada (no hay televisión). Una práctica más segura, por consiguiente, es inicializar la referencia en el mismo momento de su creación: String

S

=

"asdf';

Sin embargo, esta sentencia hace uso de una característica especial de Java: las cadenas de texto pueden inicializarse con texto entre comillas. Normalmente, es necesario usar un tipo de inicialización más general para los objetos.

Uno debe crear todos los objetos Cuando se crea una referencia, se desea conectarla con un nuevo objeto. Así se hace, en general, con la palabra clave new, que dice "Créame un objeto nuevo de ésos". Por ello, en el ejemplo anterior se puede decir: String s

=

new String

(

"asdf") ;

Esto no sólo significa "Créame un nuevo String", sino que también proporciona información sobre cómo crear el String proporcionando una cadena de caracteres inicial. Por supuesto, String no es el único tipo que existe. Java viene con una plétora de tipos predefinidos. Lo más importante es que uno puede crear sus propios tipos. De hecho, ésa es la actividad fundamental de la programación en Java, y es precisamente lo que se irá aprendiendo en este libro.

Dónde reside el almacenamiento Es útil visualizar algunos aspectos relativos a cómo se van disponiendo los elementos al ejecutar el programa, y en particular, sobre cómo se dispone la memoria. Hay seis lugares diferentes en los que almacenar información:


2: Todo es un objeto

57

1.

Registros. Son el elemento de almacenamiento más rápido porque existen en un lugar distinto al de cualquier otro almacenamiento: dentro del procesador. Sin embargo, el número de registros está severamente limitado, de forma que los registros los va asignando el compilador en función de sus necesidades. No se tiene control directo sobre ellos, y tampoco hay ninguna evidencia en los programas de que los registros siquiera existan.

2.

La pila. Reside en la memoria RAM (memoria de acceso directo) general, pero tiene soporte directo del procesador a través del puntero de pila. Éste se mueve hacia abajo para crear más memoria y de nuevo hacia arriba para liberarla. Ésta es una manera extremadamente rápida y eficiente de asignar espacio de almacenamiento, antecedido sólo por los registros. El compilador de Java debe saber, mientras está creando el programa, el tamaño exacto y la vida de todos los datos almacenados en la pila, pues debe generar el código necesario para mover el puntero hacia arriba y hacia abajo. Esta limitación pone límites a la flexibilidad de nuestros programas, de forma que mientras existe algún espacio de almacenamiento en la pila -referencias a objetos en particular- los propios objetos Java no serán ubicados en la pila. El montículo. Se trata de un espacio de memoria de propósito general (ubicado también en el área RAM) en el que residen los objetos Java. Lo mejor del montículo es que, a diferencia de la pila, el compilador no necesita conocer cuánto espacio de almacenamiento necesita asignar al montículo o durante cuánto tiempo debe permanecer ese espacio dentro del montículo. Por consiguiente, manejar este espacio de almacenamiento proporciona una gran flexibilidad. Cada vez que se desee crear un objeto, simplemente se escribe el código, se crea utilizando la palabra new, y se asigna el espacio de almacenamiento en el montículo en el momento en que este código se ejecuta. Por supuesto hay que pagar un precio a cambio de esta flexibilidad: lleva más tiempo asignar espacio de almacenamiento del montículo que lo que lleva hacerlo en la pila (es decir, si se pudieran crear objetos en la pila en Java, como se hace en C++).

4.

Almacenamiento estático. El término "estático" se utiliza aquí con el sentido de "con una ubicación/posición fija" (aunque también sea en RAM). El almacenamiento estático contiene datos que están disponibles durante todo el tiempo que se esté ejecutando un programa. Podemos usar la palabra clave static para especificar que un elemento particular de un objeto sea estático, pero los objetos en sí nunca se sitúan en el espacio de almacenamiento estático.

5.

Almacenamiento constante. Los valores constantes se suelen ubicar directamente en el código del programa, que es seguro, dado que estos valores no pueden cambiar. En ocasiones, las constantes suelen ser acordonadas por sí mismas, de forma que puedan ser opcionalmente ubicadas en memoria de sólo lectura (ROM).

6.

Almacenamiento no-RAM. Si los datos residen completamente fuera de un programa, pueden existir mientras el programa no se esté ejecutando, fuera del control de dicho programa. Los dos ejemplos principales de esto son los objetos de flujo de datos (strearn), que se convierten en flujos o corrientes de bytes, generalmente para ser enviados a otra máquina, y los objetos persistentes, que son ubicados en el disco para que mantengan su estado incluso cuando el programa ha terminado. El truco con estos tipos de almacenamiento es convertir los objetos en algo que pueda existir en otro medio, y que pueda así recuperarse en forma de objeto basado en RAM cuando sea necesario. Java proporciona soporte para persistencia ligera, y


58

Piensa en Java

las versiones futuras de Java podrían proporcionar soluciones aún más complejas para la persistencia.

Un caso especial: los tipos primitivos Hay un grupo de tipos que tiene un tratamiento especial: se trata de los tipos "primitivos", que se usarán frecuentemente en los programas. La razón para el tratamiento especial es que crear un objeto con new -especialmente variables pequeñas y simples- no es eficiente porque new coloca el objeto en el montículo. Para estos tipos, Java vuelve al enfoque de C y C++. Es decir, en vez de crear la variable utilizando new, se crea una variable "automática" que no es una referencia. La variable guarda el valor, y se coloca en la pila para que sea más eficiente. Java determina el tamaño de cada tipo primitivo. Estos tamaños no varían de una plataforma a otra como ocurre en la mayoría de los lenguajes. La invariabilidad de tamaño es una de las razones por las que Java es tan llevadero.

Tipo primitivo

Tamaño

Mínimo

Máximo

Tipo de envoltura

boolean

-

-

-

Boolean

char

16 bits

Unicode O

Unicode 2'"l

Character

byte

8 bits

-12s

+127

Byte

short

-21s

16 bits 1

1

+215-1 1

Short 1

int

32 bits

-231

+231-1

Integer

long

64 bits

-263

+26:-1

hng

float

32 bits

IEEE754

IEEE754

Float

double

64 bits

IEEE754

IEEE754

Double

void

-

-

-

Void

Todos los tipos numéricos tienen signo, de forma que es inútil tratar de utilizar tipos sin signo. El tamaño del tipo boolean no está explícitamente definido; sólo se especifica que debe ser capaz de tomar los valores true o false. Los tipos de datos primitivos también tienen clases "envoltura". Esto quiere decir que si se desea hacer un objeto no primitivo en el montículo para representar ese tipo primitivo, se hace uso del envoltorio asociado. Por ejemplo: char c = ' x ' ; Character C = new Character (c);


2: Todo es un objeto

59

O también se podría utilizar: Character C

=

new ~haracter( ' x' ) ;

Las razones para hacer esto se mostrarán más adelante en este capítulo.

Números de a l t a precisión Java incluye dos clases para llevar a cabo aritmética de alta precisión: BigInteger y BigDecimal. Aunque estos tipos vienen a encajar en la misma categoría que las clases "envoltorio", ninguna de ellas tiene un tipo primitivo. Ambas clases tienen métodos que proporcionan operaciones análogas que se lleven a cabo con tipos primitivos. Es decir, uno puede hacer con BigInteger y BigDecimal cualquier cosa que pueda hacer con un int o un float, simplemente utilizando llamadas a métodos en vez de operadores. Además, las operaciones serán más lentas dado que hay más elementos involucrados. Se sacrifica la velocidad en favor de la exactitud.

BigInteger soporta enteros de precisión arbitraria. Esto significa que uno puede representar valores enteros exactos de cualquier tamaño y sin perder información en las distintas operaciones. BigDecimal es para números de coma flotante de precisión arbitraria; pueden usarse, por ejemplo, para cálculos monetarios exactos. Para conocer los detalles de los constructores y métodos que pueden invocarse para estas dos clases, puede recurrirse a la documentación existente en línea.

Arrays en Java Virtualmente, todos los lenguajes de programación soportan arrays. Utilizar arrays en C y C++ es peligroso porque los arrays no son sino bloques de memoria. Si un programa accede al array fuera del rango de su bloque de memoria o hace uso de la memoria antes de la inicialización (errores de programación bastante frecuentes) los resultados pueden ser impredecibles. Una de los principales objetos de Java es la seguridad, de forma que muchos de los problemas habituales en los programadores de C y C++ no se repiten en Java. Está garantizado que un array en Java estará siempre inicializado, y que no se podrá acceder más allá de su rango. La comprobación de rangos se resuelve con una pequeña sobrecarga de memoria en cada array, además de verificar el índice en tiempo de ejecución, pero se asume que la seguridad y el incremento de productividad logrados merecen este coste. Cuando se crea un array de objetos, se está creando realmente un array de referencias a los objetos, y cada una de éstas se inicializa automáticamente con un valor especial representado por la palabra clave null. Cuando Java ve un null, reconoce que la referencia en cuestión no está señalando ningún objeto. Debe asignarse un objeto a cada referencia antes de utilizarla, y si se intenta hacer uso de una referencia que aún vale null, se informará de que se ha dado un problema en tiempo de ejecución. Por consiguiente, en Java se evitan los errores típicos de los arrays.


60

Piensa en Java

Uno también puede crear un array de tipos primitivos. De nuevo, es el compilador el que garantiza la inicialización al poner a cero la memoria que ocupará ese array. Se hablará del resto de arrays más detalladamente en capítulos posteriores.

Nunca es necesario destruir u n o b j e t o En la mayoría de los lenguajes de programación, el concepto de tiempo de vida de una variable ocupa una parte importante del esfuerzo de programación. ¿Cuánto dura una variable? Si se supone que uno va a destruirla, ¿cuándo debe hacerse? La confusión relativa a la vida de las variables puede conducir a un montón de fallos, y esta sección muestra cómo Java simplifica enormemente esto al hacer el trabajo de limpieza por ti.

Ámbito La mayoría de lenguajes procedurales tienen el concepto de alcance o ámbito.Éste determina tanto la visibilidad como la vida de los nombres definidos dentro de ese ámbito. En C, C++y Java, el ámbito se determina por la ubicación de llaves O. Así, por ejemplo: I int x = 12; / * sólo x disponible * / t int q = 96; / * tanto x como q están disponibles * /

/ * sólo x disponible * / / * q está "fuera del ámbito o alcance" * /

Una variable definida dentro de un ámbito solamente está disponible hasta que finalice su ámbito. Las tabulaciones hacen que el código Java sea más fácil de leer. Dado que Java es un lenguaje de formato libre, los espacios extra, tabuladores y retornos de carro no afectan al programa resultante. Fíjese que uno no puede hacer lo siguiente, incluso aunque sea legal en C y C++: int x t

=

12;

int x

1

=

96; / * ilegal * /


2: Todo es un objeto

61

El compilador comunicará que la variable x ya ha sido definida. Por consiguiente, la capacidad de C y C++ para "esconder" una variable de un ámbito mayor no está permitida, ya que los diseñadores de Java pensaron que conducía a programas confusos.

Ámbito de los objetos Los objetos en Java no tienen la misma vida que los tipos primitivos. Cuando se crea un objeto Java haciendo uso de new, éste perdura hasta el final del ámbito. Por consiguiente, si se escribe:

I String s

1

=

new String ("un

string"

) ;

/ * Fin del ámbito * /

la referencia S desaparece al final del ámbito. Sin embargo, el objeto String al que apunta S sigue ocupando memoria. En este fragmento de código, no hay forma de acceder al objeto, pues la única referencia al mismo se encuentra fuera del ámbito. En capítulos posteriores se verá cómo puede pasarse la referencia al objeto, y duplicarla durante el curso de un programa. Resulta que, dado que los objetos creados con new se mantienen durante tanto tiempo como se desee, en Java desaparecen un montón de posibles problemas propios de C++. Los problemas mayores parecen darse en C++puesto que uno no recibe ningún tipo de ayuda del lenguaje para asegurarse de que los objetos estén disponibles cuando sean necesarios. Y lo que es aún más importante, en C++uno debe asegurarse de destruir los objetos cuando se ha acabado con ellos. Esto nos conduce a una cuestión interesante. Si Java deja los objetos vivos por ahí, ¿qué evita que se llene la memoria provocando que se detenga la ejecución del programa? Éste es exactamente el tipo de problema que ocurriría en C++.Es en este punto en el que ocurren un montón de cosas "mágicas". Java tiene un recolector de basura, que recorre todos los objetos que fueron creados con new y averigua cuáles no serán referenciados más. Posteriormente, libera la memoria de los que han dejado de ser referenciados, de forma que la memoria pueda ser utilizada por otros objetos. Esto quiere decir que no es necesario que uno se preocupe de reivindicar ninguna memoria. Simplemente se crean objetos, y cuando posteriormente dejan de ser necesarios, desaparecen por sí mismos. Esto elimina cierta clase de problemas de programación: el denominado "agujero de memoria", que se da cuando a un programador se le olvida liberar memoria.

Crear nuevos tipos d e datos: clases Si todo es un objeto, ¿qué determina qué apariencia tiene y cómo se comporta cada clase de objetos? O dicho de otra forma, ¿qué establece el tipo de un objeto? Uno podría esperar que haya una palabra clave "type", lo cual ciertamente hubiera tenido sentido. Sin embargo, históricamente, la mayoría de lenguajes orientados a objetos han hecho uso de la palabra clave class para indicar Voy a decirte qué apariencia tiene un nuevo tipo de objeto". La palabra clave class (que se utilizará tanto


62

Piensa en Java

que no se pondrá en negrita a lo largo del presente libro) siempre va seguida del nombre del nuevo tipo. Por ejemplo: class UnNombreDeTipo

{

/ * Aquí va el cuerpo de la clase * /

}

Esto introduce un nuevo tipo, de forma que ahora es posible crear un objeto de este tipo haciendo uso de la palabra clave new: UnNombreDeTipo u

=

new UnNombreDeTipo

();

En UnNombreDeTipo, el cuerpo de la clase sólo consiste en un comentario (los asteriscos, las barras inclinadas y lo que hay dentro, que se discutirán más adelante en este capítulo), con lo que no hay demasiado que hacer con él. De hecho, uno no puede indicar que se haga mucho de nada (es decir, no se le puede mandar ningún mensaje interesante) hasta que se definan métodos para ella.

Campos y métodos Cuando se define una clase @ todo lo que se hace en Java es definir clases, se hacen objetos de esas clases y se envían mensajes a esos objetos), es posible poner dos tipos de elementos en la nueva clase: datos miembros (denominados generalmente campos), y funciones miembros (típicamente llamados métodos). Un dato miembro es un objeto de cualquier tipo con el que te puedes comunicar a través de su referencia. También puede ser algún tipo primitivo (que no sea una referencia). Si es una referencia a un objeto, hay que inicializar esa referencia para conectarla a algún objeto real (utilizando new, como se ha visto antes) en una función especial denominada constructor (descrita completamente en el Capítulo 4). Si se trata de un tipo primitivo es posible inicializarla directamente en el momento de definir la clase (como se verá después, también es posible inicializar las referencias en este punto de la definición). Cada objeto mantiene el espacio de almacenamiento necesario para todos sus datos miembro; éstos no son compartido con otros objetos. He aquí un ejemplo de una clase y algunos de sus datos miembros: class SoloDatos t int i; float f; boolean b;

1 Esta clase no hace nada, pero es posible crear un objeto: SoloDatos

S

=

new SoloDatos();

Es posible asignar valores a los datos miembros, pero primero es necesario saber cómo hacer referencia a un miembro de un objeto. Esto se logra escribiendo el nombre de la referencia al objeto, seguido de un punto, y a continuación el nombre del miembro del objeto:


2 : Todo es un objeto

63

Por ejemplo: s.i = 47; s.f. = l.lf; f .b = false;

También es posible que un objeto pueda contener otros datos que se quieran modificar. Para ello, hay que seguir "conectando los puntos". Por ejemplo:

La clase SoloDrrtos no puede hacer nada que no sea guardar datos porque no tiene funciones miembro (métodos). Para entender cómo funcionan los métodos, es necesario entender los parámetros y valores de retorno, que se describirán en breve.

Valores por defecto para los miembros primitivos Cuando un tipo de datos primitivo es un miembro de una clase, se garantiza que tenga un valor por defecto siempre que no se inicialice:

1

1

Valor por defecto

1

1 boolean

1

false

1

1 char

1

~u0000~(null)

1

1 short

1

(short)O

1

1

O.Od

I

Tipo primitivo

1 int

1 double

Debe destacarse que los valores por defecto son los que Java garantiza cuando se usa la variable como miembro de una clase. Esto asegura que las variables miembro de tipos primitivos siempre serán inicializadas (algo que no ocurre en C++), reduciendo una fuente de errores. Sin embargo, este valor inicial puede no ser correcto o incluso legal dentro del programa concreto en el que se esté trabajando. Es mejor inicializar siempre todas las variables explícitamente. Esta garantía no se aplica a las variables "locales" -aquellas que no sean campos de clases. Por consiguiente, si dentro de una definición de función se tiene:

1

i n t x;


64

Piensa en Java

Entonces x tomará algún valor arbitrario (como en C y C++);no se inicializará automáticamente a cero. Cada uno es responsable de asignar un valor apropiado a la variable x antes de usarla. Si uno se olvida, Java seguro que será mejor que C++: se recibirá un error en tiempo de compilación indicando que la variable debería haber sido inicializada. (Muchos compiladores de C++ advertirán sobre variables sin inicializar, pero en Java éstos se presentarán como errores.)

Métodos, parámetros y valores de retorno Hasta ahora, el término fünción se ha utilizado para describir una subrutina con nombre. El término que se ha usado más frecuentemente en Java es método, al ser "una manera de hacer algo". Si se desea, es posible seguir pensando en funciones. Verdaderamente sólo hay una diferencia sintáctica, pero de ahora en adelante se usará el término "método" en lugar del término "función". Los métodos en Java determinan los mensajes que puede recibir un objeto. En esta sección se aprenderá lo simple que es definir un método. Las partes fundamentales de un método son su nombre, sus parámetros, el tipo de retorno y el cuerpo. He aquí su forma básica: tipoRetorno nombreMetodo

(

/ * lista de parámetros * /

)

{

/ * Cuerpo del método * /

1

El tipo de retorno es el tipo del valor que surge del método tras ser invocado. La lista de parámetros indica los tipos y nombres de las informaciones que es necesario pasar a ese método. Cada método se identifica unívocamente mediante el nombre del método y la lista de parámetros. En Java los métodos pueden crearse como parte de una clase. Es posible que un método pueda ser invocado sólo por un objeto2,y ese objeto debe ser capaz de llevar a cabo esa llamada al método. Si se invoca erróneamente a un método de un objeto, se generará un error en tiempo de compilación. Se invoca a un método de un objeto escribiendo el nombre del objeto seguido de un punto y el nombre del método con su lista de argumentos, como: nombreObjeto.nombreMetodo(argl, arg2, arg3). Por ejemplo, si se tiene un método f( ) que no recibe ningún parámetro y devuelve un dato de tipo int, y si se tiene un objeto a para el que puede invocarse a f( ), es posible escribir: int x

=

a.f();

El tipo del valor de retorno debe ser compatible con el tipo de x.

LOS métodos static, que se verán más adelante, pueden ser invocados por la clase, sin necesidad de un objeto.


2: Todo es un objeto

65

Este acto de invocar a un método suele denominarse envío de u n mensaje a u n objeto. En el ejemplo de arriba, el mensaje es f( ) y el objeto es a. La programación orientada a objetos suele resumirse como un simple "envío de mensajes a objetos".

La lista d e parametros La lista de parámetros de un método especifica la información que se le pasa. Como puede adivinarse, esta información -como todo lo demás en Java- tiene forma de objetos. Por tanto, lo que hay que especificar en la lista de parámetros son los tipos de objetos a pasar y el nombre a utilizar en cada uno. Como en cualquier situación en Java en la que parece que se estén manipulando directamente objetos, se están pasando referencias". El tipo de referencia, sin embargo, tiene que ser correcto. Si se supone, por ejemplo, que un parámetro debe ser un String, lo que se le pase debe ser una cadena de caracteres. Consideremos un método que reciba como parámetro un String, cuya definición, que debe ser ubicada dentro de la definición de la clase para que sea compilada, puede ser la siguiente: int almacenamiento (String S) return s.length ) * 2; 1

{

Este método dice cuántos bytes son necesarios para almacenar la información de un String en particular (cada carácter de una cadena tiene 16 bits, o 2 bytes para soportar caracteres Unicode). El parámetro S es de tipo String. Una vez que se pasa S al método, es posible tratarlo como a cualquier otro objeto (se le pueden enviar mensajes). Aquí se invoca al método length( ), que es uno de los métodos para String; devuelve el número de caracteres que tiene la cadena. También es posible ver el uso de la palabra clave return, que hace dos cosas. Primero, quiere decir, "abandona el método, que ya hemos acabado". En segundo lugar, si el método produce un valor, ese valor se ubica justo después de la sentencia return. En este caso, el valor de retorno se produce al evaluar la expresión s.length( )*2. Se puede devolver el tipo que se desee, pero si no se desea devolver nada, hay que indicar que el método devuelve void. He aquí algunos ejemplos: boolean indicador ( ) { return true; } float naturalLogBase ( ) { return 2.718f; void nada ( ) { return; } void nada2 ( ) { }

]

Cuando el tipo de retorno es void, se utiliza la palabra clave return sólo para salir del método, y es, por consiguiente, innecesaria cuando se llega al final del mismo. Es posible salir de un método en cualquier punto, pero si se te da un valor de retorno distinto de void, el compilador te obligará (meCon la excepción habitual de los ya mencionados tipos de datos "especiales" boolean, char, byte, short. int, long, float y double. Normalmente se pasan objetos, lo cual verdaderamente quiere decir que se pasan referencias a objetos.


66

Piensa en Java

diante mensajes de error) a devolver el tipo apropiado de datos independientemente de lo que devuelvas. En este punto, puede parecer que un programa no es más que un montón de objetos con métodos que toman otros objetos como parámetros y envían mensajes a esos otros objetos. Esto es, sin duda, mucho de lo que está ocurriendo, pero en el capítulo siguiente se verá cómo hacer el trabajo de bajo nivel detallado, tomando decisiones dentro de un método. Para este capítulo, será suficiente con el envío de mensajes.

Construcción programa Java Hay bastantes aspectos que se deben comprender antes de ver el primer programa Java.

Visibilidad de los nombres Un problema de los lenguajes de programación es el control de nombres. Si se utiliza un nombre en un módulo del programa, y otro programador utiliza el mismo nombre en otro módulo ¿cómo se distingue un nombre del otro para evitar que ambos nombres "colisionen"? En C éste es un problema particular puesto que un programa es un mar de nombres inmanejable. En las clases de C t t (en las que se basan las clases de Java) anidan funciones dentro de las clases, de manera que no pueden colisionar con nombres de funciones anidadas dentro de otras clases. Sin embargo, C++ sigue permitiendo los datos y funciones globales, por lo que las colisiones siguen siendo posibles. Para solucionar este problema, C++ introdujo los espacios de nombres utilizando palabras clave adicionales. Java pudo evitar todo esto siguiendo un nuevo enfoque. Para producir un nombre no ambiguo para una biblioteca, el identificador utilizado no difiere mucho de un nombre de dominio en Internet. De hecho, los creadores de Java utilizaron los nombres de dominio de Internet a la inversa, dado que es posible garantizar que éstos sean únicos. Dado que mi nombre de dominio es BruceEckel.com, mi biblioteca de utilidad de manías debería llamarse com.bruceEckel.utilidad.manias. Una vez que se da la vuelta al nombre de dominio, los nombres supuestamente representan subdirectorios. En Java 1.0 y 1.1, las extensiones de dominio com, edu, org, net, etc. se ponían en mayúsculas por convención, de forma que la biblioteca aparecería como COM.bruceEckel.utilidad.manias. Sin embargo, a mitad de camino del desarrollo de Java 2, se descubrió que esto causaba problemas, por lo que de ahora en adelante se utilizarán minúsculas para todas las letras de los nombres de paquetes. Este mecanismo hace posible que todos sus ficheros residan automáticamente en sus propios espacios de nombres, y cada clase de un fichero debe tener un identificador único. Por tanto, uno no necesita aprender ninguna característica especial del lenguaje para resolver el problema -el lenguaje lo hace por nosotros.


2: Todo es un objeto

67

Utilización de otros componentes Cada vez que se desee usar una clase predefinida en un programa, el compilador debe saber dónde localizarla. Por supuesto, la clase podría existir ya en el mismo fichero de código fuente que la está invocando. En ese caso, se puede usar simplemente la clase -incluso si la clase no se define hasta más adelante dentro del archivo. Java elimina el problema de las "referencias hacia delante" de forma que no hay que pensar en ellos. ¿Qué hay de las clases que ya existen en cualquier otro archivo? Uno podría pensar que el compilador debería ser lo suficientemente inteligente como para localizarlo por sí mismo, pero hay un problema. Imagínese que se quiere usar una clase de un nombre determinado, pero existe más de una definición de esa clase presumiblemente se trata de definiciones distintas). O peor, imagine que se está escribiendo un programa, y a medida que se está construyendo se añade una nueva clase a la biblioteca cuyo nombre choca con el de alguna clase ya existente. Para resolver este problema, debe eliminarse cualquier ambigüedad potencial. Esto se logra diciéndole al compilador de Java exactamente qué clases se quieren utilizar mediante la palabra clave import. Esta palabra clave dice al compilador que traiga un paquete, que es una biblioteca de clases (en otros lenguajes, una biblioteca podría consistir en funciones y datos además de clases, pero debe recordarse que en Java todo código debe escribirse dentro de una clase).

La mayoría de las veces se utilizarán componentes de las bibliotecas de Java estándar que vienen con el propio compilador. Con ellas, no hay que preocuparse de los nombres de dominio largos y dados la vuelta; uno simplemente dice, por ejemplo: import java.util.ArrayList;

para indicar al compilador que se desea utilizar la clase ArrayList de Java. Sin embargo, util contiene bastantes clases y uno podría querer utilizar varias de ellas sin tener que declararlas todas explícitamente. Esto se logra sencillamente utilizando el '*' que hace las veces de comodín: import java.util.*;

Es más común importar una colección de clases de esta forma que importar las clases individualmente.

La palabra clave s t a t i c Generalmente, al crear una clase se está describiendo qué apariencia tienen sus objetos y cómo se comportan. No se tiene nada hasta crear un objeto de esa clase con new, momento en el que se crea el espacio de almacenamiento y los métodos pasan a estar disponibles. Pero hay dos situaciones en las que este enfoque no es suficiente. Una es cuando se desea tener solamente un fragmento de espacio de almacenamiento para una parte concreta de datos, independientemente de cuántos objetos se creen, o incluso aunque no se cree ninguno. La otra es si se necesita un método que no esté asociado con ningún objeto particular de esa clase. Es decir, se necesita un método al que se pueda invocar incluso si no se ha creado ningún objeto. Ambos efec-


68

Piensa en Java

tos se pueden lograr con la palabra clave estático. Al decir que algo es estático se está indicando que el dato o método no está atado a ninguna instancia de objeto de esa clase en particular. Por ello, incluso si nunca se creó un objeto de esa clase se puede invocar a un método estático o acceder a un fragmento de datos estático. Con los métodos y datos ordinarios no estático, es necesario crear un objeto y utilizarlo para acceder al dato o método, dado que los datos y métodos no estático deben conocer el objeto particular con el que está trabajando. Por supuesto, dado que los métodos estático no precisan de la creación de ningún objeto, no pueden acceder directamente a miembros o métodos no estático simplemente invocando a esos otros miembros sin referirse a un objeto con nombre (dado que los miembros y objetos no estático deber, estar unidos a un objeto en particular). Algunos lenguajes orientados a objetos utilizan los términos datos a nivel de clase y métodos a nivel de clase, para indicar que los datos y métodos solamente existen para la clase, y no para un objeto particular de la clase. En ocasiones, estos términos también se usan en los textos. Para declarar un dato o un miembro a nivel de clase estático, basta con colocar la palabra clave estático antes de la definición. Por ejemplo, el siguiente fragmento produce un miembro de datos estáticos y lo inicializa: class PruebaEstatica { static int i = 47;

Ahora, incluso si se construyen dos objetos de Tipo PruebaEstatica, sólo habrá un espacio de almacenamiento para PruebaEstatica.i. Ambos objetos compartirán la misma i. Considérese: PruebaEstatica stl PruebaEstatica st2

= =

new PruebaEstatica(); new PruebaEstaticaO;

En este momento, tanto st1.i como st2.i tienen el valor 47, puesto que se refieren al mismo espacio de memoria. Hay dos maneras de referirse a una variable estática. Como se indicó más arriba, es posible nombrarlas a través de un objeto, diciendo, por ejemplo, st2.i. También es posible referirse a ella directamente a través de su nombre de clase, algo que no se puede hacer con miembros no estáticos (ésta es la manera preferida de referirse a una variable estática puesto que pone especial énfasis en la naturaleza estática de esa variable).

El operador ++ incrementa la variable. En este momento, tanto st1.i como st2.i valdrán 48. Algo similar se aplica a los métodos estáticos. Es posible hacer referencia a ellos, bien a través de un objeto especificado al igual, que ocurre con cualquier método, o bien con la sintaxis adicional NombreClase.método( ). Un método estático se define de manera semejante: class FunEstatico { static void incr ( )

1

{

PruebaEstatica. i++;

}


2: Todo es un objeto

69

Puede observarse que el método incr( ) de FunEstatico incrementa la variable estática i. Se puede invocar a incr( ) de la manea típica, a través de un objeto: FunEstatico sf sf.incr ( ) ;

=

new FunEstatico ( )

;

O, dado que incr() es un método estático, es posible invocarlo directamente a través de la clase:

1

FunEstatico. incr ( )

;

Mientras que static al ser aplicado a un miembro de datos, cambia definitivamente la manera de crear los datos (uno por cada clase en vez de uno por cada objeto no estático), al aplicarse a un método, su efecto no es tan drástico. Un uso importante de estático para los métodos es permitir invocar a un método sin tener que crear un objeto. Esto, como se verá, es esencial en la definición del método main( ), que es el punto de entrada para la ejecución de la aplicación. Como cualquier método, un método estático puede crear o utilizar objetos con nombre de su propio tipo, de forma que un método estátjco se usa a menudo como un "pastor de ovejas" para un conjunto de instancias de su mismo tipo.

Tu primer programa Java Finalmente, he aquí el programa4. Empieza imprimiendo una cadena de caracteres y posteriormente la fecha, haciendo uso de la clase Date, contenida en la biblioteca estándar de Java. Hay que tener en cuenta que se introduce un estilo de comentarios adicional: el '//', que permite insertar un comentario hasta el final de la línea: / / HolaFecha . java import java.uti1. *; public class HolaFecha { public static void main(String[] args) { System.out.println ("Hola, hoy es: System. out .println (new Date ( ) ) ;

");

i 1 Al principio de cada fichero de programa es necesario poner la sentencia import para incluir cualquier clase adicional que se necesite para el código contenido en ese fichero. Nótese que digo "adi'Algunos entornos de programación irán sacando programas en la pantalla, y luego los cerrarán antes de que uno tenga siquiera o p ción a ver los resultados. Para detener la salida, se puede escribir el siguiente fragmento de código al final de la función main ( ); try I System. in.read ( ) )

;

catch(Exception e) ( 1

Esto hará que la salida se detenga hasta presionar "Intro" (o cualquier otra tecla). Este código implica algunos conceptos que no se verán hasta mucho más adelante, por lo que todavía no lo podemos entender, aunque el truco es válido igualmente.


70

Piensa en Java

cional"; se debe a que hay una cierta biblioteca de clases que se carga automáticamente en todos los ficheros Java: la java.lang. Arranque su navegador web y eche un vistazo a la documentación de Sun (si no la ha bajado de java.sun.com o no ha instalado la documentación de algún otro modo, será mejor hacerlo ahora). Si se echa un vistazo a la lista de paquetes, se verán todas las bibliotecas de clases que incluye Java. Si se selecciona java.lang aparecerá una lista de todas las clases que forman parte de esa biblioteca. Dado que java.lang está incluida implícitamente en todos los archivos de código Java, todas estas clases ya estarán disponibles. En java.lang no hay ninguna clase Date, lo que significa que será necesario importarla de alguna otra biblioteca. Si se desconoce en qué biblioteca en particular está una clase, o si se quieren ver todas las clases, es posible seleccionar "Tree" en la documentación de Java. En ese momento es posible encontrar todas y cada una de las clases que vienen con Java. Después, es posible hacer uso de la función "buscar" del navegador para encontrar Date. Al hacerlo, se verá que está listada como java.util.Date, lo que quiere decir que se encuentra en la biblioteca util, y que es necesario importar java.util.* para poder usar Date. Si se vuelve al principio, se selecciona java.lang y después System, se verá que la clase System tiene varios campos, y si se selecciona out, se descubrirá que es un objeto estático PrintStream. Dado que es estático, no es necesario crear ningún objeto. El objeto out siempre está ahí y se puede usar directamente. Lo que se hace con el objeto out está determinado por su tipo: PrintStream. La descripción de este objeto se muestra, convenientemente, a través de un hipervínculo, por lo que si se hace clic en él se verá una lista de todos los métodos de PrintStream a los que se puede invocar. Hay unos cuantos, y se irán viendo según avancemos en la lectura del libro. Por ahora, todo lo que nos interesa es println( ), que significa "escribe lo que te estoy dando y finaliza con un retorno de carro". Por consiguiente, en cualquier programa Java que uno escriba se puede decir System.out.println("cosas") cuando se desee para escribir algo en la consola. El nombre de la clase es el mismo que el nombre del archivo. Cuando se está creando un programa independiente como éste, una de las clases del archivo tiene que tener el mismo nombre que el archivo. (El compilador se queja si no se hace así.) Esa clase debe contener un método llamado main( ), de la forma:

1

public static void rnain (String[] args)

(

La palabra clave public quiere decir que el método estará disponible para todo el mundo (como se describe en el Capítulo 5). El parámetro del método main( ) es un array de objetos String. Este programa no usará args, pero el compilador Java obliga a que esté presente, pues son los que mantienen los parámetros que se invoquen en la línea de comandos. La línea que muestra la fecha es bastante interesante: System.out .println (new Date ( )

) ;

Considérese su argumento: se está creando un objeto Date simplemente para enviar su valor a println. Tan pronto como haya acabado esta sentencia, ese Date deja de ser necesario, y en cualquier momento aparecerá el recolector de basura y se lo llevará. Uno no tiene por qué preocuparse de limpiarlo.


2: Todo es un objeto

71

Compilación y ejecución Para compilar y ejecutar este programa, y todos los demás programas de este libro, es necesario disponer, en primer lugar, de un entorno de programación Java. Hay bastantes entornos de desarrollo de terceros, pero en este libro asumiremos que se está usando el JDK de Sun, que es gratuito. Si se está utilizando otro sistema de desarrollo, será necesario echar un vistazo a la documentación de ese sistema para determinar cómo se compilan y ejecutan los programas. Conéctese a Internet y acceda a jaua.sun.com. Ahí encontrará información y enlaces que muestran cómo descargar e instalar el JDK para cada plataforma en particular. Una vez que se ha instalado el JDK, y una vez que se ha establecido la información de path en el computador, para que pueda encontrar javac y java, se puede descargar e instalar el código fuente de este libro (que se encuentra en el CD ROM que viene con el libro, o en http.//www.BruceEckel.com).Al hacerlo, se creará un subdirectorio para cada capítulo del libro. Al ir al subdirectorio c 0 2 y escribir:

1

javac HolaFecha . java

no se obtendrá ninguna respuesta. Si se obtiene algún mensaje de error, se debe a que no se ha instalado el JDK correctamente, por lo que será necesario ir investigando los problemas que se muestren. Por otro lado, si simplemente ha vuelto a aparecer el prompt del intérprete de comandos, basta con teclear:

1

java HolaFecha

y se obtendrá como salida el mensaje y la fecha.

Éste es el proceso a seguir para compilar y ejecutar cada uno de los programas de este libro. Sin embargo, se verá que el código fuente de este libro también tiene un archivo denominado makefile en cada capítulo, que contiene comandos "make" para construir automáticamente los archivos de ese capítulo. Puede verse la página web del libro en http://www.BruceEcke1.com para ver los detalles de uso de los makefiles.

Comentarios y documentación empotrada Hay dos tipos de comentarios en Java. El primero es el estilo de comentarios tradicional de C, que fue heredado por C++. Estos comentarios comienzan por /* y pueden extenderse incluso a lo largo de varias líneas hasta encontrar */. Téngase en cuenta que muchos programadores comienzan cada línea de un comentario continuo por el signo *, por lo que a menudo se vera /*

*

* */

Esto es un comentario que se extiende a lo largo de varias líneas


72

Piensa en Java

Hay que recordar, sin embargo, que todo lo que esté entre /* y */ se ignora, por lo que no hay ninguna diferencia con decir: / * Éste es un comentario que se extiende a lo largo de varias líneas * /

La segunda forma de hacer comentarios viene de C++. Se trata del comentario en una sola línea que comienza por // y continúa hasta el final de la línea. Este tipo de comentario es muy conveniente y se utiliza muy frecuentemente debido a su facilidad de uso. Uno no tiene que buscar por el teclado donde está el / y el * (basta con pulsar dos veces la misma tecla), y no es necesario cerrar el comentario, por lo que a menudo se verá: / / esto es un comentario en una sola línea

Documentación en forma de comentarios Una de las partes más interesantes del lenguaje Java es que los diseñadores no sólo tuvieron en cuenta que la escritura de código era la única actividad importante -sino que también pensaron en la documentación del código. Probablemente el mayor problema a la hora de documentar el código es el mantenimiento de esa documentación. Si la documentación y el código están separados, cambiar la documentación cada vez que se cambia el código se convierte en un problema. La solución parece bastante simple: unir el código a la documentación. La forma más fácil de hacer esto es poner todo en el mismo archivo. Para completar la estampa, sin embargo, es necesaria alguna sintaxis especial de comentarios para marcarlos como documentación especial, y una herramienta para extraer esos comentarios y ponerlos en la forma adecuada.

La herramienta para extraer los comentarios se denomina javadoc. Utiliza parte de la tecnología del compilador de Java para buscar etiquetas de comentario especiales que uno incluye en sus programas. No sólo extrae la información marcada por esas etiquetas, sino que también extrae el nombre de la clase o del método al que se adjunta el comentario. De esta manera es posible invertir la mínima cantidad de trabajo para generar una decente documentación para los programas.

La salida de javadoc es un archivo HTML que puede visualizarse a través del navegador Web. Esta herramienta permite la creación y mantenimiento de un único archivo fuente y genera automáticamente documentación útil. Gracias a javadoc se tiene incluso un estándar para la creación de documentación, tan sencillo que se puede incluso esperar o solicitar documentación con todas las bibliotecas Java.

Sintaxis Todos los comandos de javadoc se dan únicamente en comentarios /**. Estos comentarios acaban, como siempre, con */. Hay dos formas principales de usar javadoc: empotrar HTML, o utilizar "etiquetas doc". Las etiquetas doc son comandos que comienzan por '@' y se sitúan al principio de una línea de comentarios (en la que se ignora un posible primer '*'). Hay tres "tipos" de documentación en forma de comentarios, que se corresponden con el elemento al que precede el comentario: una clase, una variable o un método. Es decir, el comentario relativo


2: Todo es un objeto

73

a una clase aparece justo antes de la definición de la misma; el comentario relativo a una variable precede siempre a la definición de la variable, y un comentario de un método aparece inmediatamente antes de la definición de un método. Un simple ejemplo: / * * Un comentario de clase * / public class PruebaDoc { / * * Un comentario de una variable * / public int i; / * * Un comentario de un método * / public void f ( ) { }

1 Nótese que javadoc procesará la documentación en forma de comentarios sólo de miembros public y protected. Los comentarios para miembros private y "friendly" (véase Capítulo 5) se ignoran, no

mostrándose ninguna salida (sin embargo es posible usar el modificador -private para incluir los miembros privados). Esto tiene sentido, dado que sólo los miembros públicos y protegidos son visibles fuera del objeto, que será lo que constituya la perspectiva del programador cliente. Sin embargo, la salida incluirá todos los comentarios de la clase. La salida del código anterior es un archivo HTML que tiene el mismo formato estándar que toda la documentación Java, de forma que los usuarios se sientan cómodos con el formato y puedan navegar de manera sencilla a través de sus clases. Merece la pena introducir estos códigos, pasarlos a través de javadoc y observar el fichero HTML resultante para ver los resultados.

HTML empotrado Javadoc pasa comandos HTML al documento HTML generado. Esto permite un uso total de HTML; sin embargo, el motivo principal es permitir dar formato al código, como: /**

* <pre> * System.out .println (new Date ( ) ) ; * </pre> */ También puede usarse HTML como se haría en cualquier otro documento web para dar formato al propio texto de las descripciones: / **

* Uno puede <em>incluso</em> insertar una lista: * * * * *

<01> <li> Elemento uno <li> Elemento dos <li> Elemento tres </ol>


74

Piensa en Java

Nótese que dentro de los comentarios de documentación, los asteriscos que aparezcan al principio de las líneas serán desechados por javadoc, junto con los espacios adicionales a éstos. Javadoc vuelve a dar formato a todo adaptándolo a la apariencia estándar de la documentación. No deben utilizarse encabezados como <hl>o < h n como HTML empotrado porque javadoc inserta sus propios encabezados y éstos interferirían con ellos. Todos los tipos de documentación en comentarios -de HTML empotrado.

clases, variables y métodos-

soportan

asee: referencias a otras clases Los tres tipos de comentarios de documentación (de clase, variable y métodos) pueden contener etiquetas a s e e , que permiten hacer referencia a la documentación de otras clases. Javadoc generará HTML con las etiquetas @see en forma de vínculos a la otra documentación. Las formas son: @see nombredeclase @see nombredeclase-totalmente-cualificada Fsee nombredeclase-totalmente-cualifi~ada#nombre-metodo

Cada una añade un hipervínculo "Ver también" a la documentación generada. Javadoc no comprobará los hipervínculos que se le proporcionen para asegurarse de que sean válidos.

Etiquetas de documentación de clases Junto con el HTML empotrado y las referencias a s e e , la documentación de clases puede incluir etiquetas de información de la versión y del nombre del autor. La documentación de clases también puede usarse para las interfaces (véase Capítulo 8).

Es de la forma:

1

@versión información-de-versión

en el que información-de-versión es cualquier información significativa que se desee incluir. Cuando se especifica el indicador -versión en la línea de comandos javadoc, se invocará especialmente a la información de versión en la documentación HTML generada.

Es de la forma:

1

Fautor información-del-autor

donde la información-del-autor suele ser el nombre, pero podría incluir también la dirección de correo electrónico u otra información apropiada. Al activar el parámetro -author en la línea de comandos javadoc, se invocará a la información relativa al autor en la documentación HTML generada.


2: Todo es un objeto

75

Se pueden tener varias etiquetas de autor, en el caso de tratarse de una lista de autores, pero éstas deben ponerse consecutivamente. Toda la información del autor se agrupará en un único párrafo en el HTML generado.

Esta etiqueta permite indicar la versión del código que comenzó a utilizar una característica concreta. Se verá que aparece en la documentación para ver la versión de JDK que se está utilizando.

Etiquetas de documentación de variables La documentación de variables solamente puede incluir HTML empotrado y referencias @see.

Etiquetas de documentación de métodos Además de documentación empotrada y referencias @see,los métodos permiten etiquetas de documentación para los parámetros, los valores de retorno y las excepciones.

eparam Es de la forma:

1

@paran

nombre-pardme tro descripción

donde nombre-parámetroes el identificador de la lista de parámetros, y descripción es el texto que vendrá en las siguientes líneas. Se considera que la descripción ha acabado cuando se encuentra una nueva etiqueta de documentación. Se puede tener cualquier número de estas etiquetas, generalmente una por cada parámetro.

Es de la forma:

1

ereturn descripción

donde descripción da el significado del valor de retorno. Puede ocupar varias líneas.

Las excepciones se verán en el Capítulo 10, pero sirva como adelanto que son objetos que pueden "lanzarse" fuera del método si éste falla. Aunque al invocar a un método sólo puede lanzarse una excepción, podría ocurrir que un método particular fuera capaz de producir distintos tipos de excepciones, necesitando cada una de ellas su propia descripción. Por ello, la etiqueta de excepciones es de la forma:

1

Fthrows nombre-de-clase-totalmente-cualificada descripción


76

Piensa en Java

donde nombre-de-clase-totalmente-cualificada proporciona un nombre sin ambigüedades de una clase de excepción definida en algún lugar, y descripción (que puede extenderse a lo largo de varias líneas) indica por qué podría levantarse este tipo particular de excepción al invocar al método.

Se utiliza para etiquetar aspectos que fueron mejorados. Esta etiqueta es una sugerencia para que no se utilice esa característica en particular nunca más, puesto que en algún momento del futuro puede que se elimine. Un método marcado como @deprecatedhace que el compilador presente una advertencia cuando se use.

Ejemplo de documentación He aquí el primer programa Java de nuevo, al que en esta ocasión se ha añadido documentación en forma de comentarios: / / : c02:HolaFecha.java import java.uti1.*;

/ * * El primer ejemplo de Piensa en Java. * Muestra una cadena de caracteres y la fecha de hoy. * @author Bruce Eckel * @author www.BruceEckel.com * @version 2.0 */ public class HolaFecha { / * * Único punto de entrada para la clase y la aplicación * @param args array de cadenas de texto pasadas como

parámetros * @return No hay valor de retorno * Fexception exceptions No se generarán excepciones */ public static void main (String[] args) { System.out .printl ola, hoy es: " ) ; System.out .println (new Date O ) ;

La primera línea del archivo utiliza mi propia técnica de poner ":"como marcador especial de la línea de comentarios que contiene el nombre del archivo fuente. Esa línea contiene la información de la trayectoria al fichero (en este caso, c02 indica el Capítulo 2) seguido del nombre del archivo". La última línea también acaba con un comentario, esta vez indicando la finalización del listado de código fuente, que permite que sea extraído automáticamente del texto de este libro y comprobado por un compilador. W n a herramienta que he creado usando Python (ver http:liwww.Pyihori.org) utiliza esta información para extraer esos ficheros de código, ponerlos en los subdirectorios apropiados y crear los "makefiles".


2: Todo es un objeto

77

Estilo de codificación El estándar no oficial de java dice que se ponga en mayúsculas la primera letra del nombre de una clase. Si el nombre de la clase consta de varias palabras, se ponen todas juntas (es decir, no se usan guiones bajos para separar los nombres) y se pone en mayúscula la primera letra de cada palabra, como por ejemplo:

En casi todo lo demás: métodos, campos (variables miembro), y nombres de referencias a objeto, el estilo aceptado es el mismo que para las clases, con la excepción de que la primera letra del identificador debe ser minúscula. Por ejemplo: class TodosLosColoresDelArcoiris { int unEnteroQueRepresentaUnColor; void cambiarElTonoDelColor (int nuevoTono) // ...

{

Por supuesto, hay que recordar que un usuario tendría que teclear después todos estos nombres largos, por lo que se ruega a los programadores que lo tengan en cuenta. El código Java de las bibliotecas de Sun también sigue la forma de apertura y cierre de las llaves que se utilizan en este libro.

Resumen En este capítulo se ha visto lo suficiente de programación en Java como para entender cómo escribir un programa sencillo, y se ha realizado un repaso del lenguaje y algunas de sus ideas básicas. Sin embargo, los ejemplos hasta la fecha han sido de la forma "haz esto, después haz esto otro, y finalmente haz algo más". ¿Y qué ocurre si quieres que el programa presente alternativas, como "si el resultado de hacer esto es rojo, haz esto; sino, haz no se qué más?" El soporte que Java proporciona a esta actividad fundamental de programación se verá en el capítulo siguiente.

tjercicios Las soluciones a determinados ejercicios se encuentran en el documento The Thinking in Java Annotated Solution Guide, disponible a bajo coste en http://www.BruceEcke1.com.

1.

Siguiendo el ejemplo HolaFecha.java de este capítulo, crear un programa "Hola, mundo" que simplemente escriba esa frase. Sólo se necesita un método en la clase (la clase "main" que es la que se ejecuta al arrancar el programa). Recordar hacerla static e incluir la lista de parámetros, incluso aunque no se vaya a usar. Compilar el programa con javac y ejecutarlo


78

Piensa en Java

utilizando java. Si se utiliza un entorno de desarrollo distinto a JDK, aprender a compilar y ejecutar programas en ese entorno. Encontrar los fragmentos de código involucrados en UnNombreDeTipo y convertirlos en un programa que se compile y ejecute. Convertir los fragmentos de código de SoloDatos en un programa que se compile y ejecute. Modificar el Ejercicio 3, de forma que los valores de los datos de SoloDatos se asignen e impriman en main( ). Escribir un programa que incluya y llame al método almacenamiento(), definido como fragmento de código en este capítulo. Convertir los fragmentos de código de FunEstatico en un programa ejecutable. Escribir un programa que imprima tres parámetros tomados de la línea de comandos. Para lograrlo, será necesario indexarlos en el array de Strings de línea de comandos. Convertir el ejemplo TodosLosColoresDelArcoiris en un programa que se compile y ejecute. Encontrar el código de la segunda versión de HolaFechajava, que es el ejemplo de documentación en forma de comentarios. Ejecutar javadoc del fichero y observar los resultados con el navegador web. Convertir PruebaDoc en un fichero que se compile y pasarlo por javadoc. Verificar la documentación resultante con el navegador web. Añadir una lista de elementos HTML a la documentación del Ejercicio 10. Tomar el programa del Ejercicio 10 y añadirle documentación en forma de comentarios. Extraer esta documentación en forma de comentarios a un fichero HTML utilizando javadoc y visualizarla con un navegador web.


3: Controlar el flujo del programa Al igual que una criatura con sentimientos, un programa debe manipular su mundo y tomar decisiones durante su ejecución. En Java, se manipulan objetos y datos haciendo uso de operadores, y se toman decisiones con la ejecución de sentencias de control. Java se derivó de C++, por lo que la mayoría de esas sentencias y operadores resultarán familiares a los programadores de C y C++.Java también ha añadido algunas mejoras y simplificaciones. Si uno se encuentra un poco confuso durante este capítulo, acuda al CD ROM multimedia adjunto al libro: Thinking in C: Foundations for Java and C++. Contiene conferencias sonoras, diapositivas, ejercicios y soluciones diseñadas específicamente para ayudarle a adquirir familiaridad con la sintaxis de C necesaria para aprender Java.

Utilizar operadores

Java

Un operador toma uno o más parámetros y produce un nuevo valor. Los parámebos se presentan de

distinta manera que en las llamadas ordinarias a métodos, pero el efecto es el mismo. Uno debería estar razonablemente cómodo con el concepto general de operador con su experiencia de programación previa. La suma (+), la resta y el menos unario (-) , la multiplicación (*), la división (/), y la asignación (=) funcionan todos exactamente igual que en el resto de lenguajes de programación. Todos los operadores producen un valor a partir de sus operandos. Además, un operador puede variar el valor de un operando. A esto se le llama efecto lateral. El uso más común de los operadores que modifican sus operandos es generar el efecto lateral, pero uno debería tener en cuenta que el valor producido solamente podrá ser utilizado en operadores sin efectos laterales. Casi todos los operadores funcionan únicamente con datos primitivos. Las excepciones las constituyen “=" , "==" y "!=", que funcionan con todos los objetos (y son una fuente de confusión para los objetos). Además, la clase String soporta "+" y "+=".

Precedencia La precedencia de los operadores define cómo se evalúa una expresión cuando hay varios operadores en la misma. Java tiene reglas específicas que determinan el orden de evaluación. La más fácil de recordar es que la multiplicación y la división siempre se dan tras la suma y la resta. Los programadores suelen olvidar el resto de reglas de precedencia a menudo, por lo que se deberían usar paréntesis para establecer explícitamente el orden de evaluación. Por ejemplo:


80

Piensa en Java

tiene un significado diferente que la misma sentencia con una agrupación particular de paréntesis:

Asignación La asignación se lleva a cabo con el operador =. Significa "toma el valor de la parte derecha (denominado a menudo dvalor) y cópialo a la parte izquierda (a menudo denominada ivalor"). Un ivalor es cualquier constante, variable o expresión que pueda producir un valor, pero un ivalor debe ser una variable única con nombre. (Es decir, debe haber un espacio físico en el que almacenar un valor.) Por ejemplo, es posible asignar un valor constante a una variable (A = 4;), pero no se puede asignar nada a un valor constante -no puede ser un ivalor. (No se puede decir 4 = A,.) La asignación de tipos primitivos de datos es bastante sencilla y directa. Dado que el dato primitivo alberga el valor actual y no una referencia a un objeto, cuando se asignan primitivas se copian los contenidos de un sitio a otro. Por ejemplo, si se dice A = B para datos primitivos, los contenidos de B se copian a A. Si después se intenta modificar A, lógicamente B no se verá alterado por esta modificación. Como programador, esto es lo que debería esperarse en la mayoría de situaciones. Sin embargo, cuando se asignan objetos, las cosas cambian. Siempre que se manipula un objeto, lo que se está manipulando es la referencia, por lo que al hacer una asignación "de un objeto a otro" se está, de hecho, copiando una referencia de un sitio a otro. Esto significa que si se escribe C = D siendo ambos objetos, se acaba con que tanto C como D apuntan al objeto al que originalmente sólo apuntaba D. El siguiente ejemplo demuestra esta afirmación. He aquí el ejemplo: / / : c03:Asignacion.java / / La asignación con objetos tiene su truco. class Numero int i;

{

public class Asignacion { public static void main (String[] args) Numero nl = new Numero(); Numero n2 = new Numero ( ) ; n1.i = 9; n2.i = 47; System.out.println("1:nl.i: " + n1.i

nl

=

{

+ ",

n2.i: "

+

n2.i);

n2;

System.out.println("2: n1.i: " t n1.i t n1.i = 27; System.out .println ("3: nl. i: " + n1.i +

",

n2.i: "

+ n2.i);

", n2.i: " + n2.i);


3: Controlar el flujo del programa

81

La clase Número es sencilla, y sus dos instancias ( n l y n2) se crean dentro del método main(). Al valor i de cada Número se le asigna un valor distinto, y posteriormente se asigna n 2 a n l , y se varía n l . En muchos lenguajes de programación se esperaría que n l y n 2 fuesen independientes, pero dado que se ha asignado una referencia, he aquí la salida que se obtendrá:

Al cambiar el objeto n l parece que se cambia el objeto n 2 también. Esto ocurre porque, tanto n l , como n 2 contienen la misma referencia, que apunta al mismo objeto. (La referencia original que estaba en n l que apuntaba al objeto que albergaba el valor 9 fue sobreescrita durante la asignación y, en consecuencia, se perdió; su objeto será eliminado por el recolector de basura.) A este fenómeno se le suele denominar uso de alias y es una manera fundamental que tiene Java de trabajar con los objetos. Pero, ¿qué ocurre si uno no desea que se dé dicho uso de alias en este caso? Uno podría ir más allá con la asignación y decir:

Esto mantiene los dos objetos separados en vez de desechar uno y vincular n l y n 2 al mismo objeto, pero pronto nos damos cuenta que manipular los campos de dentro de los objetos es complicado y atenta contra los buenos principios de diseño orientado a objetos. Este asunto no es trivial, por lo que se deja para el Apéndice A, dedicado al uso de alias. Mientras tanto, se debe recordar que la asignación de objetos puede traer sorpresas.

Uso d e alias d u r a n t e llamadas a m é t o d o s También puede darse uso de alias cuando se pasa un objeto a un método: / / : c03:PasarObjeto.java / / Pasar objetos a métodos puede no ser aquello a lo que uno está / / acostumbrado. class Carta { char c; 1 public class PasarObjecto { static void f(Carta y) { y.c = ' z , 1 public static void main (String[] args) { Carta x = new Carta(); X.C = 'a'; System.out.println("I: x.c: " + x.c); f (x); System.out.printl("2:x.c: " + x.c); 1 .


82

Piensa en Java

En muchos lenguajes de programación el método f( ) parecería estar haciendo una copia de su argumento Carta y dentro del ámbito del método. Pero una vez más, se está pasando una referencia, por lo que la línea: y.c

=

'z';

está, de hecho, cambiando el objeto fuera de f( ). La salida tiene el aspecto siguiente:

El uso de alias y su solución son un aspecto complejo, y aunque uno debe esperar al Apéndice A para tener todas las respuestas, hay que ser consciente de este problema desde este momento, de forma que podamos estar atentos y no caer en la trampa.

Operadores matemáticos Los operadores matemáticos básicos son los mismos que los disponibles en la mayoría de lengua-

jes de programación: suma (+), resta (-), división (/), multiplicación (*) y módulo (%,que devuelve el resto de una división entera). La división entera trunca, en vez de redondear, el resultado. Java también utiliza una notación abreviada para realizar una operación y llevar a cabo una asignación simultáneamente. Este conjunto de operaciones se representa mediante un operador seguido del signo igual, y es consistente con todos los operadores del lenguaje (cuando tenga sentido). Por ejemplo, para añadir 4 a la variable x y asignar el resultado a x puede usarse: x+=4. El siguiente ejemplo muestra el uso de los operadores matemáticos: / / : c03:OperadoresMatematicos.java / / Demuestra los operadores matemáticos import java.uti1.*;

public class OperadoresMatematicos { / / Crear un atajo para ahorrar teclear: static void visualizar(String S) { System.out .println (S);

1 / / Atajo para visualizar un string y un entero: static void p I n t ( t i S , i n t . i) { visualizar(s + " = " + i); 1 / / Atajo para visualizar una cadena de caracteres y u n float: static void pFlt (String S, float f) { visualizar(s + " = " + f);


3: Controlar el flujo del programa

83

public static void main(String [ ] args) { / / Crear un generador de números aleatorios / / El generador se alimentará por defecto de la hora actual: Random aleatorio = new Random() ; int i, j, k; / / '%' limita el valor a 99: j = aleatorio.nextInt ( ) % 100; k = aleatorio.nextInt ( ) %100; pInt ("jl',j);p~nt("k",k); i = j t k; p ~ n t ( " j+ k", i); i = j - k; pInt("j - k " , i); i = k / j; pInt("k / j " , i); i = k *j; pInt("k * j", i); i = k % j; pInt("k % j", i); j o-ok; p ~ n ( t "j %= k", j) ; / / Pruebas de números de coma flotante: float u,v,w; / / Se aplica también a doubles v = aleatorio.nextFloat ( ) ; w = aleatorio.nextFloat ( ) ; pFlt("v", v); pFlt("w", w); u = v t w; pFlt("v + w " , u); U = v - w; pFlt(I1v - w " , u); U = v * w; pFlt("v * w " , u); u = v / w; pFlt("v / w", u); / / Lo siguiente funciona también para char, byte / / short, int, long, y double: u += v; pFlt("u += v", u); u -= v; pFlt("u -= v", u) ; u *= v; pFlt("u *= v " , u); u / = v; pFlt("u /= v", u);

1 1 ///:-

Lo primero que se verán serán los métodos relacionados con la visualización por pantalla: el método visualizar( ) imprime un String, el método pInt( ) imprime un String seguido de un int, y el médodo pFlt( ) imprime un String seguido de un float. Por supuesto, en última instancia todos usan System.out.prhtln( ). Para generar números, el programa crea en primer lugar un objeto Random. Como no s e le pasan parámetros en su creación, Java usa la hora actual como semilla para el generador de números aleatorio. El programa genera u11 conjunto de números aleatorios de distinto tipo con el objeto Random simplemente llamando a distintos métodos: nextInt( ), nextlong( ), nextFloat( ) o nextDouble( ). Cuando el operador módulo se usa con el resultado de un generador de números aleatorios, limita el resultado a un límite superior del operando menos uno (en este caso 99).


84

Piensa en Java

Los operadores unarios de suma y resta El menos unario (-) y el más unario (+) son los mismos operadores que la resta y la suma binarios. El compilador averigua cuál de los dos usos es el pretendido por la manera de escribir la expresión. Por ejemplo, la sentencia:

tiene un significado obvio. El compilador es capaz de averiguar:

Pero puede que el lector llegue a confundirse, por lo que es más claro decir:

El menos unario genera el valor negativo del valor dado. El más unario proporciona simetría con el menos unario, aunque no tiene ningún efecto.

Autoincremento y Autodecremento Tanto Java, como C, está lleno de atajos. Éstos pueden simplificar considerablemente el tecleado del código, y aumentar o disminuir su legibilidad. Dos de los atajos mejores son los operadores de incremento y decremento (que a menudo se llaman operadores de autoincremento y autodecremento). El operador de decremento es -- y significa "disminuir en una unidad". El operador de incremento es ++ y significa "incrementar en una unidad". Si a es un entero, por ejemplo, la expresión ++a es equivalente a (a = a + 1). Los operadores de incremento y decremento producen el valor de la variable como resultado. Hay dos versiones de cada tipo de operador, llamadas, a menudo, versiones prefija y postfija. El preincremento quiere decir que el operador ++ aparece antes de la variable o expresión, y el postincremento significa que el operador ++ aparece después de la variable o expresión. De manera análoga, el predecremento quiere decir que el operador -- aparece antes de la variable o expresión, y el post-decremento significa que el operador -- aparece después de la variable o expresión. Para el preincremento y el predecremento (por ejemplo, ++ao-a), la operación se lleva a cabo y se produce el valor. En el caso del postincremento y postdecremento (por ejemplo, a++o a--) se produce el valor y después se lleva a cabo la operación. Por ejemplo: / / : c03:AutoInc.java / / Mostrar el funcionamiento de los operadores

++

public class AutoInc { public static void main (String[] args) { int i = 1; visualizar ("i : " + i) ; visualizar (I1++i : " + ++i) ; / / Pre-incremento visualizar (I1i++ : " + i++) ; / / Post-incremento

y --


3: Controlar el flujo del programa

visualizar ( " i : visualizar ("--i visualizar ("i-visualizar ( " i :

85

" + i) ; " + --i) ; / / Pre-decremento

:

:

"

+ i--) ; / / Post-decremento

" + i) ;

J

static void visualizar (String S) System.out .println (S);

{

1 1 ///:-

La salida de este programa es:

Se puede pensar que con la forma prefija se consigue el valor después de que se ha hecho la operación, mientras que con la forma postfija se consigue el valor antes de que la operación se lleve a cabo. Éstos son los únicos operadores (además de los que implican asignación) que tienen efectos laterales. (Es decir, cambian el operando en vez de usarlo simplemente como valor.) El operador de incremento es una explicación para el propio nombre del lenguaje C++, que significa "un paso después de C". En una de las primeras conferencias sobre Java, Bill Joy (uno de sus creadores), dijo que "Java=C++-" (C más más menos menos), tratando de sugerir que Java es C++ sin las partes duras no necesarias, y por consiguiente, un lenguaje bastante más sencillo. A medida que se progrese en este libro, se verá cómo muchas partes son más simples, y sin embargo, Java no es mucho más fácil que C++.

Operadores relacionales Los operadores relacionales generan un resultado de tipo boolean. Evalúan la relación entre los valores de los operandos. Una expresión relaciona1produce true si la relación es verdadera, y false si la relación es falsa. Los operadores relacionales son menor que (<), mayor que (>), menor o igual que (<=), mayor o igual que (>=), igual que (==) y distinto que (!=). La igualdad y la desigualdad funcionan con todos los tipos de datos predefinidos, pero las otras comparaciones no funcionan con el tipo boolean.

Probando la equivalencia de objetos I m operadores relacionales == y != funcionan con todos los objetos, pero su significado suele confundir al que programa en Java por primera vez. He aquí un ejemplo:

public class Equivalencia { public static void main (String[l args)

{


86

Piensa en Java

Integer nl = new Integer(47); Integer n2 = new Integer (47); Systern.out .println (n1 == n2) ; Systern.out .println (n1 ! = n2) ;

1 1 ///:-

La expresión System.out.println(n1 == n2) visualizará el resultado de la comparación de tipo 1ógico. Seguramente la salida debería ser true y después false, pues ambos objetos Integer son el mismo. Pero mientras que los contenidos de los objetos son los mismos, las referencias no son las mismas, y los operadores == y != comparan referencias a objetos. Por ello, la salida es, de hecho, false y después true. Naturalmente, esto sorprende a la gente al principio. ¿Qué ocurre si se desea comparar los contenidos de dos objetos? Es necesario utilizar el método especial equals( ) que existe para todos los objetos (no tipos primitivos, que funcionan perfectarnente con == y !=). He aquí cómo usarlo:

public class MetodoComparacion { public static void main(String[] args) Integer nl = new Integer(47); Integer n2 = new Integer(47); System.out.println(nl.equals(n2)); 1 1 ///:-

{

El resultado será true, tal y como se espera. Ah, pero no es así de simple. Si uno crea su propia clase, como ésta:

/

/ / :cO3 :MetodoComparacion2.java class Valor int i;

{

1 public class MetodoCornparacion2 { public static void main(String[] args) Valor vl = new Valor ( ) ; Valor v2 = new Valor ( ) ; v1.i = v2.i = 100; System.out.println(vl.equals(v2)); 1 1 ///:-

{

se obtiene como resultado falso. Esto se debe a que el comportamiento por defecto de equals( ) es comparar referencias. Por tanto, a menos que se invalide equals( ) en la nueva clase no se obtendrá el comportamiento deseado. Desgraciadamente no se mostrarán las invalidaciones hasta el Capítulo


3: Controlar el flujo del programa

87

7, pero debemos ser conscientes mientras tanto de la forma en que se comporta equals( ) podría ahorrar algunos problemas.

La mayoría de clases de la biblioteca Java implementan equals( ), de forma que compara los contenidos de los objetos en vez de sus referencias.

Operadores lógicos Los operadores lógicos AND (&&), OR (11) y NOT(!) producen un valor lógico (true o false) basado en la relación lógica de sus argumentos. Este ejemplo usa los operadores relacionales y lógicos: / / : c03:Logico.java / / Operadores relacionales y lógicos import java.uti1. *; public class Loqico

{

public static void main(String[] args) { Random aleatorio = new Random ( ) ; int i = aleatorio.nextInt() % 100; int j = aleatorio.nextInt() % 100; visualizar("i = " + i); visualizar("j = " + j); visualizar("i > j es " + (i > j)); visualizar("i < j es " + (i < j)); visualizar ( " i >= j es " + (i >= j) ) ; visualizar("i <= j es " + (i <= j)); visualizar ( " i == j es " + (1 == 1)); visualizar("i ! = j es " + (i ! = 1));

/ / Tratar un int como un boolean no es legal en Java ( " i & & j es " + (i & & j)); ("i 1 1 j es " + (i 1 1 j)); ( " ! i es " + ! i) ;

/ / ! visualizar / / ! visualizar / / ! visualizar

visualizar(" (i<10) & & visualizar ( " (i<10) 1 1

(j<10) es " + (j<10) es " +

static void visualizar (String S) System.out .println ( S ) :

((i < 10) & & ( (i < 10) 1 I

(j < 10)); (j < 10) ) ;

{

Sólo es posible aplicar AND, OR o NOT a valores boolean. No se puede construir una expresión 1ógica con valores que no sean de tipo boolean, cosa que sí se puede hacer en C y C++. Se pueden


88

Piensa en Java

ver intentos fallidos de hacer esto en las líneas que comienzan por //! en el ejemplo anterior. Sin embargo, las sentencias que vienen a continuación producen valores lógicos utilizando comparaciones relacionales, y después se usan operaciones lógicas en los resultados. El listado de salida tendrá la siguiente apariencia: i = 85 ; j = 4; i > j es true i < j es false i >= j es true i <= j es false i == j es false i ! = es true (i < 10) & & (j < 10) es false (i < 10) 1 I (j > 10) es true

Obsérvese que un valor lógico se convierte automáticamente a formato de texto si se utiliza allí donde se espera un String. Se puede reemplazar la definición int en el programa anterior por cualquier otro tipo de datos primitivo, excepto boolean. Hay que tener en cuenta, sin embargo, que la comparación de números en coma flotante es muy estricta. Un número que sea diferente por muy poco de otro número sigue siendo "no igual". Un número infinitamente próximo a cero es distinto de cero.

Cortocircuitos Al manipular los operadores lógicos se puede entrar en un fenómeno de "cortocircuito". Esto significa que la expresión se evaluará únicamente hasta que se pueda determinar sin ambigüedad la certeza o falsedad de toda la expresión. Como resultado, podría ocurrir que no sea necesario evaluar todas las partes de la expresión lógica. He aquí un ejemplo que muestra el funcionamiento de los cortocircuitos: / / : c03:CortoCircuito.java / / Demuestra el comportamiento de los cortocircuitos con operadores lógicos. public class Cortocircuito { static boolean pruebal (int val) { System.out .println ("pruebal( " + val + " ) " ) ; System.out .println ("resultado: " + (val < 1) ) returri val < 1;

;

1 static boolean prueba2 (int val) { System.out .println ("prueba2( " + val + " ) " ) ; System.out .println ("resultado: " + (val < 2) ) ; return val < 2; }


3: Controlar el flujo del programa

static boolean prueba3 (int val) { System.out .println ("prueba3( " + val + ")" ) ; System.out .println ("resultado: " + (val < 3) ) return val < 3;

89

;

1 public static void main (Stringi] args) { if (pruebal(0) & & prueba2 (2) & & prueba3 (2)) System.out.println("La expresión es verdadera"); else System.out.println("La expresión es falsa"); }

1 ///:-

Cada test lleva a cabo una comparación con el argumento pasado y devuelve verdadero o falso. También imprime información para mostrar lo que se está invocando. Las comprobaciones se usan en la expresión: if (pruebal(O)

&&

prueba2 (2)

&&

prueba3 (2))

Naturalmente uno podría pensar que se ejecutarían las tres pruebas, pero en la salida se muestra de otra forma: pruebal (0) resultado : true prueba2 (2) resultado: false la expresión es falsa

La primera prueba produjo un resultado verdadero, de forma que la evaluación de la expresión continúa. Sin embargo, el segundo test produjo un resultado falso. Puesto que esto significa que toda la expresión va a ser falso ¿por qué continuar evaluando el resto de la expresión? Podría ser costoso. Ésa es precisamente la razón para realizar un cortocircuito; es posible lograr un incremento potencial de rendimiento si no es necesario evaluar todas las partes de la expresión lógica.

Operadores de bit Los operadores a nivel de bit permiten manipular bits individuales de la misma forma que si fueran tipos de datos primitivos íntegros. Los operadores de bit llevan a cabo álgebra lógica con los bits correspondientes de los dos argumentos, para producir el resultado. Los operadores a nivel de bit provienen de la orientación a bajo nivel de C, para la manipulación directa del hardware y el establecimiento de los bits de los registros de hardware. Java se diseñó originalmente para ser empotrado en las cajas set-top de los televisores. de forma que esta orientación de bajo nivel tenía sentido. S in embargo, probablcmcnte no se haga mucho uso de estos operadores de nivel de bit. El operador de bit AND (&) produce un uno a la salida si los dos bits de entrada son unos; si no, produce un cero. El operador de bit OR (1) produce un uno en la salida si cualquiera de los bits de


90

Piensa en Java

entrada es un uno, y produce un cero sólo si los dos bits de entrada son cero. El operador de bit OR EXCLUSIVO o XOR (A),produce un uno en la salida si uno de los bits de entrada es un uno, pero no ambos. El operador de bit NOT (-, también llamado operador de complemento a uno) es un operador unario; toma sólo un argumento. (Todos los demás operadores de bits son operadores binarios.) El operador de bit NOT produce el contrario del bit de entrada -un uno si el bit de entrada es cero y un cero si el bit de entrada es un uno. Los operadores de bit y lógicos utilizan los mismos caracteres, por lo que ayuda tener algún mecanismo mnemónico para ayudar a recordar su significado: dado que los bits son "pequeños", sólo hay un carácter en los operadores de bits. Los operadores de bit se pueden combinar con el signo = para unir la operación a una asignación: A= son válidos (dado que - es un operador unario, no puede combinarse con el signo =).

&=, I= y

El tipo boolean se trata como un valor de un bit, por lo que es en cierta medida distinto. Se puede llevar a cabo un AND, OR o XOR de bit, pero no se puede realizar un NOT de bit (se supone que para evitar la confusión con el NOT lógico). Para los datos de tipo boolean, los operadores de bit tienen el mismo efecto que los operadores lógicos, excepto en que no tienen capacidad de hacer cor-

tocircuito~.Además, los operadores de bit sobre datos de tipo boolean incluyen un operador XOR lógico no incluido bajo la lista de operadores "lógicos". Hay que tratar de evitar los datos de tipo boolean en las expresiones de desplazamiento, descritas a continuación.

Operadores de desplazamiento Los operadores de desplazamiento también manipulan bits. Sólo se pueden utilizar con tipos primitivos enteros. El operador de desplazamiento a la izquierda (<<) provoca que el operando de la izquierda del operador sea desplazado a la izquierda, tantos bits como se especifique tras el operador (insertando ceros en los bits menos significativos). El operador de desplazamiento a la derecha con signo (>>) provoca que el operando de la izquierda del operador sea desplazado a la derecha el número de bits que se especifique tras el operador. El desplazamiento a la derecha con signo >> utiliza la extensión de signo: si el valor es positivo se insertan ceros en los bits más significativos; si el valor es negativo, se insertan unos en los bits más significativos. Java también ha incorporado el operador de rotación a la derecha sin signo >>>, que utiliza la extensión cero: independientemente del signo, se insertan ceros en los bits más significativos. Este operador no existe ni en C ni en C++. Si se trata de desplazar un char, un byte o un short, éste será convertido a int antes de que el desplazamiento tenga lugar y el resultado será también un int. Sólo se utilizarán los cinco bits menos significativos de la parte derecha. Esto evita que se desplace un número de bits mayor al número de bits de un int. Si se está trabajando con un long, se logrará un resultado long. Sólo se usarán los seis bits menos significativos de la parte derecha, por lo que no es posible desplazar más bits que los que hay en un long. Los desplazamientos pueden combinarse con el signo igual (<<= o >>= o >>>=). El ivalor se reemplaza por el ivalor desplazado por el dvalor. Hay un problema, sin embargo, con el desplazamiento sin signo a la derecha combinado con la asignación. Si se utiliza con un byte o short no se logra el resultado correcto. En vez de esto, los datos son convertidos a int y desplazados a la derecha, y


3: Controlar el flujo del programa

91

teriormente se truncan al ser asignados de nuevo a sus variables, por lo que en esos casos el resultado suele ser -1. El ejemplo siguiente demuestra esto: / / : c03:DesplDatosSinSigno.java / / Prueba del desplazamiento a la derecha sin signo.

public class DesplDatosSinSigno { public static void main (String[] args) int i = -1; i >>>= 10; System.out.println (i); long 1 = -1; 1 >>>= 10;

{

short S = -1; >>>= 10;

S

System.out .println (S); byte b

=

-1;

b >>>= 10; System.out .println (b); b = -1. System.out.println(b>>>lO);

1 1 ///:-

En la última línea, no se asigna el valor resultante de nuevo a b, sino que se imprime directamente para que se dé el comportamiento correcto.

He aquí un ejemplo que demuestra el uso de todos los operadores que involucran a bits: / / : c03:ManipulacionBits.java / / Utilizando los operadores de bit. import java.util. *; public class ManipulacionBits { public static void main (Stringr] args) Random aleatorio = new Random ( ) ; int i = aleatorio. nextInt ( ) ; int j = aleatorio. nextInt ( ) ; p ~ i n ~ (n" t -1 " , -1); pBinInt ("+11',+1) ; int posmax = 2147483647; pBinInt ( "posmax", posmax) ; int negmax - -2147483648; p ~ i n ~ (n"negmax" t , negmax) ; pBinInt ( " i " , i) ; pBinInt ("-i", -i) ;

{


92

Piensa en Java

pBin1nt ( " -i", -i) ; pBinInt ( " j " , j ) ; pBinInt("i & j " , i & 1); pBinInt ("i 1 j " , i 1 j) ; pBinInt("i j", i 1); pBin1nt(I1i << 5 " , i << 5); pBin1nt(I1i >> 5 " , i >> 5); pBin1nt ( "(-i) >> 5 " , (-i) >> 5) ; pBin1nt ( " i >>> 5 " , i >>> 5) ; pBin1nt ( " (-i) >>> 5 " , (-i) >>> 5) ; A

A

long 1 = aleatorio.nextLong ( ) ; long m = aleatorio.nextLong ( ) ; pBinLong ( " - l ~ " -1L) , ; pBinLong("+l~",+1L); long 11 = 9223372036854775807L; pBinLonq ( "maxpos" , 11): long lln = -9223372036854775808L; pBinLong ( "maxneg", lln) ; pBinLong ( " l " , 1) ; pBinLong (lb-l",-1) ; p~in~on ( "g -1", -1); pBinLong ( " m " , m) ; pBinLong("1 & m " , 1 & m) ; pBinLong( " 1 I m " , 1 1 m) ; pBinLong ( " 1 m", 1 m) ; pBinLong ( " 1 << 5 " , 1 << 5) ; pBinLong ( " 1 >> 5 " , 1 >> 5) ; pBinLong ( " (-1) >> 5 " , (-1) >> 5) ; pBinLong ( " 1 >>> 5 " , 1 >>> 5) ; pBinLong ( " (-1) >>> 5 " , (-1) >>> 5) ; A

A

1 static void pBinInt (String S , int i) { System.out.println( S + 'Ir int: " + i + " , binario: " ) System.out .print ( " "); for(int j = 31; j >=O; j--) if(((1 << j) & i) ! = 0) System.out .print ( " 1 " ) ; else System.out.print("OM); System.out.println(); 1 static void pBinLong(String S, long 1) System.out.println(

;

{


3: Controlar el flujo del programa

s + " , long: " + 1 + " , binario: System.out .print ( " "); for(int i = 63; i >=O; i--) if(((1L << i) & 1) ! = 0) System.out.print("1"); else System.out .print ( " O " ) ; System.out.println();

93

");

1 1 ///:-

Los dos métodos del final, pBinInt( ) y pBinLong( ) toman un int o un long, respectivamente, y lo imprimen en formato binario junto con una cadena de caracteres descriptiva. De momento, ignoraremos la implementación de estos métodos. Se habrá dado cuenta el lector del uso de System.out.print( ) en vez de System.out.println( ). El método print( ) no finaliza con un salto de línea, por lo que permite ir visualizando una línea por fragmentos. Además de demostrar el efecto de todos los operadores de bit para int y long, este ejemplo también muestra los valores mínimo, el máximo, +1y -1 para int y para long, por lo que puede verse qué aspecto tienen. Nótese que el bit más significativo representa el signo: O significa positivo, y 1 significa negativo. La salida de la porción int tiene la apariencia siguiente: -1, int: -1, binario: 11111111111111111111llllllllllll +l, int: 1, binario: 00000000000000000000000000000001 posmax, int: 2147483647, binario: 01111111111111111111111111111111 negmax, int: -2147483648, binario: 10000000000000000000000000000000 i, int: 59081716, binario: 00000011100001011000001111110100 -ir int: -59081717, binario: 11111100011110100111110000001011 -ir int: -59081716, binarios: 11111100011110100111110000001100 j, int: 198850956, binario: 00001011110110100011100110001100 i & J , int: 58720644, binario: 00000011100000000000000110000100 i 1 j, int: 199212028, binario: 00001011110111111011101111111100 j, int: 140491384, binario: i 00001000010111111011101001111000 A


94

Piensa en Java

i << 5, int: 1890614912, binario: 01110000101100000111111010000000 i >> 5, int: 1846303, binario: 00000000000111000010110000011111 ( - i) >>5, int: -1846304, binario: 11111111111000111101001111100000 i >>> 5, int: 1846303, binario: 00000000000111000010110000011111 ( - i) >>> 5, int: 132371424, binario 00000111111000111101001111100000

La representación binaria de los números se denomina también complemento a dos con signo.

Operador ternario if-else Este operador es inusual por tener tres operandos. Verdaderamente es un operador porque produce un valor, a diferencia de la sentencia if-else ordinaria que se verá en la siguiente sección de este capítulo. La expresión es de la forma: exp-booleana ? valor0 : valorl

Si el resultado de la evaluación exp-boolean es true, se evalúa valor0 y su resultado se convierte en el valor producido por el operador. Si exp-booleana es false, se evalúa valorl y su resultado se convierte en el valor producido por el operador. Por supuesto, podría usarse una sentencia if-else ordinaria (descrita más adelante), pero el operador ternario es mucho más breve. Aunque C (del que es originario este operador) se enorgullece de ser un lenguaje sencillo, y podría haberse introducido el operador ternario en parte por eficiencia, deberíamos ser cautelosos a la hora de usarlo cotidianamente -es fácil producir código ilegible. El operador condicional puede usarse por sus efectos laterales o por el valor que produce, pero en general se desea el valor, puesto que es éste el que hace al operador distinto del if-else. He aquí un ejemplo: static int ternario(int i) { return i < 10 ? i * 100 : i

* 10;

1 Este código, como puede observarse, es más compacto que el necesario para escribirlo sin el operador ternario: static int alternativo(int i) if (i < 10) return i * 100; else r e L u r r i i * 10;

{

La segunda forma es más sencilla de entender, y no requiere de muchas más pulsacioncs. Por tanto, hay que asegurarse de evaluar las razones a la hora de elegir el operador ternario.


3: Controlar el flujo del programa

95

El operador coma La coma se usa en C y C++ no sólo como un separador en las listas de parámetros a funciones, sino también como operador para evaluación secuencial. El único lugar en que se usa el operador coma en Java es en los bucles for, que serán descritos más adelante en este capítulo.

El operador de S t r i n g

+

Hay un uso especial en Java de un operador: el operador + puede utilizarse para concatenar cadenas de caracteres, como ya se ha visto. Parece un uso natural del + incluso aunque no encaje con la manera tradicional de usar el +. Esta capacidad parecía una buena idea en C++,por lo que se añadió la sobrecarga de operadores a C++, para permitir al programador de C++ añadir significados a casi to-

dos los operadores. Por desgracia, la sobrecarga de operadores combinada con algunas otras restricciones de C++, parece convertirse en un aspecto bastante complicado para que los programadores la usen al diseñar sus clases. Aunque la sobrecarga de operadores habría sido mucho más fácil de implementar en Java que en C++,se seguía considerando que se trataba de un aspecto demasiado complicado, por lo que los programadores de Java no pueden implementar sus propios operadores sobrecargados como pueden hacer los programadores de C++. El uso del + de String tiene algún comportamiento interesante. Si una expresión comienza con un String, entonces todos los operandos que le sigan deben ser de tipo String (recuerde que el compilador convertirá una secuencia de caracteres entre comas en un String): int x = O , y = 1, z = 2; String sString = "x, y, z " ; System.out.println(sString t x t y t z);

Aquí, el compilador Java convertirá a x, y y z en sus representaciones String en vez de sumarlas. Mientras que si se escribe: System.out .printl (x t sString) ;

Java convertirá x en un String.

Pequeños fallos frecuentes a l usar operadores Uno de los errores frecuentes al utilizar operadores es intentar no utilizar paréntesis cuando se tien e la mhs mínima duda sobre cómo se evaluará una expresión. Esto sigue ocurriendo lambién en Java. Un error extremadamente frecuente en C y C++ es éste: while //

1

(x

...

=

y)

{


96

Piensa en Java

El programador estaba intentando probar una equivalencia (==) en vez de hacer una asignación. En C y C++ el resultado de esta asignación siempre será true si y es distinta de cero, y probablemente se entrará en un bucle infinito. En Java, el resultado de esta expresión no es un boolean, y el compilador espera un boolean pero no convertirá el int en boolean, por lo que dará el conveniente error en tiempo de compilación, y capturará el problema antes de que se intente siquiera ejecutar el programa. De esta forma, esta trampa jamás puede ocurrir en Java. (El único momento en que no se obtendrá un error en tiempo de compilación es cuando x e y sean boolean, en cuyo caso x = y es una expresión legal, y en el caso anterior, probablemente un error.) Un problema similar en C y C++ es utilizar los operadores de bit AND y OR, en vez de sus versiones lógicas. Los AND y OR de bit utilizan uno de los caracteres (& o 1) y los AND y OR lógicos utilizan dos (&& y 11). Como ocurre con el = y el ==, es fácil escribir sólo uno de los caracteres en vez de ambos. En Java, el compilador vuelve a evitar esto porque no los permite utilizar con operadores incorrectos.

Operadores de conversión La palabra conversión se utiliza con el sentido de "convertir1 a un molde". Java convertirá automáticamente un tipo de datos en otro cuando sea adecuado. Por ejemplo, si se asigna un valor entero a una variable de coma flotante, el compilador convertirá automáticamente el int en float. La conversión permite llevar a cabo estas conversiones de tipos de forma explícita, o forzarlas cuando no se diesen por defecto.

Para llevar a cabo una conversión, se pone el tipo de datos deseado (incluidos todos los modificadores) entre paréntesis a la izquierda de cualquier valor. He aquí un ejemplo: void conversiones ( ) { int i = 200; long 1 = (long)i; long 12 = (lon9)2OO; }

Como puede verse, es posible llevar a cabo una conversión, tanto con un valor numérico, como con una variable. En las dos conversiones mostradas, la conversión es innecesaria, dado que el compilador convertirá un valor int en long cuando sea necesario. No obstante, se permite usar conversiones innecesarias para hacer el código más limpio. En otras situaciones, puede ser esencial una conversión para lograr que el código compile. En C y C++, las conversiones pueden conllevar quebraderos de cabeza. En Java, la conversión de tipos es segura, con la excepción de que al llevar a cabo una de las denominadas conversiones reductoras (es decir, cuando se va de un tipo de datos que puede mantener más información a otro que no puede contener tanta) se corre el riesgo de perder información. En estos casos, el compilador fuerza a hacer una conversión explícita, diciendo, de hecho, "esto puede ser algo peligroso de hacer

' N. del Traductor: Casting se traduce aquí por convertir.


3: Controlar el flujo del programa

97

-si quieres que lo haga de todas formas, tiene que hacer la conversión de forma explícita". Con una conversión extensora no es necesaria una conversión explícita porque el nuevo tipo es capaz de albergar la información del viejo tipo sin que se pierda nunca ningún bit. Java permite convertir cualquier tipo primitivo en cualquier otro tipo, excepto boolean, que no permite ninguna conversión. Los tipos clase no permiten ninguna conversión. Para convertir una a otra debe utilizar métodos especiales (String es un caso especial y se verá más adelante en este libro que los objetos pueden convertirse en una familia de tipos; un Roble puede convertirse en Árbol y viceversa, pero esto no puede hacerse con un tipo foráneo como Roca.)

Literales Generalmente al insertar un valor literal en un programa, el compilador sabe exactamente de qué tipo hacerlo. Sin embargo, en ocasiones, el tipo es ambiguo. Cuando ocurre esto es necesario guiar al compilador añadiendo alguna información extra en forma de caracteres asociados con el valor literal. El código siguiente muestra estos caracteres:

class Literales { char c = Oxffff; / / Carácter máximo valor hexadecimal byte b = Ox7f; / / Máximo byte valor hexadecimal short S = Ox7fff; / / Máximo short valor hexadecimal int il = Ox2f; / / Hexadecimal (minúsculas) int i2 = OX2F; / / Hexadecimal (mayúsculas) int i3 = 0177; / / Octal (Cero delantero) / / Hex y Oct también funcionan con long. long nl = 200L; / / sufijo long long n2 = 2001; / / sufijo long long n3 = 200; / / ! long 16(200); / / prohibido float fl = 1; float f2 = 1F; / / sufijo float float f3 = lf; / / sufijo float float f4 = le-45f; / / 10 elevado a -45 float f5 = le+9f; / / sufijo float double dl = Id; / / sufijo double double d2 = 1D; / / sufijo double double d3 = 47e47d: / / 10 elevado a 47 1 ///:-

La base 16 (hexadecimal), que funciona con todos los tipos de datos enteros, se representa mediante un Ox o OX delanteros, seguidos de 0-9 y a-f, tanto en mayúsculas como en minúsculas. Si se trata de inicializar una variable con un valor mayor que el que puede albergar (independientemente de la forma numérica del valor), el compilador emitirá un mensaje de error. Fíjese en el código anterior, los valores hexadecimales máximos posibles para char, byte y short. Si se excede de éstos, el compi-


98

Piensa en Java

lador generará un valor int automáticamente e informará de la necesidad de hacer una conversión reductora para llevar a cabo la asignación. Se sabrá que se ha traspasado la línea.

La base 8 (octal) se indica mediante un cero delantero en el número, y dígitos de O a 7. No hay representación literal de números binarios en C, C++ o Java. El tipo de un valor literal lo establece un carácter arrastrado por éste. Sea en mayúsculas o minúsculas, L significa long, F significa float, y D significa double. Los exponentes usan una notación que yo a veces encuentro bastante desconcertante: 1,39 e-47f. En ciencias e ingeniería, la "e" se refiere a la base de los logaritmos naturales, aproximadamente 2,718. (Hay un valor double mucho más preciso en Java, denominado Math.E.) Éste se usa en expresión exponencial, como 1,39 e-47,que quiere decir 1,39 x 2,718.". Sin embargo, cuando se inven-

tó Fortran se decidió que la e querría indicar "diez elevado a la potencia" lo cual es una mala decisión, pues Fortran fue diseñado para ciencias e ingeniería y podría pensarse que los diseñadores deben ser conscientes de que se ha introducido semejante ambigüedad2. En cualquier caso, esta costumbre siguió en C y C++,y ahora en Java. Por tanto, si uno está habituado a pensar que e es la base de los logaritmos naturales, tendrá que hacer una traslación mental al ver una expresión como 1,39 e-47f en Java; significa 1,39 * Nótese que no es necesario utilizar el carácter final cuando el compilador puede averiguar el tipo apropiado. Con

1

long n3

=

200;

no hay ambigüedad, por lo que una L tras el 200 sería superflua. Sin embargo, con

1

float £4

=

le-47f; / / 10 elevado a

el compilador, normalmente, tomará los números exponenciales como double, de forma que sin la f arrastrada dará un error indicando que es necesario hacer una conversión de double en un float.

Promoción Al hacer operaciones matemáticas o de bit sobre tipos de datos primitivos, se descubrirá que si son más pequeños que un int (es decir, char, byte, o short), estos valores se promocionarán a int antes de hacer las operaciones, y el valor resultante será de tipo int. Por tanto, si se desea asignar el valor devuelto, de nuevo al tipo de menor tamaño, será necesario utilizar una conversión. (Y dado ' John Kirkham escribe: "Empecé a trabajar con computadores en 1962 utilizando FORTRAN 11 en un IBM 1620. En ese tiempo, y a través de los años sesenta y setenta, FORTRAN era uri leriguaje todo eri iiiayúsculas. Esto empezó probablemente porque muchos de los primeros dispositivos de enlrada erari viejas unidades de teletipo que utilizaban código Baudot de 5 hits, que no tcnia capacidad de empleo de rriiriúsculas. La 'E' para la notación exponencial era también siempre mayúscula y nunca sc confundía con la base de los logaritmos naturales 'e', que siempre era minúscula. La 'E' simplemente quería decir siempre exponencial, que era la base del sistema de numeración utilizado -generalmente 10. Eri ese ~rioineiitose coriienzó a extender entre los programadores el sistema octal. Aunque yo nunca lo vi usar, si hubiera visto un número octal en notación exponencial, habría considerado que tenía base 8. La primera vez que recuerdo ver un exponencial utilizando una 'e' minúscula fue al final de los setenta, y lo encontré bastante confuso. El problema aumentó cuando la 'e' se introdujo en FORTRAN, a diferencia de sus principios. De hecho, nosotros teníamos funciones para usar cuando realmente se quería usar la base logaritmica natural, pero todas ellas eran en mayúsculas".


3: Controlar el flujo del programa

99

que se está haciendo una asignación, de nuevo hacia un tipo más pequeño, se podría estar perdiendo información.) En general, el tipo de datos de mayor tamaño en una expresión será el que determine el tamaño del resultado de esa expresión; si se multiplica un float y un double, el resultado será double; si se suman un int y un long, el resultado será long.

Java n o tiene "sizeof" En C y C++,el operador sizeof( ) satisface una necesidad específica: nos dice el número de bits asignados a elementos de datos. La necesidad más apremiante de sizeof( ) en C y C++ es la portabilidad. Distintos tipos de datos podrían tener distintos tamaños en distintas máquinas, por lo que el programador debe averiguar cómo de grandes son estos tipos de datos, al llevar a cabo operaciones

sensibles al tamaño. Por ejemplo, un computador podría almacenar enteros en 32 bits, mientras que otro podría almacenar enteros como 16 bits. Los programas podrían almacenar enteros con valores más grandes en la primera de las máquinas. Como podría imaginarse, la portabilidad es un gran 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 los mismos tamaños en todas las máquinas. No es necesario pensar en la portabilidad a este nivel -está intrínsecamente diseñada en el propio lenguaje.

Volver a hablar acerca de la precedencia Tras oír quejas en uno de mis seminarios, relativas a la complejidad de recordar la precedencia de los operadores uno de mis alumnos sugirió un recurso mnemónico que es simultáneamente un comentario (en inglés); "Ulcer Addicts Really Like C A lot."

Mnemónico

1

Tipo d e operador

1

Operador

1 Ulcer

I

l

1

Unario

( +-++-

I

1 Addicts

1

Aritméticos (y de desplazamiento)

1

1

* / % + - << >>

Really

Relaciona1

> < >= <= == !=

Like

Lógicos (y de bit)

&&lI&IA

1c A Lot

/

Condicional (ternario) Asignación

1

A>B?X:Y = (y

1

asignaciones compuestas como *=)

Por supuesto, con los operadores de desplazamiento y de bit distribuidos por toda la tabla, el recurso mnemónico no es perfecto, pero funciona para las operacione de no bit.


100

Piensa en Java

Un compendio de operadores El ejemplo siguiente muestra qué tipos de datos primitivos pueden usarse como operadores particulares. Básicamente, es el mismo ejemplo repetido una y otra vez, pero usando distintos tipos de datos primitivos. El fichero se compilará sin error porque las líneas que causarían errores están marcadas como comentarios con un //!. //: // // //

c03:TodosOperadores.java Prueba todos los operadores con todos los tipos de datos para probar cuáles son aprobados por el compilador de Java.

class Todosoperadores

{

/ / Para aceptar los resultados de un test booleano: void

f (boolean b)

{ }

void pruebaBoo1 (boolean x, boolean y) / / Operadores aritméticos: / / ! x = x * y; / / ! x = x / y; / / ! x = x % y; / / ! x = x t y; / / ! x = x - y; / / ! x++; / / ! x--; / / ! x = +y; / / ! x = -y; / / Relacionales y lógicos : ! f(x > y); ! £(x >= y); ! f(x < y); ! f(x <= y); f (x == y) ; f (x ! = y); f(!y); x = x & & y; x = x I I y; / / Operadores de bit: / / ! x = -y; x = x & y ; x = x 1 y; X = x y; / / ! x = x << 1; / / ! x = x >> 1; / / ! x = x >>> 1; / / Asignación compuesta: A

{


3: Controlar el flujo del programa

x += y; x -= y; x *= Y; x / = y; x %= y; x <<= 1; x >>= 1; / / ! x >>>= 1; x & = y; x Y; x l = y; / / Conversi贸n: ! char c = ( c h a r ) ~ ; / / ! byte B = by te)^; ! short S = (short)~; ! int i = (int)x; / / ! long 1 = (1ong)x; ! float f = (f1oat)x; / / ! double d = (double)~; //! //! //! //! //! //! //!

A=

1 void pruebalhar (char x, char y) / / Operadores aritm茅ticos: x = (char) (x * y) ; x = (char) (x / y) ; x = (char) (x % y) ; x = (char) (x + y) ; x = (char) (x - y) ; x++; x--; x = (char)ty; x = (char)-y; / / Relacionales y l贸gicos : f(x > y); f (x >= y); f(x < y); f (x <= y) ; f (x == y) ; f (x ! = y); ! f (!x); ! !

f(x

&&

y);

f(x l l y); / / Operadores de bit: x= (char)-y; x = (char)(x & y) ; x = (char)(x I y);

{

101


102

Piensa en Java

x = (char) (x x = (char)(x x = (char)(x x = (char)(x / / Asignaci贸n x += y; x -= Y; x *= Y; x /= y; X eo-y; x <<= 1; x >>= 1; x >>>= 1; x &= y; X

"=

y); << 1) ; >> 1) ; >>> 1); compuesta: A

Y;

x I = y; / / Conversi贸n: / / ! boolean b = (boolean)~; by te)^; byte B = short S = (short)~; int i = (int)x; long 1 = (long)x; float f = (f1oat)x; double d = (double)x;

1 void pruebaByte(byte x, byte y) / / Operadores aritm茅ticos: x = (byte)(x* y) ; x = (byte)(x / Y) ; x = (byte)(x % y) ; x = (byte)(x + y) ; x = (byte)(x - y) ; x+f; x--; x = (byte)+ y; x = (byte)- y; / / Relacionales y l贸gicos : f(x > y); f (x >= y) ; f(x < Y ) ; f (x <= y); f (x == y) ; f (x ! = y); ! f (!x); ! f(x & & y);

{


3: Controlar el flujo del programa

f(x l l y); / / Operadores de bit: x = (byte)-y; x = (byte)(x & y) ; x = (byte)(x I y); y) ; x = (byte)(x x = (byte)(x << 1); x = (byte)(x >> 1) ; x = (byte)(x >>> 1) ; / / Asignaci贸n compuesta: x += y; x -= y; X *= Y; !

A

x /=

y:

X

y;

Po-

x <<= 1; x >>= 1; x >>>= 1; x & = y; x " = Y; x I = y; / / Conversi贸n: / / ! boolean b = (boolean)~; char c = (char)x; short S = (short)~; int i = (int)x; long 1 = (1ong)x; float f = (float)x; double d = (double)~; void pruebashort (short x, short y) / / Operadores aritm茅ticos: x = (short)(x * y) ; x = (short)(x / y) ; x = (short)(x % y) ; x = (short)(x t y) ; x = (short)(x - y) ; x+t; x--; x = (shnrt)+y; x = (short)-y; / / Relacionales y l贸gicos : f(x > y); f (x y) ; f(x < y);

.=

{

103


104

Piensa en Java

f (x <= y); f (x == y); f (x ! = y); ! f (!x); ! f(x & & y); ! f (x I I y); / / Operadores de bit: x = (short)-y; x = (short)(x & y) ; x = (short) (x I y); y) ; x = (short) (x x = (short) (x << 1) ; x = (short) (x >> 1) ; x = (short) (x >>> 1) ; / / Asignaci贸n compuesta: x t = y; x -= y; x *= Y; x / = y; x Poy; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^ = y; x i = y; / / Conversi贸n: / / ! boolean b = (boolean)~; char c = ( c h a r ) ~ ; by te)^; byte B = int i = (int)x; long 1 = (1ong)x; float f = (f1oat)x; double d = (double)x; A

1 void pruebaInt (int x, int y) / / Operadores aritm茅ticos: X = x * y; x = x / y ; x = x % y ; x = x t y; X = X - y; x++; x--; x = +y;

{


3: Controlar el flujo del programa

x = -y; / / Relacionales y l贸gicos: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x ! = y); ! f(!x); ! f ( x & & y); ! f ( x l l y); / / Operadores de bit: x = -y; X

=

x

&

y:

x = x ) y; X = x y; x = x << 1; x = x >> 1; x = x >>> 1; / / Asignaci贸n compuesta: x += y; x -= Y; x *= Y; x / = y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x & = y; X Y; x I = y; / / Conversi贸n: / / ! boolean b = (boolean)~; char c = ( c h a r ) ~ ; byte B = by te)^; short S = (short)~; long 1 = (1ong)x; float f = (float)x; double d - (double)x; A

A=

1 void pruebalong (lorig x, l o r i y y) / / Operadores aritm茅ticos: x = x * y ; x = x / y ; X = x % y;

{

105


106

Piensa en Java

x = x + y ; X = x - y; x++; x--; x = +y; x = -y; / / Relacionales y l贸gicos : f(x > y); f (x >= y) ; f(x < y); f (x <= y) ; f (x == y) ; f(x ! = y); ! f (!x); /

f ( x

GG

y);

! f(x I I y ) ; / / Operadores de bit: x = -y; x = x & y ; x = x / y; X = x y; x = x << 1; x = x >> 1; x = x >>> 1; / / Asignaci贸n compuesta: x += y; x -= Y; x *= Y; x / = y; Soy; x <<= 1; x >>= 1; x >>>= 1; x &= y; X Y; x I = y; / / Conversi贸n: / / ! boolean b = (boolean)~; char c = ( c h a r ) ~ ; byte B = by te)^; short S = ( s h o r L )x ; int i = (int)x; f l o a t f = (f1oat)x; double d = (double)~; A

1


3: Controlar el flujo del programa

void pruebaFloat (float x, float y) / / Operadores aritm茅ticos: X = x * y; x = x / y ; X = x % y; x = x + y ; X = x - y; x++; x--; x = +y; x = -y; / / Relacionales y l贸gicos : f ( x > y); f (x

i=

y);

f(x < y); f (x <= y); f (x == y); f(x ! = y); ! f (!x); !

f ( x && y ) ;

/ f(x 1 1 y); / / Operadores de bit: / / ! x = -y; / / ! x = x & y; / / ! x = x 1 y; / / ! x = x " y; / / ! x = x << 1; / / ! x = x >> 1; / / ! x = x >>> 1; / / Asignaci贸n compuesta: x += y; x -= y; x *= Y; x /= y; Poy; / / ! x <<= 1; / / ! x >>= 1; / / ! x >>>= 1; / / ! x & = y; / / ! x ^= Y; / / ! x I = y; / / Conversi贸n: / / ! boolean b = (boolean)~; char c = (char)~; byte B = (byte)x;

{

107


108

Piensa en Java

short s = (shortlx; int i = (int)x; long 1 = (1ong)x; double d = (double)~;

1 void pruebaDouble (double x, double y) / / Operadores aritm茅ticos: X = x * y; x = x / y; X = x % y; x = x + y ; X = x - y; xt+; x--; x = +y;

x

=

-y;

/ / Relacionales y l贸gicos: f(x > y); f (x >= y) ; f(x < y); f (x <= y) ; f (x == y) ; f (x ! = y); ! f (!x); ! f(x & & y); ! f(x l l y); / / Operadores de bit: / / ! x = -y; / / ! x = x & y; / / ! x = x I y; //! x = x y; / / ! x = x << 1; / / ! x = x >> 1; / / ! x = x >>> 1; / / Asignaci贸n compuesta: x += y; x -= Y; x *= Y; x / = y; x %= y; / / ! x <<' 1; / / ! x >>= 1; / / ! x >>>= 1; / / ! x &= y; / / ! x ^ = y; A

{


3: Controlar el flujo del programa

109

x l = y; / / Conversión: / / ! boolean b = (boolean)x; char c = ( c h a r ) ~ ; by te)^; byte B = short S = (short)~; int i = (int)x; long 1 = (long)x; float f = (f1oat)x; //!

Fíjese que boolean es bastante limitado. Se le pueden asignar los valores true y false, y se puede comprobar su validez o falsedad, pero no se pueden sumar valores lógicos o llevar a cabo ningún otro tipo de operación sobre ellos.

En char, byte y short se puede ver el efecto de promoción con los operadores aritméticos. Cada operación aritmética que se haga con estos tipos genera como resultado un int, que debe ser explícitamente convertido para volver al tipo original (una conversión reductora que podría implicar pérdida de información) para volver a ser asignado a ese tipo. Con los valores int, sin embargo, no es necesaria ninguna conversión, porque todo es ya un int. Aunque no hay que relajarse pensando que todo está ya a salvo. Si se multiplican dos valores d e tipo int lo suficientemente grandes, se desbordará el resultado. Esto se demuestra en el siguiente ejemplo: / / : c03:Desbordamiento.java / / ;Sorpresa! Java permite desbordamientos. public class Desbordamiento { public static void main (String[] args) { int grande = Ox7fffffff; / / Valor entero máximo visualizar ("grande = " + grande) ; int mayor = grande * 4; visualizar ("mayor = l' t mayor) ; static void visualizar (String S) System.out .println (S);

{

1 1 ///:La salida de esto es: grande = 2147483647 mayor = -4

y no se recibe ningún error ni advertencia proveniente del cornpilador, ni excepciones en tiempo de ejecución. Java es bueno, pero no tanto.


110

Piensa en Java

La asignaciones compuestas no requieren conversiones para char, byte o short, incluso aunque estén llevando a cabo promociones que tienen los mismos resultados que los operadores aritméticos directos. Por otro lado, la falta de conversión, definitivamente, simplifica el código. Se puede ver que, con la excepción de boolean, cualquier tipo primitivo puede convertirse a otro tipo primitivo. De nuevo, debemos ser conscientes del efecto de la conversión reductora cuando se hace una conversión a un tipo menor. Si no, se podría perder información sin saberlo durante la conversión.

Control de ejecución Java utiliza todas las sentencias de control de ejecución de C, de forma que si se ha programado con C o C++, la mayoría de lo que se ha visto será familiar. La mayoría de los lenguajes procedurales tie-

nen algún tipo de sentencia de control, y casi siempre hay solapamiento entre lenguajes. En Java, las palabras clave incluyen if-else, while, do-while,for, y una sentencia de selección denominada switch. Java, sin embargo, no soporta el siempre perjudicial goto (lo que podría seguir siendo la manera más expeditiva de solventar cierto tipo de problemas). Todavía se puede hacer un salto del estilo del "goto", pero es mucho más limitado que un goto típico.

True y false Todas las sentencias condicionales utilizan la certeza o falsedad de una expresión de condición para determinar el cauce de ejecución. Un ejemplo de una expresión condicional es A == B. Ésta hace uso del operador condicional == para ver si el valor de A es equivalente al valor de B. La expresión devuelve true o false. Cualquiera de los operadores relacionales vistos anteriormente en este capítulo puede usarse para producir una sentencia condicional. Fíjese que Java no permite utilizar un número como un boolean, incluso aunque está permitido en C y C++ (donde todo lo distinto de cero es verdadero, y cero es falso). Si se quiere usar un valor que no sea lógico en una conducción lógica, como if(a), primero es necesario convertirlo a un valor boolean utilizando una expresión condicional, como if(a!=O).

La sentencia if-else es probablemente la manera más básica de controlar el flujo de un programa. El else es opcional, por lo que puede usarse if de dos formas: if (expresión condicional) sentencia

if (expresión condicional) sentencia else sentencia


3: Controlar el flujo del programa

111

La expresión condicional debe producir un resultado boolean. La sentencia equivale bien a una sentencia simple acabada en un punto y coma, o a una sentencia compuesta, que es un conjunto de sentencias simples encerradas entre llaves. Cada vez que se use la palabra sentencia, siempre implicará que ésta puede ser simple o compuesta. He aquí un método prueba( ) como ejemplo de if-else. Se trata de un método que indica si un número dicho en un acertijo es mayor, menor o equivalente al número solución: / / : cO3: IfElse. java public class IfElse { static int prueba(int intento, int solucion) int resultado = 0; if (intento > solucion) resultado

=

{

+l;

else if (intento < solucion) resultado = -1; else resultado = 0; / / Coincidir return resultado;

1 public static void main(String[] args) System.out .println (prueba (10, 5) ) ; System.out .println (prueba(5, 10) ) ; System.out .println (prueba(5, 5)) ;

{

1 1 ///:-

Es frecuente alinear el cuerpo de una sentencia de control de flujo, de forma que el lector pueda determinar fácilmente dónde empieza y dónde acaba.

return La palabra clave return tiene dos propósitos: especifica qué valor devolverá un método (si no tiene un valor de retorno void), y hace que el valor se devuelva inmediatamente. El método prueba( ) puede reescribirse para sacar ventaja de esto: / / : c03:IfElseZ.java public class IfElse2 { static int prueba (int intento, int solucionar) int resultado = 0; if (intento > solucionar) ret-iirn tl; else if (intento < solucionar) return -1; else return O; / / Coincidir

{


112

Piensa en Java

}

public static void main(String[l args) System.out .println (prueba(10, 5)) ; System.out .println (prueba(5, 10)) ; System.out .println (prueba(5, 5) ) ;

{

1 1 ///:-

No hay necesidad de else porque el método no continuará ejecutándose una vez que se ejecute el return.

Iteración Las sentencias while, do-while y for son para el control de bucles, y en ocasiones se clasifican como sentencias de iteración. Se repite una sentencia hasta que la expresión Condicional controladora se evalúe a falsa. La forma de un bucle while es: while (Expresión-Condicional) sentencia

La expresión condicional se evalúa al comienzo de cada interación del bucle, y de nuevo antes de cada iteración subsiguiente de la sentencia. He aquí un ejemplo sencillo que genera números aleatorios hasta que se dé una condición determinada: / / : c03:PruebaWhile.java / / Muestra el funcionamiento del bucle while. public class PruebaWhile { public static void main(String[] args) double r = 0; while(r < 0.99d) { r = Math.random() ; System.out.println(r);

{

1 1 1 ///:-

Este ejemplo usa el método estático random( ) de la biblioteca Math, que genera un valor double entre O y 1. (Incluye el O, pero no el 1.) La expresión condicional para el while dice "siga haciendo este bucle hasta que el número sea 0,99 o mayor". Cada vez que se ejecute este programa, se logrará un listado de números de distinto tamaño.

do-while La forma del do-while es


3: Controlar el flujo del programa

113

do sentencia while (Expresión condicional);

La única diferencia entre while y do-while es que la sentencia del do-while se ejecuta siempre, al menos, una vez, incluso aunque la expresión se evalúe como falsa la primera vez. En un while, si la condicional es falsa la primera vez, la sentencia no se ejecuta nunca. En la práctica, do-while es menos común que while.

for Un bucle for lleva a cabo la inicialización antes de la primera iteración. Después, lleva a cabo la comprobación condicional y, al final de cada iteración, hace algún tipo de "paso". La forma del bucle for es: for(inicia1ización; Expresión condicional; paso) sentencia

Cualquiera de las expresiones inicialización, expresión condicional o paso puede estar vacía. Dicha expresión se evalúa antes de cada iteración, y en cuanto el resultado sea falso, la ejecución continuará en la línea siguiente a la sentencia for. Al final de cada iteración se ejecuta paso. Los bucles for suelen utilizarse para crear contadores: / / : c03:ListaCaracteres.java / / Muestra el funcionamiento del bucle "for" listando / / todos los caracteres ASCII.

public class Listacaracteres { public static void main (String[] arqs) { for( char c = O; c < 128; c++) / / Limpiar pantalla ANSI if (C ! = 26 ) System.out.println( "valor: " + (int)c + '' caracter: " + c) ;

Fíjese en que la variable c está definida en el punto en que se usa, dentro de la expresión de control del bucle for, en vez de al principio del bloque delimitado por la llave de apertura. El ámbito de c es la expresión controlada por el for. Los lenguajes procedurales tradicionales como C requieren que todas las variables se definan al principio de un bloque, de forma que cuando el compilador cree un bloque, pueda asignar espacio para esas variables. En Java y C++ es posible diseminar las declaraciones de variables a lo largo del bloque, definiéndolas en el momento en que son necesarias. Esto permite un estilo de codificación más natural y hace que el código sea más fácil de entender.


114

Piensa en Java

Se puede definir múltiples variables dentro de una sentencia for, pero deben ser del mismo tipo: for(int i = O, j =l; i < 10 & & j i = 11; i++, j++) / * cuerpo del bucle for * /

La definición int de la sentencia for cubre tanto a i como a j. La habilidad de definir variables en expresiones de control se limita al bucle for. No se puede utilizar este enfoque con cualquiera de las otras sentencias de selección o iteración.

El o ~ e r a d o rcoma Anteriormente en este capítulo, dije que el operador coma (no el separador coma, que se usa para separar definiciones y parámetros de funciones) sólo tiene un uso en Java: en la expresión de control de un bucle for. Tanto en la inicialización como en las porciones de "paso" de las expresiones de control, se tiene determinado número de sentencias separadas por comas, y estas sentencias se evaluarán secuencialmente. El fragmento de bloque previo utiliza dicha capacidad. He aquí otro ejemplo: / / : c03:OperadorComa.java public class Operadorcoma { public static void main (String[] args) for(int i = 1, J = i + 10; i < 5; i++, j = i * 2 ) { System.out.println("i= " + i + " j= " + j);

{

He aquí la salida:

Se puede ver que tanto en la inicialización, como en las porciones de "paso" se evalúan las sentencias en orden secuencial. Además, la porción de inicialización puede tener cualquier número de definiciones de un tipo.

break y continue Dentro del cuerpo de cualquier sentencia de iteración también se puede controlar el flujo del bucle utilizando break y continue. Break sale del bucle sin ejecutar el resto de las sentencias del bucle. Continue detiene la ejecución de la iteración actual y vuelve al principio del bucle para comenzar la siguiente iteración. Este programa muestra ejemplos de break y continue dentro de bucles for y while:


3: Controlar el flujo del programa

115

/ / : c03:BreakYContinue.java / / Muestra el funcionamiento de las palabras clave break y continue. public class BreakYContinue { public static void main(String[] args) { for(int i = O; i < 100; itt) { if (i == 74) break; / / Sale del bucle for if (i % 9 ! = 0) continue; / / Siguiente iteración System-out.println (i); 1 int i = 0; / / Un "bucle infinito": while (true)

{

itt; int j = i * 27; if(j == 1269) break; / / Sale del bucle if (i % 10 ! = 0) continue; / / Parte superior del bucle System.out .println(i);

1 } }

///:-

En el bucle for el valor de i nunca llega a 100 porque la sentencia break rompe el bucle cuando i vale 74. Normalmente, el break sólo se utilizaría de esta manera si no se supiera cuándo va a darse la condición de terminación. La sentencia continue hace que la ejecución vuelva a la parte superior del bucle de iteración (incrementando por consiguiente la i) siempre que i no sea totalmente divisible por 9. Cuando lo es, se imprime el valor.

La segunda porción muestra un "bucle infinito" que debería, en teoría, continuar para siempre. Sin embargo, dentro del bucle hay una sentencia break que romperá el bucle y saldrá de él. Además, se verá que la sentencia continue vuelve a la parte de arriba del bucle sin completar el resto. (Por consiguiente la impresión se da en el segundo bucle sólo cuando el valor de i es divisible por 10.) La salida es:


116

Piensa en Java

El valor O se imprime porque O % 9 da O. Una segunda forma de hacer un bucle infinito es escribir for(;;). El compilador trata tanto a while (true), como a for(;;) de la misma manera, de forma que cualquiera que se use en cada caso, no es más que una cuestión de gusto.

El infame "goto" La palabra clave goto ha estado presente en los lenguajes de programación desde los comienzos. Sin duda, el goto era la génesis del control de los programas en el lenguaje ensamblador: "Si se da la condición A, entonces saltar aquí, sino, saltar ahí," Si se lee el código ensamblador generado al final por cualquier compilador, se verá que el control del programa contiene muchos saltos. Sin em-

bargo, un goto es un salto al nivel de código fuente, y eso es lo que le ha traído tan mala reputación. Si un programa siempre salta de un punto a otro, ¿no hay forma de reorganizarlo de manera que el flujo de control no dé tantos saltos? goto cayó en desgracia con la publicación del famoso artículo "El Goto considerado dañinon3,de Edsger Dijkstra, y desde entonces, la prohibición del goto ha sido un deporte popular, con los partidarios de la palabra clave repudiada buscando guarida.

Como es típico en situaciones como ésta, el terreno imparcial es el más fructífero. El problema no es el uso del goto, sino el uso excesivo de goto -en raras ocasiones el goto es de hecho la mejor manera de estructurar el flujo del programa. Aunque goto es una palabra reservada en Java, no se utiliza en el lenguaje; Java no tiene goto. Sin embargo, tiene algo que se parece un poco a un salto atado vinculado a las palabras clave break y continue. No es un salto sino más bien una forma de romper una sentencia de iteración. El motivo por el que aparece muy a menudo en discusiones relacionadas con el goto, es que utiliza el mismo mecanismo: una etiqueta. Una etiqueta es un identificador seguido de dos puntos, como ésta: etiquetal:

El único sitio en el que una etiqueta es útil en Java es justo antes de una sentencia de iteración. Y eso significa justo antes -no hace ningún bien poner cualquier otra sentencia entre la etiqueta y la iteración. Y la única razón para poner una etiqueta antes de una iteración es si se va a anidar otra iteración o un "switch" dentro. Eso es porque las palabras break y continue únicamente interrumpirán normalmente al bucle actual, pero cuando se usan con una etiqueta, interrumpirán a los bucles hasta donde exista la etiqueta: etiquetal: iteracion-externa

{

iteracion-interna

{

Nota del traductor: "Goto considered harrnful".


3: Controlar el flujo del programa

117

break; //1 //.. . continue; / / 2 / / . .. continue etiquetal; //3 //. .. break etiquetal; //4

1

1

En el caso 1, el break rompe la iteración interna, pasando a la iteración exterior. En el caso 2, el continue hace volver al principio de la iteración interna. Pero en el caso 3, el continue etiquetal

rompe tanto la iteración interna, como la externa, retrocediendo hasta etiquetal. Posteriormente, de hecho, continúa la iteración, pero empezando en la iteración exterior. En el caso 4, el break etiquetal también rompe el bucle haciendo volver hasta etiquetal, pero no vuelve a entrar en la iteración. De hecho, rompe ambas iteraciones. He aquí un ejemplo de utilización de bucles for: / / : c03:ForEtiquetado.java / / El bucle "for etiquetado' de Java. public class ForEtiquetado { public static void main(String[] args) { int i = 0; externo: / / Aquí no puede haber sentencias for (; true ; ) { / / bucle infinito interno: / / Aquí no puede haber sentencias for(; i < 10; i++) { visualizar ( " i = " + i) ; if (i == 2) { visualizar("continuar"); continue;

1 if (i == 3) I visualizar("sa1ir"); i++; / / En caso contrario i / / no se incrementa nunca. break; 1 if (i == 7 ) t visualizar ( "continuar el externo") ; i++; / / En caso contrario i / / no se incrementa nunca. continue externo;

1


118

Piensa en Java

if (i == 8) { visualizar ( "salir externo") break externo;

;

for(int k = O; k < 5 ; k++) if (k == 3) { prt ( "continuar el interno") ; continue interno;

{

1 J

1

1 / / Aquí no se puede hacer break o continue / / a etiquetas 1 static void visualizar (String S) { System.out .println(S); 1 1 ///:-

Este ejemplo usa el método visualizar( ) que ha sido definido en los otros ejemplos. Nótese que break sale del bucle for, y que la expresión de incremento no se da hasta acabar de pasar por el bucle for. Dado que break se salta la expresión e incremento, el incremento se da directamente en el caso de i==3.La sentencia continuar externo en el caso de i==7va también a la parte superior del bucle, y se salta también el incremento, por lo que también se incrementa directamente. He aquí la salida: i = O continuar i = 1 continuar i = 2 continuar i = 3 salir i = 4 continuar i = 5 continuar i = 6 continuar i = 7 continuar

el interno el interno

el interno el interno el interno

el externo


3: Controlar el flujo del programa

119

i = 8 salir externo

Si no fuera por la sentencia break externo, no habría manera de salir del bucle externo desde el bucle interno, dado que break, por sí misma puede romper únicamente el bucle más interno. (Y lo mismo ocurre con continue.) Por supuesto, en los casos en los que salir de un bucle implique también salir del método, uno puede usar simplemente un return. He aquí una demostración de sentencias etiquetadas break y continue con bucles while: //:

c03:WhileEtiquetado.java

/ / El bucle "while etiquetado" de Java. public class WhileEtiquetado { public static void main (String[] args) int i = 0; externo: while (true) { visualizar ("Bucle while externo") ; while(true) { i++; visualizar ("i = " + i) ; if (i == 1) t visualizar ( "continuar") ; cont inue ; }

if (i == 3 ) t visualizar("Continuar externo"); continue externo;

1 if (i == 5 ) I visualizar ( "salir") break;

;

1 if (i == 7 ) t visualizar ("break externo"); break externo; 1

1 1 static void visualizar (String S) System.out .println ( S ) ;

1 1 ///:-

{

{


120

Piensa en Java

Las mismas reglas son ciertas para while: 1.

Un continue sin más va hasta el comienzo del bucle más interno, y continúa.

2.

Un continue etiquetado va a la etiqueta, y vuelve a entrar en el bucle situado justo después de la etiqueta.

3.

Un break "abandona" el bucle.

4.

Un break etiquetado abandona el final del bucle marcado por la etiqueta.

La salida de este método lo deja claro: Bucle while externo i = l continuar i = 2 i = 3 continuar externo Bucle while externo i = 4 i = 5 salir Bucle while externo i = 6 i = 7 salir externo

Es importante recordar que la única razón para usar etiquetas en Java es cuando se tienen bucles anidados, y se quiere utilizar sentencias break o continue a través de más de un nivel de anidamiento. En el articulo "El goto considerado dañino" de Dijkstra, se ponen objeciones a las etiquetas, no al goto en sí. Dijkstra observó que el número de errores tiende a incrementarse con el número de etiquetas que haya en un programa. Las etiquetas y las sentencias goto hacen difícil el análisis estático, puesto que introducen ciclos en el grafo de ejecución de los programas. Fíjese que las etiquetas de Java no tienen este problema, pues están limitadas a su ubicación, y no pueden ser utilizadas para transferir el control de forma directa. También es interesante tener en cuenta que éste es el caso en el que una característica de un lenguaje se convierte en más interesante, simplemente restringiendo el poder de la propia sentencia.

switch La orden switch suele clasificarse como sentencia de selección. La sentencia switch selecciona de entre fragmentos de código basados en el valor de una expresión entera. Es de la forma: switch (selector-entero)

{

case valor-entero1 : sentencia; break;


3: Controlar el flujo del programa

case case case case

valor-entero2 : valor-entero3 : valor-entero4 : valor-entero5 : // ... default : sentencia;

sentencia; sentencia; sentencia; sentencia;

121

break; break; break; break;

1

El selector entero es una expresión que produce un valor entero. El switch compara el resultado de selector entero con cada valor entero. Si encuentra un valor que coincida, ejecuta la sentencia (simple o compuesta) correspondiente. Si no encuentra ninguna coincidencia, ejecuta la sentencia default. Observese en la definición anterior que cada case acaba con break, lo que causa que la ejecución salte al final del cuerpo de la sentencia switch. Ésta es la forma convencional de construir una sentencia switch, pero el break es opcional. Si no se pone, se ejecutará el código de las sentencias "case" siguientes, hasta encontrar un break. Aunque este comportamiento no suele ser el deseado, puede ser útil para un programador experimentado. Hay que tener en cuenta que la última sentencia, la que sigue a default, no tiene break porque la ejecución llega hasta donde le hubiera llevado el break. Se podría poner un break al final de la sentencia default sin que ello causara ningún daño, si alguien lo considerara importante por razones de estilo. La sentencia switch es una forma limpia de implementar una selección de múltiples caminos (por ejemplo, seleccionar un camino de entre cierto número de caminos de ejecución diferentes), pero requiere de un selector que se evalúe a un valor como int o char. Si se desea utilizar, por ejemplo, una cadena de caracteres o un número de coma flotante como selector, no se podrá utilizar una sentencia switch. En el caso de tipos no enteros, es necesario utilizar una serie de sentencias if.

He aquí un ejemplo que crea letras al azar y determina si se trata de vocales o consonantes: / / : c03:VocalesYConsonantes.java / / Demuestra el funcionamiento de la sentencia switch. public class VocalesYConsonantes { public static void main (String[] args) { for(int i = O; i < 100; i++) { char c = (char) (Math.random ( ) * 26 t 'a'); System.out .print (c t " : " ) ; switch(c) { case 'a': case 'e': case 'i': case 'o': case 'u': System. out .println ("vocal"); break; case 'y':


122

Piensa en Java

case 'w': System.out.println( "A veces una vocal") ; break; default:

Dado que Math.random( ) genera un valor entre O y 1, sólo es necesario multiplicarlo por el límite superior del rango de números que se desea producir (26 para las letras del alfabeto) y añadir un desplazamiento para establecer el límite inferior. Aunque aquí parece que se está haciendo un switch con un carácter, esta sentencia está usando, de hecho, el valor entero del carácter. Los caracteres entre comillas simples de las sentencias case también producen valores enteros que se usan para las comparaciones. Fijese cómo las sentencias case podrían "apilarse" unas sobre otras para proporcionar varias coincidencias para un fragmento de código particular. También habría que ser conscientes de que es esencial poner la sentencia break al final de un caso particular, de otra manera, el control simplemente irá descendiendo, pasando a ejecutar el case siguiente.

Detalles de cálculo La sentencia char c = (char) (Math.random ( ) * 26 +'a') ; merece una mirada más detallada. Math.random( ) produce un double, por lo que se convierte el valor 26 a double para llevar a cabo la multiplicación, que también produce un double. Esto significa que debe convertirse la 'a' a double para llevar a cabo la suma. El resultado double se vuelve a convertir en char con un molde. ¿Qué es lo que hace la conversión a char? Es decir, si se tiene el valor 29,7 y se convierte a char, {cómo se sabe si el valor resultante es 30 o 29? La respuesta a esta pregunta se puede ver en este ejemplo: / / : c03:ConvertirNumeros.java / / ¿Qué ocurre cuando se convierte un float / / o un double a un valor entero? public class ConvertirNumeros { public static void main(String[] args) double encima = 0.7, debajo = 0.4;

{


3: Controlar el flujo del programa

123

System.out .println ("encima: " + encima) ; System.out .println ("debajo: " + debajo) ; System.out.println( " (int)encima: " + (int)encima) ; System.out.println( " (int)debajo: " + (int)debajo) ; System.out.println( " (char)( ' a 1 + encima) : " + (char)( 'a ' + encima) ) ; System.out.println( " (char) ('a' + debajo) : " + (char) ('a' + debajo)); }

1 ///:-

La salida es: encima: 0.7 debajo: 0.4 (int)encima : (int)debajo: (char)( 'a' + (char)('a' +

O

O encima) debajo)

= =

a a

Por lo que la respuesta es que si se hace una conversión de un float o un double a un valor entero lo truncará. Hay una segunda cuestión que concierne a Math.random( ). ¿Produce un valor de cero a uno, incluyendo o excluyendo al valor 'l'?En el lingo matemático les (O, 1) o [O, 11, o (O, 11 o [O, l)?(El corchete significa "incluye" mientras que el paréntesis significa "excluye".) De nuevo, la solución la puede proporcionar un programa de prueba: / / : c03:LimitesAleatorios.java / / ¿Produce Math. random ( ) O. O y 1.O? public class LimitesAleatorios { static void uso() { System.out.println("Uti1izacion: \n\tV "LimitesAleatorios inferior\n\t1' + "LimitesAleatorios superior"); System.exit (1); }

public static void main (String [ ] if (args.length ! = 1) uso ( ) ; if (args[O] .equals ("inferior") ) while(Math.random() ! = 0.0) ; / / Seguir intentándolo

+


124

Piensa en Java

else if (args[O].equals ("superior")) { while (Math.random ( ) ! = 1.0) ; / / Seguir intentandolo System.out .println ("Produjo 1.O! ");

Para ejecutar el programa, se teclea una línea de comandos como: java LimitesAleatorios inferior

java LimitesAleatorios superior

En ambos casos nos vemos forzados a romper el programa manualmente, de forma que da la sensación de que Math.random( ) nunca produce ni 0,O ni 1,O. Pero éste es el punto en el que un experimento así puede defraudar. Si se considera4que hay al menos 262fracciones double distintas entre O y 1,la probabilidad de alcanzar cualquier valor experimentalmente podría superar el tiempo de vida de un computador o incluso el de la persona que realiza la prueba. Resulta que 0,O está incluido en la salida de Math.random( ). O, en el lingo de las matemáticas es [O, 1).

Resumen Este capítulo concluye el estudio de los aspectos fundamentales que aparecen en la mayoría de los lenguajes de programación: cálculo, precedencia de operadores, conversión de tipos, y selección e iteración. Ahora estamos listos para empezar a dar pasos y acercarse al mundo de la programación

' Chuck Allison escribe: "El número total de números en el sistema de números en coma flotante e s 2(M-m+l)bA(p-1)+1,donde b e s la base (generalmente 2), p e s la precisión (dígitos de la mantisa), M e s el exponente mayor, y m e s el exponente menor. IEEE 754 utiliza: M = 1023, m = -1022, p = 53, b = 2 por lo que el número total de números es

2(1023+1022+1)2Y52 = 2((2"10-1)+(2"10-1)2"52 = (2"lO-1)2"54 =2"64- 2"54 La mitad de estos números (los correspondientes a los exponentes del rango [-1022, 11 son menores a 1 en magnitud (tanto positivos como negativos), por lo que 1/4 de esa expresión, o 2"62 - 2"52+1 (aproximadamente 2"62) está en el rango [O, 1).Véase mi artículo en http://www.fieshsources.com/1995006.htm (final del texto).


3: Controlar el flujo del programa

125

orientada a objetos. El siguiente capítulo cubrirá los aspectos importantes de la inicialización y limpieza de objetos, seguido del esencial concepto de ocultación de información en el capítulo siguiente.

tjercicios Las soluciones a determinados ejercicios se encuentran en el documento The Thinking in Jaua Annotated Solution Guide, disponible a bajo coste en http://www.BruceEcke1.com.

Hay dos expresiones en la sección denominada "precedencia" de este capitulo. Poner estas expresiones en un programa y demostrar que producen resultados diferentes. Poner los métodos temario( ) y alternativo ( ) en un programa que funcione. Poner los métodos prueba( ) y prueba2( ) de las secciones "if-else" y "return" en un programa que funcione. Escribir un programa que imprima valores de 1 a 100. Modificar el Ejercicio 4, de forma que el programa exista utilizando la palabra clave break en el valor 47. Intentar hacerlo usando return en vez de break. Escribir una función que reciba como parámetros dos cadenas de texto, y use todas las comparaciones lógicas para comparar ambas cadenas e imprimir los resultados. Para el caso de == y !=, llevar a cabo también las pruebas de equals( ) . En main ( ) , llamar a la función con varios objetos String distintos. Escribir un programa que genere 25 valores enteros al azar. Para cada valor, utilizar una sentencia if-then-else para clasificarlo como mayor, menor o igual que un segundo valor generado al azar. Modificar el Ejercicio 7, de forma que el código esté dentro de un bucle while "infinito". Después se ejecutará hasta que se interrumpa desde el teclado (generalmente presionando Control-C). Escribir un programa que use dos bucles for anidados y el operador módulo (%)para detectar e imprimir números primos (números enteros que no son divisibles por otro número que no sean ellos mismos o 1). Crear una sentencia switch que escriba un mensaje en cada caso, e introducirla en un bucle for que pruebe cada caso. Poner un break después de cada caso y probarlo. A continuación, quitar las sentencias break y ver qué ocurre.


4: Inicialización y limpieza A medida que progresa la revolución computacional, la programación "insegura" se ha convertido en uno de los mayores culpables del encarecimiento de la programación. Dos de estos aspectos de seguridad son la inicialización y la limpieza. Muchos de los fallos que se dan en C ocurren cuando el programador olvida inicializar una variable. Esto es especialmente ha-

bitual con las bibliotecas, cuando los usuarios no saben cómo inicializar un componente de una biblioteca, o incluso cuándo deben hacerlo. La limpieza o eliminación es un problema especial porque es fácil olvidarse de un elemento una vez que ya no se utiliza, puesto que ya no tiene importancia.

Por consiguiente, los recursos que ese elemento utilizaba quedan reservados y es fácil acabar quedándose sin recursos (y el más importante, la memoria). C++ introdujo el concepto de constructor, un método especial invocado automáticamente en la creación de un objeto. Java también adoptó el constructor, y además tiene un recolector de basura que libera automáticamente recursos de memoria cuando dejan de ser utilizados. Este capítulo examina los aspectos de inicialización y eliminación, y su soporte en Java.

Inicialización garantizada constructor con Es posible imaginar la creación de un método denominado inicializar( ) para cada clase que se escriba. El nombre se debe invocar antes de utilizar el objeto. Por desgracia, esto significa que el usuario debe recordar llamar al método. En Java, el diseñador de cada clase puede garantizar que se inicialice cada objeto proporcionando un método especial llamado constructor. Si una clase tiene un constructor, Java llama automáticamente al constructor cuando se crea un objeto, antes de que los usuarios puedan siquiera pensar en poner sus manos en él. Por consiguiente, la inicialización queda garantizada. El siguiente reto es cómo llamar a este método. Hay dos aspectos. El primero es que cualquier nombre que se use podría colisionar con un nombre que nos gustaría utilizar como miembro en una clase. El segundo es que dado que el compilador es el responsable de invocar al constructor, debe saber siempre qué método invocar. La solución de C++ parece la mejor y más lógica, por lo que se utiliza también en Java: el nombre del constructor es el mismo que el nombre de la clase. Tiene sentido que un método así se invoque automáticamente en la inicialización. He aquí hay una clase con un constructor simple: / / : c04:ConstructorSimple.java / / Muestra de un constructor simple.


128

Piensa en Java

class Roca { Roca() { / / Éste es el constructor System.out.println("Creando Roca");

public class ConstructorSimple { public static void main (String[] args) for(int i = O; i < 10; itt) new Roca ( ) ;

{

1 1 ///:Ahora, al crear un objeto: new Roca ( )

;

se asigna almacenamiento y se invoca al constructor. Queda garantizado que el objeto será inicializado de manera adecuada antes de poder poner las manos sobre él. Fíjese que el estilo de codificación de hacer que la primera letra de todos los métodos sea minúscula no se aplica a los constructores, dado que el nombre del constructor debe coincidir exactamente con el nombre de la clase. Como cualquier método, el constructor puede tener parámetros para permitir especificar cómo se crea un objeto. El ejemplo de arriba puede cambiarse sencillamente de forma que el constructor reciba un argumento: / / : c04:ConstructorSimple2.java / / Los constructores pueden tener parámetros. class Roca2 { Roca2 (int i) { System.out.println( "Creando la roca numero " + i) ;

1 1 public class Coristr uctorSimple2 { public static void main(String[l for(int i = O; i < 10; i+t) new Roca2 (i):

args)

{


4: Inicialización y limpieza

129

Los parámetros del constructor proporcionan un medio para pasar parámetros a la inicialización de un objeto. Por ejemplo, si la clase Arbol tiene un constructor que toma un número entero que indica la altura del árbol, crearíamos un objeto Arbol como éste: Arbol a

=

new Arbol(l2); / / Un árbol de 12 metros

Si Arbol(int) es el único constructor, entonces el compilador no permitirá crear un objeto Arbol de ninguna otra forma. Los constructores eliminan un montón de problemas y simplifican la lectura del código. En el fragmento de código anterior, por ejemplo, no se verá ninguna llamada explícita a ningún método inicidizar( ) que esté conceptualmente separado, por definición. En Java, la definición e inicialización son conceptos que están unidos -no se puede tener uno sin el otro. El constructor es un tipo inusual de método porque no tiene valor de retorno. Esto es muy diferente al valor de retorno void, en el que el método no devuelve nada pero se sigue teniendo la opción de hacer que devuelva algo más. Los constructores no devuelven nada y no es necesario tener ninguna opción. Si hubiera un valor de retorno, y si se pudiera seleccionar el propio, el compilador, de alguna manera, necesitaría saber qué hacer con ese valor de retorno.

Sobrecarga

métodos

Uno de los aspectos más importantes de cualquier lenguaje de programación es el uso de los nombres. Al crear un objeto, se da un nombre a cierta región de almacenamiento. Un método es un nombre que se asigna a una acción. Al utilizar nombres para describir el sistema, se crea un programa más fácil de entender y de modificar por la gente. Es como escribir en prosa -la meta es comunicarse con los lectores. Para hacer referencia a objetos y métodos se usan nombres. Los nombres bien elegidos hacen más sencillo que todos entiendan un código. Surge un problema cuando se trata de establecer una correspondencia entre el concepto de matiz del lenguaje humano y un lenguaje de programación. A menudo, la misma palabra expresa varios significados -se ha sobrecargado. Esto es útil, especialmente cuando incluye diferencias triviales. Se dice "lava la camisa", "lava el coche" y "lava el perro". Sería estúpido tener que decir "IavaCamisas la camisa", "lavacoche el coche" y "lavaperro el perro" simplemente para que el que lo escuche no tenga necesidad de intentar distinguir entre las acciones que se llevan a cabo. La mayoría de los lenguajes humanos son redundantes, por lo que incluso aunque se te olviden unas pocas palabras, se sigue pudiendo entender. No son necesarios identificadores únicos -se puede deducir el significado del contexto. La mayoría de los lenguajes de programación (C en particular) exigen que se tenga un identiíicador único para cada función. Así, no se podría tener una función llamada print( ) para imprimir enteros si existe ya otra función llamada print( ) para imprimir decimales -cada función requiere un nombre único.


130

Piensa en Java

En Java ('y C++) otros factores fuerzan la sobrecarga de los nombres de método: el constructor. Dado que el nombre del constructor está predeterminado por el nombre de la clase, sólo puede haber un nombre de constructor. Pero ¿qué ocurre si se desea crear un objeto de más de una manera? Por ejemplo, suponga que se construye una clase que puede inicializarse a sí misma de manera estándar o leyendo información de un archivo. Se necesitan dos constructores, uno que no tome argumentos (el constructor por defecto, llamado también constructor sin parámetros), y otro que tome como parámetro un Stnng, que es el nombre del archivo con el cual inicializar el objeto. Ambos son constructores, por lo que deben tener el mismo nombre -el nombre de la clase. Por consiguiente, la sobrecarga de métodos es esencial para permitir que se use el mismo nombre de métodos con distintos tipos de parámetros. Y aunque la sobrecarga de métodos es una necesidad para los constructores, es bastante conveniente y se puede usar con cualquier método. He aquí un ejemplo que muestra métodos sobrecargados, tanto constructores como ordinarios: //:

c04:Sobrecarga.java

/ / Muestra de sobrecarga de métodos / / tanto constructores como ordinarios. import java.util.*; class Arbol { int altura; Arbol 0 visualizar ("Plantando un retoño") ; altura = 0; }

Arbol (int i) { visualizar("Creando un nuevo arbol que tiene " t i + metros de alto"); altura = i; 1 void info ( ) { visualizar("E1 arbol tiene " + altura t " metros de alto"); void info(String S) { visualizar(s + " : El arbol tiene " + altura t " metros de alto");

1 static void visualizar (String S) System.out.println ( S ) ; 1

{

public class sobrecarga { public static void main (String[] args)

{


4: Inicialización y limpieza

131

for(int i = O; i < 5; i++) { Arbol t = new Arbol (i); t.info(); t. info ("metodo sobrecargado") ;

1 / / Constructor sobrecargado: new Arbolo ;

1 1 ///:-

Se puede crear un objeto Arbol, bien como un retoño, sin argumentos, o como una planta que crece en un criadero, con una altura ya existente. Para dar soporte a esto, hay dos constructores, uno

que no toma argumentos (a los constructores sin argumentos se les llama constructores por defecto1), y uno que toma la altura existente. Podríamos también querer invocar al método ido( ) de más de una manera. Por ejemplo, con un parámetro String si se tiene un mensaje extra para imprimir, y sin él si no se tiene nada más que decir. Parecería extraño dar dos nombres separados a lo que es obviamente el mismo concepto. Afortunadamente, la sobrecarga de métodos permite usar el mismo nombre para ambos.

Distinguir métodos sobrecargados Si los métodos tienen el mismo nombre, ¿cómo puede saber Java qué método se debe usar en cada caso? Hay una regla simple: cada método sobrecargado debe tomar una única lista de tipos de parámetros. Si se piensa en esto por un segundo, tiene sentido: ¿de qué otra forma podría un programador distinguir entre dos métodos que tienen el mismo nombre si no fuera por los tipos de parámetros? Incluso las diferencias en el orden de los parámetros son suficientes para distinguir ambos métodos: (Aunque normalmente este enfoque no es necesario, pues produce un código difícil de mantener.) / / : c04:OrdenSobrecarga.java / / Sobrecarga basada en el / / orden de los parámetros.

public class Ordensobrecarga { static void print (String S, int i) System.out.println( "cadena: " t S t ", entero: " t i);

{

1

' En algunos documentos sobre Java de Sun, por el contrario, se refieren a éstos con el poco elegante pero descriptivo nombre de "constructores sin parámetros". El término "constructor por defecto" se ha usado durante muchos años, por lo que será el que utilizaremos.


132

Piensa en Java

static void print (int i, String S) System.out.println( "Entero: " + i + ", Cadera: " + S) ;

{

1

public static void main(String[] args) print ("Primero cadena", 11) ; print (99, "Primero entero") ;

{

1 1 ///:Ambos métodos print( ) tienen los mismos argumentos, pero distinto orden, y eso es lo que los

hace diferentes.

Sobrecarga con tipos primitivos Un tipo primitivo puede ser promocionado automáticamente de un tipo menor a otro mayor, y esto puede ser ligeramente confuso si se combina con la sobrecarga. El ejemplo siguiente demuestra lo que ocurre cuando se pasa un tipo primitivo a un método sobrecargado: / / : c04:SobrecargaPrimitivo.java / / Promoción de tipos primitivos y sobrecarga.

public class SobrecargaPrimitivo { / / los booleanos no pueden convertirse automáticamente static void visualizar (String S) { System.out .println (S); }

void void void void void void void

fl fl fl fl fl £1 fl

void void void void void void

f2 byte x) { visualizar ("f2(byte)" ) ; } f2 short x) { visualizar ("f2(short)" ) ; f2 (int x) { visualizar ("f2(int)") ; } f2 (long x) { visualizar ("f2(long)" ) ; } f2(float x) { visualizar("f2(float)"); f2 (double x) { visualizar ("£2 (double)" )

char x) { byte x) { short x) int X) { long x) { float x) double x)

void £3 (short x)

visualizar ("fl (char)" ) ; } visualizar ("£1(byte)" ) ; } { visualizar ("fl (short)" ) ; visualizar ("fl(int)" ) ; } visualizar ("fl (long)" ) ; } { visualizar ("f1 (float)" ) ; { visualizar ("f1 (double)" )

{

visualizar ("f3(short)" )

;

}

}

;

}

}

} ;

}

]


4: Inicialización y limpieza

void void void void

£3 (int x) { visualizar ("£3(int)" ) ; } f3 (long x) { visualizar ("£3(long)" ) ; } f3 (float x) { visualizar ("f3(float)" ) ; f3 (double x) { visualizar ("f3(double)" )

void void void void

f4 (int x) { visualizar ("f4(int)") ; } f4 (long x) { visualizar ("f4(long)" ) ; } f4 (float x) { visualizar ("f4 (float) " ) ; } f4 (double x) { visualizar ("f4(double)" ) ; }

}

; }

void £5 (long x) { visualizar ("f5(long)" ) ; } void f5 (float x) { visualizar ("f5(float)" ) ; void f5 (double x) { visualizar ("f5 (double)" )

;

void f6 (float x) void f6 (double x)

;

}

visualizar ("f7(double)" ) ;

}

void f7 (double x)

{ { {

visualizar ("f6 (float)" ) ; visualizar ("f6 (double)" )

void pruebaValoresConstante ( ) { visualizar ("Probando con el 5") ; fl(5) ;f2 (5);f3(5);f4 (5);f5(5); f 7 ( 5 ) ;

1 void pruebalhar ( ) { char x = 'x'; visualizar ("parametro char: " ) ; f1 (x);f2 (x);f3 (x);f4 (x);f5 (x);f 7 ( x )

;

1 void pruebaByte ( ) { byte x = 0; visualizar ("parametro byte : "); fl (x);f2 (x);f3 (x);f4 (x);f5(x);f6(x);f7 (x); 1 void pruebashort ( ) { short x = 0; visualizar("parametro short:"); fl (x);f2 (x);f3 (x);f4 (x);£5 (x);f 7 ( x ) ; }

void pruebaInt ( ) { int x = 0; visualizar ("parametro int: "); fl (x);f2 (x);f3 (x);f4 (x);f5 (x);f6 (x);f7 (x); 1 void pruebalong ( ) { long x = 0;

} }

}

133


134

Piensa en Java

visualizar ("parametro long: " ) ; fl (x);f2 (x);f3 (x);f4 (x);f5 (x);f 7 ( x )

;

1 void pruebaFloat ( ) { float x = 0; visualizar("parametro float:"); fl (x);f2 (x);f3 (x);f4 (x);f5 (x);f 7 ( x )

;

}

void pruebaDouble ( )

double x

=

{

0;

visualizar("parametro double:");

fl (x);f2 (x);f3 (x);f4 (x);f5 (x);f 7 ( x )

1

1 public static void main (String[] args) SobrecargaPrimitivo p = new SobrecargaPrimitivo();

; {

Si se observa la salida de este programa, se verá que el valor constante 5 se trata como un int, de forma que si hay disponible un método sobrecargado que tome un int, será el utilizado. En todos los demás casos, si se tiene un tipo de datos menor al parámetro del método, ese tipo de dato será promocionado. Un dato de tipo char produce un efecto ligeramente diferente, pues si no encuentra una coincidencia exacta de char, se promociona a int. ¿Qué ocurre si el parámetro es mayor que el que espera el método sobrecargado? La respuesta la proporciona una modificación del programa anterior: / / : c04:Degradacion.java / / Degradación de tipos primitivos y sobrecarga.

public class Degradacion { static void visualizar (String System.out.println (S);

void fl (char x) void fl (byte x)

{ {

S)

{

visualizar ("fl (char)" ) visualizar ("fl(byte)" )

; } ;

}


4: Inicialización y limpieza

void void void void void

f1 (short x) { visualizar ("f1 (short)" ) ; f1 (int X) { visualizar ("f1 (int)"); } f1 (long x) { visualizar ("f1 (long)" ) ; } fl(f1oat x) { visualizar("fl(float)"); f1 (double x) { visualizar ("f1 (double)" )

void void void void void void

f2 (char x) { visualizar ("f2(char)" ) ; } f2 (byte x) { visualizar ("f2(byte)" ) ; } f2 (short x) { visualizar ("f2(short)" ) ; f2 (int x) { visualizar ("f2 (int)") : } f2 (long x) { visualizar ("f2(long)" ) ; } f2 (float x) { visualizar ("f2(float)" ) ;

void void void void void

f3 (char x) { visualizar ("f3(char)" ) ; } £3 (byte x) { visualizar ("£3(byte)"); } f3 (short x) { visualizar ("f3(short)" ) ; £3 (int x) { visualizar ("£3(int)" ) ; } £3 (long x) { visualizar ("£3(long)" ) ; }

void void void void

£4 (char x) { visualizar ("f4 (char)" ) ; f4 (byte x) { visualizar ("f4(byte)" ) ; f4 short x) { visualizar ("f4(short)" ) f4 int X) { visualizar ("f4(int)" ) ; }

;

void £ 6 char x) void f6 byte x)

{

visualizar ("f6(char)" ) visualizar ("f6(byte)" )

; }

{

void f7 char x)

{

visualizar ("f7(char)" )

; }

{

1

}

}

}

} } ;

; }

void pruebaDouble ( ) { double x = 0; visualizar ("parametro double : " ) ; fl(x);f2((float)x) ;£3((1ong)x);£4((int)x); f5( (short)~) ;f6 ( by te)^) ;f7 ( ( c h a r ) ~;) 1 public static void main(String[] args) { Degradacion p = new Degradacion ( ) ; p.pruebaDouble ( ) ;

1 ///:-

}

}

{

visualizar ("f5(char)" ) ; visualizar ("f5(byte)" ) ; visualizar ("f5 (short)" )

} ;

}

void f5 char x) void £5 byte x) void f5 short x)

{

}

}

}

135


136

Piensa en Java

Aquí, los métodos toman valores primitivos de menor tamaño. Si el parámetro es de mayor tamaño es necesario convertir el parámetro al tipo necesario poniendo entre paréntesis el nombre del tipo. Si no se hace esto, el compilador mostrará un mensaje de error. Uno debería ser consciente de que ésta es una conversión reductora, que significa que podría conllevar una pérdida de información durante la conversión. Éste es el motivo por el que el compilador obliga a hacerlo -para marcar la conversión reductora.

Sobrecarga en los valores de retorno Es común preguntarse ''¿Por qué sólo los nombres de las clases y las listas de parámetros de los mé-

todos? ¿Por qué no distinguir entre métodos basados en sus valores de retorno?" Por ejemplo, estos dos métodos, que tienen el mismo nombre y parámetros, se distinguen fácilmente el uno del otro:

Esto funciona bien cuando el compilador puede determinar de manera inequívoca el significado a partir del contexto, como en int x = f( ). Sin embargo, se puede llamar a un método e ignorar el valor de retorno; a esto se le suele llamar invocar a un método por su efecto lateral, dado que no hay que tener cuidado sobre el valor de retorno y sí desear los otros efectos de la llamada al método. Por tanto, si se llama al método de la siguiente manera:

¿cómo puede determinar Java qué f( ) invocar? ¿Y cómo podría alguien más que lea el código verlo también? Debido a este tipo de problemas, no se pueden usar los tipos de valores de retorno para distinguir los métodos sobrecargados.

Constructores por defecto Como se mencionó anteriormente, un constructor por defecto (un constructor sin parámetros) es aquél que no tiene parámetros, y se utiliza para crear un "objeto básico". Si se crea una clase que no tiene constructores, el compilador siempre creará un constructor por defecto. Por ejemplo:

class Pajaro int i;

{

public class ConstructorPorDefecto { public static void main (String[] args) { Pajaro nc = new Pajaro(); / / ¡por defecto! 1 1 ///:-


4: Inicialización y limpieza

137

La línea new Pajaro ( )

;

crea un objeto nuevo e invoca a la función constructor, incluso aunque ésta no se haya definido explícitamente. Sin ella no habría ningún método a invocar para construir el objeto. Sin embargo, si se define algún constructor (con o sin parámetros) el compilador no creará uno automáticamente: class Arbusto

{

Arbusto ( i n t i) Arbusto

{}

(double d)

{ }

1 Ahora, si se escribe

1

new Arbusto() ;

el compilador se quejará por no poder encontrar un constructor que coincida. Es como si no se pusiera ningún constructor, y el compilador dice "Debes necesitar algún constructor, por lo que crearé uno". Pero si escribes un constructor, el compilador dice "Has escrito un constructor por lo que ya sabes lo que estás haciendo; si no hiciste un constructor por defecto es porque no lo necesitabas".

palabra clave t h i s Si se tiene dos objetos del mismo tipo llamados a y b, nos pondríamos preguntar cómo es que se puede invocar a un método f( ) para ambos objetos: class Platano { void f (int i) { / * . . . * / } Platano a = new Platano(), b = new Platano(); a.f (1);

}

Si sólo hay un método llamado f( ), ¿cómo puede este método saber si está siendo invocado para el objeto a o b? Para permitir la escritura de código con una sintaxis adecuada orientada a objetos en la que "se envía un menaje a un objeto", el compilador se encarga del código clandestino. Hay un primer parámetro secreto que se pasa al método f( ), y ese parámetro es la referencia al objeto que está siendo manipulado. Por tanto, las dos llamadas a método anteriores, se convierten en algo parecido a: Platano. f (a,1) ; Platano. f (b,2) ;

Esto es interno y uno no puede escribir estas expresiones y hacer que el compilador las acepte, pero da una idea de lo que está ocurriendo.


138

Piensa en Java

Supóngase que uno está dentro de un método y que desea conseguir la referencia al objeto actual. Dado que esa referencia se pasa de forma secreta al compilador, no hay un identificador para él. Sin embargo, para este propósito hay una palabra clave: this. Esta palabra clave -que puede usarse sólo dentro de un método- produce la referencia al objeto por el que se ha invocado al método. Uno puede tratar esta referencia como cualquier otra referencia a un objeto. Hay que recordar que si se está invocando a un método de una clase desde dentro de un método de esa misma clase, no es necesario utilizar this; uno puede simplemente invocar al método. Por consiguiente, se puede decir: Class Albaricoque{ void tomar() { / * . . . * / } void deshuesaro { tomar(); / *

...

*/)

1

Dentro de deshuesar( ), uno podrzá decir this.tomar( ), pero no hay ninguna necesidad. El compilador lo hace automáticamente. La palabra clave this sólo se usa para aquellos casos especiales en los que es necesario utilizar explícitamente la referencia al objeto actual. Por ejemplo, se usa a menudo en sentencias return cuando se desea devolver la referencia al objeto actual: //:

c04:Hoja.java / / Utilización simple de la palabra clave "this". public class Hoja { int i = 0; Hoja incrementar ( ) i++; return this;

{

}

void print ( ) { System.out.println("i

=

" + i);

1 public static void main (String[] args) { Hoja x = new Hoja(); x. incrementar ( ) .incrementar ( ) .incrementar ( ) .print ( ) 1 1 ///:-

;

Dado que incrementar( ) devuelve la referencia al objeto actual, a través de la palabra clave this, pueden ejecutarse múltiples operaciones con el mismo objeto.

Invocando a constructores desde constructores Cuando se escriben varios constructores para una clase, hay veces en las que uno quisiera invocar a un constructor desde otro para evitar la duplicación de código. Esto se puede lograr utilizando la palabra clave this.


139

4: Inicialización y limpieza

Normalmente, cuando se dice this, tiene el sentido de "este objeto" o "el objeto actual", y por sí mismo produce la referencia al objeto actual. En un constructor, la palabra clave this toma un significado diferente cuando se le da una lista de parámetros: hace una llamada explícita al constructor que coincida con la lista de parámetros. Por consiguiente, hay una manera directa de llamar a otros constructores: / / : c04:Flor.java / / Invocación a constructores con "this".

public class Flor int numeropetalos = 0; String S = new String("nullW); Flor (int petalos) { numeroPetalos = petalos; System.out.println( "Constructor w/ parametro entero solo, Numero de petalos + numeroPetalos) ;

1 Flor(String SS) { System.out.println( "Constructor w/ parametro cadera solo, S=" + SS); S = SS; 1 Flor(String S, int petalos) { this (petalos); //! this(s); / / ¡No se puede invocar dos! this.s = S; / / Otro uso de "this" System.out.println("cadena y entero Parámetros"); 1 Flor 0 I this ("Hola", 47) ; System.out.println( "constructor por defecto (sin parametros) " ) ; J

void print ( ) { //! this(l1); / / ;No dentro de un no-constructor! System.out.println( "Numero de Petalos = " + numeroPetalos + " S public static void main (String[] args) Flor x = new Flor ( ) ; x.print ( ) ;

{

=

"t

S);

=

"


140

Piensa en Java

El constructor Flor(String S, int petalos) muestra quese puede invocar a un constructor utilizando this, pero no a dos. Además, la llamada al constructor debe ser la primera cosa que se haga o se obtendrá un mensaje de error del compilador. El ejemplo también muestra otra manera de ver el uso de this. Dado que el nombre del parámetro s y el nombre del atributo S son el mismo, hay cierta ambigüedad. Se puede resolver diciendo this.s para referirse al dato miembro. A menudo se verá esta forma en código Java, que también se usa en muchas partes de este libro. En print( ) se puede ver que el compilador no permite invocar a un constructor desde dentro de otro método que no sea un constructor.

El significado de estático (static) Teniendo en cuenta la palabra clave this, uno puede comprender completamente qué significa hacer un método estático. Significa que no hay un this para ese método en particular. No se puede invocar a métodos no estático desde dentro de métodos estáticos"aunque al revés sí que es posible), y se puede invocar al método estático de la propia clase, sin objetos. De hecho, esto es principalmente el fin de un método estático. Es como si se estuviera creando el equivalente a una función global (de C). La diferencia es que las funciones globales están prohibidas en Java, y poner un método estático dentro de una clase permite que ésta acceda a otros métodos estáticos y a campos estáticos. Hay quien discute que los métodos estáticos no son orientados a objetos, puesto que tienen la semántica de una función global; con un método estático no se envía un mensaje a un objeto, puesto que no hay this. Esto probablemente es un argumento justo, y si uno acaba usando un montón de métodos estáticos, seguro que tendrá que replantearse su estrategia. Sin embargo, los métodos estáticos son pragmáticos y hay veces en las que son genuinamente necesarios, por lo que el hecho de que sean o no "PO0 pura" se deja para los teóricos. Si duda, incluso Smalltalk tiene un equivalente en sus "métodos de clase ".

Limpieza: finalización y recolección de basura Los programadores conocen la importancia de la inicialización, pero a menudo se les olvida la importancia de la limpieza. Después de todo, ¿quién necesita eliminar un int? Pero con las bibliotecas, dejar que un objeto simplemente "se vaya" una vez que se ha acabado con él, no es siempre seguro. Por supuesto, Java tiene el recolector de basura para recuperar la memoria de los objetos que ya no se usan. Considere ahora un caso muy inusual. Supóngase que los objetos asignan memoria "especial" sin utilizar new. El recolector de basura sólo sabe liberar la memoria asignada con new, por lo que ahora no sabrá cómo liberar esa memoria "especial" del objeto. Para hacer frente a este caso, Java proporciona un método denominado finalize( ) que se puede definir en cada clase. He aquí cómo se supone que funciona. Cuando el recolector de basura está preparado para liberar el espacio de almacenamiento utiEl único caso en el que esto podría ocurrir e s si se pasa una referencia a un objeto dentro del método estático. Después, a través de la referencia (que ahora es this) se puede invocar a métodos no estáticos y acceder a campos no estáticos. Pero generalmente si se desea hacer algo así, simplemente se hará un método ordinario no estático.


4: Inicialización y limpieza

141

lizado por el objeto, primero invocará a ñnalize( ), y sólo recuperará la memoria del objeto durante la pasada del recolector de basura. Por tanto, si se elige usar finalize( ), éste te proporciona la habilidad de llevar a cabo alguna limpieza importante a la vez que la recolección de basura. Éste es un error potencial de programación porque algunos programadores, especialmente los de C++,podrían confundir finalhe( ) con el destructor de C++, que es una función que siempre se invoca cuando se destruye un objeto. Pero es importante distinguir entre C++ y Java en este caso, pues en C++ los objetos siempre se destruyen (en un programa sin errores), mientras que los objetos de Java no siempre son eliminados por el recolector. 0, dicho de otra forma:

La recolección de basura no es destrucción Si se recuerda esto, se evitarán los problemas. Lo que significa es que si hay alguna actividad que debe llevarse a cabo antes de que un objeto deje de ser necesario, hay que llevar a cabo esa actividad por uno mismo. Java no tiene un destructor o un concepto similar, por lo que hay que crear un método ordinario para hacer esta limpieza. Por ejemplo, supóngase que en el proceso de creación de un objeto, éste se dibuja a sí mismo en la pantalla. Si no se borra explícitamente esta imagen de la pantalla, podría ser que éste no se elimine nunca. Si se pone algún tipo de funcionalidad eliminadora dentro de finahe( ), si un objeto es eliminado por el recolector de basura, la imagen será eliminada en primer lugar de la pantalla, pero si no lo es, la imagen permanecerá. Por tanto, un segundo punto a recordar es:

Los objetos podrían no ser eliminados por el recolector de basura Uno podría averiguar que el espacio de almacenamiento de un objeto nunca se libera porque el programa nunca llega a quedarse sin espacio de almacenamiento. Si el programa se completa y el recolector de basura nunca llega a ejecutarse para liberar el espacio de almacenamiento de ningún objeto, éste será devuelto por completo al sistema operativo en el momento en que acaba el programa. Esto es bueno, porque el recolector de basura tiene algo de sobrecarga, y si nunca se ejecuta, no hay que incurrir en ese gasto.

¿Para qué sirve finalize( )? Uno podría pensar en este punto que no deberíamos utilizar finalhe( ) como un método de limpieza de propósito general. ¿Cómo de bueno es? Un tercer punto para recordar es:

La recolección de basura sólo tiene que ver con la memoria Es decir, la única razón para la existencia de un recolector de basura, es recuperar la memoria que un programa ha dejado de utilizar. Por tanto, cualquier actividad asociada a la recolección de basura, especialmente el método finalbe( ) debe estar relacionada también sólo con la memoria y su desasignación.


142

Piensa en Java

¿Significa esto que si un objeto contiene otros objetos finalice( ) debería liberar explícitamente esos objetos? Pues no -el recolector de basura cuida de la liberación de toda la memoria de los objetos independientemente de cómo se creará el objeto. Resulta que la necesidad de finalice( ) se limita a casos especiales, en los que un objeto puede reservar espacio de almacenamiento de forma distinta a la creación de un objeto. Pero, podríamos pensar: en Java todo es un objeto, así que ¿cómo puede ser? Parecería que finalice( ) tiene sentido debido a la posibilidad de que se haga algo de estilo C, asignando memoria utilizando un mecanismo distinto al normal de Java. Esto puede ocurrir principalmente a través de métodos nativos, que son la forma de invocar a código no-Java desde Java. (Los métodos nativos se discuten en el Apéndice B.) C y C++ son los únicos lenguajes actualmente soportados por los métodos nativos, pues dado que pueden llamar a subprogramas escritos en otros lenguajes, pueden efectivamente invocar a cualquier cosa. Dentro del código no-Java, se podría invocar a la familia de funciones de malloc( ) de C para asignar espacio de almacenamiento, provocando una pérdida de memoria. Por supuesto, free( ) es una función de C y C++, por lo que sería necesario invocarla en un método nativo desde el finalice( ). Después de leer esto, probablemente se tendrá la idea de que no se usará mucho finalice( ). Es correcto: no es el sitio habitual para que ocurra una limpieza normal. Por tanto, ¿dónde debería llevarse a cabo la limpieza normal?

Hay que llevar a cabo la limpieza Para eliminar un objeto, el usuario debe llamar a un método de limpieza en el punto en el que se desee. Esto suena bastante directo, pero colisiona un poco con el concepto de destructor de C++. En este lenguaje, se destruyen todos los objetos. O mejor dicho, deberian eliminarse todos los objetos. Si se crea el objeto C++ como local (por ejemplo, en la pila -lo cual no es posible en Java), la destrucción se da al cerrar la llave del ámbito en el que se ha creado el objeto. Si el objeto se creó usando new (como en Java) se llama al destructor cuando el programador llame al operador delete de C++ (que no existe en Java). Si el programador de C++ olvida invocar a delete, no se llama nunca al destructor, y se tiene un fallo de memoria, y además las otras partes del objeto no se borran nunca. Este tipo de fallo suele ser muy difícil de localizar. Por el contrario, Java no permite crear objetos locales -siempre hay que usar new. Pero en Java, no hay un "eliminar" al que invocar para liberar el objeto, dado que el recolector de basura se encarga de liberar el espacio de almacenamiento. Por tanto, desde un punto de vista simplista, se podría decir que por culpa del recolector de basura, Java no tiene destructor. Se verá a medida que se vaya avanzando en el libro, que la presencia de un recolector de basura no elimina la necesidad de, o la utilidad de los destructores (y no se debería invocar a finabe( ) directamente, pues ésta no es la solución más adecuada). Si se desea llevar a cabo algún tipo de limpieza distinta a la liberación de espacio de almacenamiento, hay que segui~llamando explícitamente al método apropiado en Java, que es el equivalente al destructor de C++ , sea o no lo más conveniente. Una de las cosas para las que puede ser útil finalbe( ) es para observar el proceso de recolección de basura. El ejemplo siguiente resume las descripciones anteriores del recolector de basura:


4: Inicializaci贸n y limpieza

143

/ / : c04:Basura.java / / Demostraci贸n de recolector de / / basura y finalizaci贸n class Silla { static boolean ejecrecol = false; static boolean f = false; static int creadas = 0; static int finalizadas = 0; int i; Silla() { i = ++creadas; if (creadas == 47) Systerri.out .println ("Creadas 47") ;

1 public void finalize() { if(!ejecrecol) { / / La primera vez se invoca a finalize() : ejecrecol = true; System.out.println( "Comenzando a finalizar tras haber creado " creadas + " sillas");

+

1 if (i == 47) { System.out.println( "Finalizando la silla #47, " + "Poniendo el indicada que evita la creacion de mas sillas"); f = true;

1 finalizadas++; if(fina1izadas >= creadas) System.out.println( "Las " + finalizadas + " han sido finalizadas");

1 public class Basura { public static void main (String[] args) { / / Mientras no se haya puesto el flag, / / hacer sillas y cadenas de texto: while ( ! Silla.f) { new Silla() ; new String ("Coger espacio") ;

1 System.out.println( "Despues de haber creado todas las sillas:\nW

+


144

Piensa en Java

"creadas en total

=

" + Silla. creadas +

", finalizadas total

= " + Silla. finalizadas) ; / / Parámetros opcionales fueran la recolección / / de basura y finalización: if (args.length > O) t if (args[O].equals ("rec") 1 args [O].equals ("todo")) { System.out .println ("gc( ) : " ) ; System.gc O ;

}

if (args[O] .equals ("finalizar") 1 1 args[O] .equals("todo")) { System.out.println("runFinalization():"); System.runFinalization();

1 1 System.out .println ("adios!") ;

1 1 ///:El programa anterior crea muchos objetos Silla, y en cierto momento después de que el recolector de basura comience a ejecutarse, el programa deja de crear objetos de tipo Silla. Dado que el recolector de basura puede ejecutarse en cualquier momento, uno no sabe exactamente cuando empezará, y hay un indicador denominado ejecrecol para indicar si el recolector de basura ha comenzado ya su ejecución o no. Un segundo indicador f es la forma de que Silla le comunique al bucle main( ) que debería dejar de hacer objetos. Ambos indicadores se ponen dentro de finalize( ), que se invoca durante la recolección de basura. Otras dos variables estáticas, creadas y finalizadas, mantienen el seguimiento del número de objetos de tipo Silla creadas frente al número de finalizadas por el recolector de basura. Finalmente, cada Silla tiene su propio (no estático) int i, por lo que se hace un seguimiento de qué número es. Cuando finalice la Silla número 47, el indicador se pone a true para detener el proceso de creación de objetos de tipo Silla. Todo esto ocurre en el método main( ), en el bucle while (!Silla.f) { new Silla( ) ; new String ("Coger espacio") ; }

Uno podría pregiintarse cómo conseguir finalizar este bucle, dado que no hay nada dentro del bucle que cambie el valor de Si1la.f. Sin embargo, el proceso finalhe( ) se supone que lo hará cuando finalice el número 47.

La creación de un objeto String en cada iteración es simplemente la asignación de almacenamiento extra para animar al recolector de basura a actuar, lo que hará cuando se empiece a poner nervioso por la cantidad de memoria disponible.


4: Inicialización y limpieza

145

Cuando se ejecute el programa, se proporciona un parámetro de línea de comandos que pueden ser "rec", "finalizar" o "todo". El argumento "rec" invocará al método System.gc( ) (para forzar la ejecución del recolector d e b a s u r a ) . La utilización del parámetro "finalizar" invoca a System.runFinalization( ) que -en teoría- hará que finalicen los objetos que no lo hayan hecho. Y "todo" hace que se llame a los dos métodos. El comportamiento de este programa y de la versión de la primera edición de este libro muestra que todo lo relacionado con el recolector de basura y la finalización ha evolucionado, habiendo ocurrido mucha de esta evolución detrás del telón. De hecho, para cuando se lea esto, puede que el comportamiento del programa haya vuelto a cambiar. Si se invoca a System.gc( ), se finalizan todos los objetos. Esto no era necesario en el caso de las implementaciones previas del JDK, aunque la documentación decía otra cosa. Además, se verá que no parece haber ninguna diferencia si se invoca o no a System.runFinalization( ). Sin embargo, se verá que sólo si se invoca a System.gc( ) después de crear y descartar todos los objetos se invocará a todos los finalizadores. Si no se invoca a System.gc( ), entonces sólo se finalizan algunos de los objetos. En Java 1.1, se introdujo un método System.runFinalizersOnExit( ) que hacía que los programas ejecutaran todos los finalizadores al salir, pero el diseño resultó tener errores y se desechó el método. Esto puede ser otro de los motivos por los que los diseñadores de Java siguen dándole vueltas al problema de la recolección de basura y la finalización. Esperamos que este asunto se 'termine de resolverse en Java 2. El programa precedente muestra que la promesa de que todos los finalizadores se ejecuten siempre es verdadera, pero sólo uno fuerza explícitamente el que suceda. Si no se fuerza la invocación a System.gc( ), se logra una salida como: Creadas 47 Comenzando a finalizar tras haber creado 3486 Finalizando la silla #47 Poniendo el indicador que evita la creacion de mas sillas Despues de haber creado todas las sillas: total creadas = 3881, total finalizadas = 2684 adios !

Por consiguiente, no se invoca a todos los finalizadores cuando acaba el programa. Si se llama a System.gc( ), acabará y destruirá todos los objetos que no estén en uso en ese momento. Recuérdese que ni el recolector de basura ni la finalización están garantizadas. Si la Máquina Virtual Java m)no está a punto de quedarse sin memoria, entonces (sabiamente) no malgastará tiempo en recuperar memoria mediante el recolector de basura.

La condición de muerto En general, no se puede confiar cn quc sc invoque a finalize( ), y es necesario crear funciones de "limpieza" aparte e invocarlas explícitamente. Por tanto, parece que finalize( ) solamente es útil para limpiezas oscuras de memoria que la mayoría de programadores nunca usarán. Sin embargo,


146

Piensa en Java

hay un uso muy interesante de finalize( ) que no confía en ser invocada siempre. Se trata de la verificación de la condición de muerte3 de un objeto. En el momento en que uno deja de estar interesado en un objeto -cuando está listo para ser eliminado- el objeto debería estar en cierto estado en el que su memoria pueda ser liberada de manera segura. Por ejemplo, si el objeto representa un fichero abierto, ese fichero debería ser cerrado por el programador antes de que el objeto sea eliminado por el recolector de basura. Si no se eliminan correctamente ciertas porciones del objeto, se tendrá un fallo en el programa que podría ser difícil de encontrar. El valor de finalbe( ) es que puede usarse para descubrir esta condición, incluso si no se invoca siempre. Si una de las finalizaciones acaba revelando el fallo, se descubre el problema, que es de lo que verdaderamente hay que cuidar. He aquí un ejemplo simple de cómo debería usarse: / / : c04:CondicionMuerte.java / / Utilización de finalize ( ) para detectar un / / objeto que no ha sido eliminado correctamente. class Libro { boolean comprobado = false; Libro (boolean comprobar) { comprobado = comprobar;

1 void correcto ( ) { comprobado = false; 1 public void finalizeo { if (comprobado) System.out.println("Error: comprobado");

1 public class CondicionMuerte { public static void main (String[] args) { Libro novela = new Libro(true) ; / / Eliminación correcta: novela.correcto ( ) ; / / Cargarse la referencia, olvidando la limpieza: new Libro (true); / / Forzar la recolección de basura y finalización: System.gc ( ) ;

1 1 ///:-

La condición de muerte consiste en que todos los objetos Libro supuestamente serán comprobados antes de ser recogidos por el recolector de basura, pero en el método main( ) un error del proUn término acuñado por Hill Venners (www.artima.com) durante un seminario que él y yo impartimos conjuntamente.


4: Inicialización y limpieza

147

gramador no comprueba alguno de los libros. Sin finalize( ) para verificar la condición de muerte, este error podría ser difícil de encontrar. Nótese que se usa system.gc( ) para forzar la finalización (y se debería hacer esto durante el desarrollo del programa para forzar la depuración). Pero incluso aunque no se use, es muy probable descubrir objetos de tipo Libro errantes a lo largo de ejecuciones repetidas del programa (asumiendo que el programa asigna suficiente espacio de almacenamiento para hacer que se ejecute el recolector de basura).

Cómo funciona un recolector de basura Si se realiza en un lenguaje de programación en el que la asignación de objetos en el montículo es cara, hay que asumir naturalmente que el esquema de Java de asignar todo (excepto los datos primitivos) en el montículo es caro. Sin embargo, resulta que el recolector de basura puede tener un impacto significativo en un incremento de la velocidad de creación de los objetos. Esto podría sonar un poco extraño al principio - q u e la liberación de espacio de almacenamiento afecte a la asignación de espacio- pero es la manera en que trabajan algunas JVM, y significa que la asignación de espacio para objetos del montículo en Java pue da ser casi tan rápida como crear espacio de almacenamiento en la pila en otros lenguajes. Por ejemplo, se puede pensar que el montículo de C++ es como un terreno en el que cada objeto toma un fragmento de suelo. Puede ser que este espacio sea abandonado tiempo después, y haya que reutilizarlo. En algunas JVM, el montículo de Java es bastante distinto; es más como una cinta transportadora que avanza cada vez que se asigna un nuevo objeto. Esto significa que la asignación de espacio de almacenamiento de los objetos es notoriamente rápida. El "puntero del montículo" simplemente se mueve hacia delante en territorio virgen, así que es exactamente lo mismo que la asignación de pila de C++. (Por supuesto, hay una pequeña sobrecarga por el mantenimiento de espacios, pero no hay nada como buscar espacio de almacenamiento.) Ahora uno puede observar que el montículo no es, de hecho, una cinta transportadora, pues si se trata como tal podría comenzar eventualmente una paginación excesiva de memoria (que constituye un factor de rendimiento importante), e incluso más tarde la memoria podría agotarse. El truco es que el recolector de basura va paso a paso, y mientras recolecta la basura, compacta todos los objetos de la pila de forma que el resultado es que se ha movido el "puntero del montículo" más cerca del principio de la cinta transportadora y más lejos de un fallo de página. El recolector de basura reorganiza los elementos y hace posible usar un modelo de montículo de alta velocidad y árbol infinito, durante la asignación de espacio de almacenamiento. Para entender cómo funciona esto es necesario tener una idea un poco mejor de la manera en que funcionan los diferentes esquemas de recolección de basura (GC, Garbage Collector). Una técnica simple pero lenta de GC es contar referencias. Esto significa que cada ejemplo tiene un contador de referencias, y cada vez que se adjunte una referencia a un objeto se incrementa en uno el contador de referencias. Cada vez que una referencia cae fuera del ámbito o se pone null se decrementa el contador de referencias. Por consiguiente, la gestión de contadores de referencias supone una carga constante y pequeña que se va produciendo durante toda la vida del programa. El recolector de


148

Piensa en Java

basura va recorriendo toda la lista de objetos y al encontrar alguno con el contador de referencias a cero, libera el espacio de almacenamiento que tenía asignado. El inconveniente radica en que si los objetos tienen referencias circulares entre sí es posible no encontrar contadores de referencias a cero, que, sin embargo, pueden ser basura. La localización de estos grupos auto-referenciados requiere de una carga de trabajo significativa por parte del recolector de basura. La cuenta de referencias se usa frecuentemente para explicar un tipo de recolección de basura, pero parece no estar implementada en ninguna Máquina Virtual de Java. En esquemas más rápidos, la recolección de basura no se basa en la cuenta d e referencias. S e basa, en cambio, en la idea de que cualquier objeto no muerto podrá recorrerse, realizar una traza en última instancia, hasta una referencia que resida bien en la pila o bien en espacio de almacenamiento estático. La cadena podría atravesar varias capas de objetos. Por consiguiente, si se comienza en la pila y en el área de almacenamiento estático y se van recorriendo todas las referencias, será posible localizar todos los objetos vivos. Por cada referencia que se encuentre, es necesario hacer un recorrido traceo hasta localizar el objeto al que apunta y después seguir todas las referencias a ese objeto, recorriendo todos los objetos a los que apunta, etc., hasta haber recorrido toda la red que se originó con la referencia de la pila o del almacenamiento estático. Cada objeto que se recorra debe seguir necesariamente vivo. Fíjese que no hay ningún problema con los grupos auto-referenciados -simplemente no son localizados en el recorrido, trazado, por lo que se consideraran basura automáticamente. En la aproximación descrita, la Máquina Virtual de Java usa un esquema de recolección de basura adaptativo, y lo que hace con los objetos vivos que encuentra depende de la variante que se haya implementado. Una de estas variaciones es la de parar-y-copiar. Esto significa que -por razones que pronto parecerán evidentes- el programa se detiene en primer lugar (este esquema no implica recolección en segundo plano). Posteriormente, cada objeto vivo que se encuentre se copia de un montículo a otro, dejando detrás toda la basura. Además, a medida que se copian los ejemplos al nuevo montículo, se empaquetan de extremo a extremo, compactando por consiguiente el nuevo montículo (y permitiendo recorrer rápida y simplemente el nuevo almacenamiento hasta el final, como se describió previamente). Por supuesto, cuando se mueve un objeto de un lugar a otro, hay que cambiar todas las referencias que apuntan a ese objeto. Las referencias que vayan del montículo o del área de almacenamiento estática a un objeto pueden cambiarse directamente, pero puede haber otras referencias que apunten a este objeto y que se encuentren más tarde durante la "búsqueda". Éstas se van recomponiendo a medida que se encuentren (podría imaginarse una tabla que establezca una relación entre las direcciones viejas y las nuevas). También hay dos aspectos que hacen ineficientes a estos denominados "recolectores de copias". El primero es la idea de que son necesarios dos montículos y se maneja por toda la memoria adelante y atrás entre estos dos montículos separados, manteniendo el doble de memoria de la que de hecho se necesita. Algunas Máquinas Virtuales dc Java siguen este esquema asignando el montículo por bloques a medida que son necesarios y haciendo simplemente copias de bloques. El segundo aspecto es la copia. Una vez que el programa se vuelve estable, debería generar poca o ninguna basura. Además de esto, un recolector de copias seguiría copiando toda la memoria de un


4: Inicialización y limpieza

149

sitio a otro, lo que es una pérdida de tiempo y recursos. Para evitar esto, algunas Máquinas Virtuales de Java detectan que no se esté generando nueva basura y pasan a un esquema distinto (ésta es la parte "adaptativa"). Este otro esquema denominado marcar y barrer4, es el que usaban las primeras versiones de la Máquina Virtual de Java de Sun. Para uso general, el esquema de marcar y barrer es bastante lento, pero si se genera poca o ninguna basura es rápido. El marcar y barrer sigue la misma lógica de empezar rastreando a través de todas las referencias, a partir de la pila y el almacenamiento estático, para encontrar objetos vivos. Sin embargo, cada vez que encuentra un objeto vivo, lo marca poniendo a uno cierto indicador, en vez de recolectarlo. Sólo

cuando acaba el proceso de marcado, se da el barrido. Durante el barrido, se liberan los objetos muertos. Sin embargo, no se da ninguna copia, de forma que si el recolector elige recolectar un montículo fragmentado, lo hace reordenando todos los objetos. El "parar-y-copiar" se refiere a la idea de que este tipo de recolector de basura no se hace en segundo plano; sino que, por el contrario, se detiene el programa mientras se ejecuta el recolector de basura. En la documentación de Sun se encuentran muchas referencias a la recolección de basura como un proceso de segundo plano de baja prioridad, pero resulta que el recolector de basura no está implementado así, al menos en las primeras versiones de la Máquina Virtual de Java de Sun. En vez de esto, el recolector de basura de Sun se ejecutaba cuando quedaba poca memoria. Además el marcado y barrido requiere la detención del programa. Como se mencionó previamente, en la Máquina Virtual de Java aquí descrita, la memoria se asigna por bloques grandes. Si se asigna un objeto grande, éste se hace con un bloque propio. El parar-ycopiar estricto exige copiar todos los objetos vivos del montículo fuente a un montículo nuevo antes de poder liberar el viejo, lo que se traduce en montones de memoria. Con los objetos, el recolector de basura puede en ocasiones usar los bloques muertos para copiar los objetos al ir recolectando. Cada bloque tiene un contador de generación para mantener información sobre si está o no vivo. En circunstancias normales, sólo se compactan los bloques creados desde la última recolección. Así se maneja la gran cantidad de objetos temporales de vida corta. Periódicamente, se hace un barrido completo -se siguen sin copiar los objetos grandes, y se copian y compactan todos los bloques que tienen objetos pequeños. La Máquina Virtual de Java monitoriza la eficiencia de la recolección de basura y si se convierte en una pérdida de tiempo porque todos los objetos tienen vida larga, pasa al esquema de marcar-y-barrer. De manera análoga, la Máquina Virtual de Java mantiene un registro del éxito del marcar-y-borrar, y si el montículo comienza a estar fragmentado vuelve de nuevo al parar-y-copiar. Éste es el momento en que interviene la parte "adaptativa", de forma que finalmente se tiene un nombre kilométrico: "Marcado-y-borrado con parada-y-copia adaptativo generacional". Hay varias técnicas que permiten acelerar la velocidad de la Máquina Virtual de Java. Una especialmente importante se refiere a la operación del cargador y del compilador "justo-a-tiempo" UIV. Cuando hay que cargar una clase (generalmente, la primera vez que se desea crear un objeto de esa clase), se localiza el fichero .clnss y se llcva a mcmoria cl "codigo byte" de esa clase. En ese momento, un enfoque sería compilar JIT todo el código, pero esto tiene dos inconvenientes: lleva un poco más de tiempo, lo cual, extrapolado a toda la vida del programa puede ser significativo; y aumenta el tamaño del ejecutable (los "códigos byte" son bastante más compactos que el código JIT ' N. Del traductor: En inglés, mark and sweep.


150

Piensa en Java

expandido), lo que podría causar paginación, que definitivamente ralentizaría el programa. Un enfoque alternativo lo constituye la evaluación perezosa, que quiere decir que el código no se compila JIT hasta que es necesario. Por tanto, el código que no se ejecute nunca será compilado por JIT .

Inicialización de miembros Java sigue este camino para garantizar que se inicialicen correctamente todas las variables antes de ser utilizadas. En el caso de variables definidas localmente en un método, esta garantía se presenta en forma de error de tiempo de compilación, de forma que si se dice: void f ( ) { int i; i++;

1

se obtendrá un mensaje de error que dice que i podría no haber sido inicializada. Por supuesto, el compilador podría haber asignado a i un valor por defecto, pero es más probable que se trate de un error del programador, que un valor por defecto habría camuflado. Al forzar al programador a dar un valor de inicialiación es más fácil detectar el fallo. Sin embargo, las cosas son algo distintas en el caso de atributos de tipo primitivo de una clase. Dado que cualquier método puede inicializar o usar ese dato, podría no ser práctico obligar al usuario a inicializarlo a su valor apropiado antes de usar el dato. Sin embargo, es poco seguro dejarlo con un valor basura, por lo que se garantiza que tendrá un valor inicial. Estos valores pueden verse aquí: / / : c04:ValoresIniciales.java / / Muestra los valores iniciales por defecto.

class Medida I boolean t; char c; byte b; short S; int i; long 1; float f; double d; void escribir ( ) { System.out.println( "Tipo dato Valor inicial\nW + t + "\nn + "boolean [IV + c + "1 "+ "char + b + "\n" + "byte " + S + "\nl' + "short + i + "\n" + " int


4: Inicialización y limpieza

" long " float "double

+ +

1 f

+ +

151

"\n" t "\n" +

" + d);

1 public class ValoresIniciales

{

public static void main (String[] args) Medida d = new Medida() ;

{

d.escribir ( ) ; / * En este caso también podría decirse: new Medida ( ) .escribir( ) ; */

1 1 ///:-

La salida del programa será: Tipo dato

Valor inicial

boolean

false

char byte short int long float double

[ l o

o o o o

0.0

o. o

El valor char es un cero, que se imprime como un espacio. Veremos más adelante que al definir una referencia a un objeto dentro de una clase sin inicializarla a un nuevo objeto, la referencia recibe el valor especial null (que es una palabra clave de Java) . Puede incluso verse que, aunque no se especifiquen los valores, se inicializan automáticamente. De esta forma, al menos, no hay amenaza de que se llegue a trabajar con valores sin inicializar.

Especificación de la inicialización ¿Qué ocurre si se quiere dar un valor inicial a una variable? Una manera directa de hacerlo consiste simplemente en asignar el valor al definir la variable en la clase. (Téngase en cuenta que esto no se puede hacer en C++,aunque los novatos en C++ siempre intentan hacerlo). Aquí se han cambiado las definiciones de la clase Medida para que proporcionen valores iniciales: class Medida { boolean b = true; char c = 'x';


152

Piensa en Java

b y t e b = 47; s h o r t s = Oxff; i n t i = 999; l o n g 1 = 1; f l o a t f = 3.14f; d o u b l e d = 3.14159;

//

1

. . .

También se pueden inicializar de la misma manera objetos no primitivos. Si Profundidad es una clase, se puede insertar una variable e inicializarla así: c l a s s Medida

{

P r o f u n d i d a d o = new P r o f u n d i d a d ( ) ; boolean b = true;

//

. . .

Si no se ha dado a o un valor inicial e intenta usarlo de cualquier forma, se obtendrá un error de tiempo de ejecución denominado excepción (del que se hablará en el Capítulo 10). Se puede incluso invocar a un método para proporcionar un valor de inicialización: class C I n i t int i //. .-

{ =

f0;

El método puede, por supuesto, tener parámetros, pero éstos no pueden ser sino miembros de la clase que no han sido aún inicializados. Por consiguiente, se puede hacer esto: class CInit int i int j

//.

{ =

f0 ;

=

g ( i );

..

1 Pero no se puede hacer esto: class CInit int j int i

{ =

g(i);

=

f();

Éste es un punto en el que el compilador se queja, con razón, del referenciado hacia delante, pues es un error relacionado con el orden de la inicialización y no con la manera de compilar el programa.


4: Inicialización y limpieza

153

Este enfoque de inicialización es simple y directo. Tiene la limitación de que todo objeto de tipo Medida tendrá los mismos valores de inicialización. Algunas veces esto es justo lo que se necesita, pero otras veces se necesita mayor flexibilidad.

Inicialización de constructores El constructor puede usarse para llevar a cabo la inicialización, lo que da una flexibilidad mayor en la programación, puesto que se puede invocar a métodos para llevar a cabo acciones en tiempo de ejecución que determinen los tiempos de ejecución. Sin embargo, hay que recordar siempre que no se está excluyendo la inicialización automática, que se da antes de entrar en el constructor. Así, por ejemplo, si se dice: class Contador

{

int i; Contador() // . . .

{

i

=

7;

}

se inicializa primero la i a O, y después a 7. Esto es cierto con todos los tipos primitivos y con las referencias a objetos, incluyendo aquéllos a los que se da inicialización explícita en el momento de su definición. Por esta razón, el compilador no intenta forzar la inicialización de elementos del constructor en ningún lugar en concreto, o antes de que se usen -la inicialización ya está garantizada4.

Orden de inicialización Dentro de una clase, el orden de inicialización lo determina el orden en que se definen las variables dentro de la clase. Las definiciones de variables pueden estar dispersas a través y dentro de las definiciones de métodos, pero las variables se inicializan antes de invocar a ningún método -incluido el constructor. Por ejemplo: / / : c04:OrdenDeInicializacion.java / / Demuestra el orden de inicialización

/ / Cuando se invoque al constructor para crear un / / objeto Etiqueta, se verá un mensaje: class Etiqueta { Etiqueta (int marcador) { System. out .println ("Etiqueta (" + marcador + " )

class Tarjeta Etiqueta tl

") ;

{ =

new Etiqueta(1); / / Antes del constructor

En contraste, C++ tiene la lista de inicializadores del constructor que hace que se dé la inicialización antes de entrar en el cuerpo del constructor, y se fuerza para los objetos. Ver Thinking in C++, 2." edición (disponible en el CD ROM de este libro, y en http://www. BruceEckel. com).


154

Piensa en Java

Tarjeta0 { / / Indicar que estamos en el constructor: System.out .println ("Tarjeta( ) Ir); t3 = new Etiqueta(33) ; / / Reiniciar t3 1 Etiqueta t2 = new Etiqueta(2); / / Después del constructor void f ( ) { System.out .println ("f( ) " ) ; Etiqueta

t3

=

new

E t i q u e t a ( 3 ); / /

Al

f i n a l

1 public class OrdenDeInicializacion { public static void main (String[] args) { Tarjeta t = new Tarjeta(); t.f O ; / / Muestra que se ha acabado la construcción

1 1 ///:En Tarjeta, la definición de los objetos Etiqueta se han dispersado intencionadamente para probar que todos se inicializarán antes de que se llegue a entrar al constructor u ocurra cualquier otra cosa. Además, t3 se reinicia dentro del constructor. La salida es: Etiqueta ( 1 ) Etiqueta (2) Etiqueta (3) Tarjeta O Etiqueta (33)

f0 Por consiguiente, la referencia t3 se inicializa dos veces, una antes y otra durante la llamada al constructor. (El primer objeto se desecha, de forma que posteriormente podrá ser eliminado por el recolector de basura.) Esto podría parecer ineficiente a primera vista, pero garantiza una inicialización correcta -¿Qué ocurriría si se definiera un constructor sobrecargado que no inicializara t3 y no hubiera una inicialización "por defecto" para t3 en su definición?

Inicialización de datos estáticos Cuando los datos son estáticos ocurre lo mismo; si se trata de un dato primitivo y no se inicializa, toma los valores iniciales estándares de los tipos primitivos. Si se trata de una referencia a un objeto, es null, a menos que se cree un objeto nuevo al que se asocie la referencia. Si se desea realizar una inicialización en el momento de la definición, ocurre lo mismo que con los no estáticos. Sólo hay un espacio de almacenamiento para un dato estático independientemente de cuántos objetos se creen. Pero las dudas surgen cuando se inicializa el espacio de almacenamiento de un dato estático. Un ejemplo puede aclarar esta cuestión:


4: Inicialización y limpieza

/ / : c04:InicializacionStatic.java / / Especificando los valores iniciales en una / / definición de clase.

class Bolo { Bolo (int marcador) { System.out.println ("Bolo("

+

marcador

+

") ") ;

1 void f (int marcador) I System.out.println ("f ("

+ marcador

t ") ") ;

1 1

class Mesa { static Bolo bl = new Bolo(1) ; Mesa 0 i System.out .println ("Mesa ( ) " ) ; b2.f (1);

1 void f2 (int marcador) { System.out .println ("£2 ( "

+

+

") ") ;

+ marcador +

") ") ;

marcador

1 static Bolo b2

=

new Bolo(2);

class Armario { Bolo b3 = new Bolo (3); static Bolo b4 = new Bolo(4); Armario() { System.out .println ("Armario0 ") ; b4.f (2); J

void f3 (int marcador) { System.out .println ("f3( "

1 static Bolo b5

=

new Bolo(5);

public class InicializacionStatic { public static void main (String[] args) { System.out.println( "Creando nuevo Armario ( ) eri el método main") ; new Armario ( ) ; System.out.println( "Creando nuevo Armario ( ) en el método main");

155


156

Piensa en Java

new Armario t2. f2 (1); t3.f3 (1);

() ;

}

static Mesa t2 = new Mesa(); static Armario t3 = new Armario() ; 1 ///:-

Bolo permite ver la creación de una clase, y Mesa y Armario crean miembros estáticos de Bolo dispersos por sus definiciones de clases. Fíjese que Armario crea un Bolo no estático antes de las definiciones estáticas. La salida muestra lo que ocurre: Bolo (1) Bolo (2) Mesa ( ) f (1)

Bolo (4) Bolo (5) Bolo (3) Armario ( ) f (2) Creando nuevo Armario() el método main Bolo (3) Armario ( ) f (2) Creando nuevo Armario() el método main Bolo (3) Armario ( ) f (2)

£ 2 (1)

f3 (1)

La inicialización estática sólo se da si es necesaria. Si no se crea un objeto Mesa y nunca se hace referencia a Mesa.bl o Mesa.b2, los objetos estáticos de tipo Bolo b l y b 2 no se crearán nunca. Sin embargo, se inicializan sólo cuando se cree el primer objeto Mesa (o se dé el primer acceso estático). Después de eso, los objetos estáticos no se reinician.

Se inicializan primero los objetos estáticos, si todavía no han sido inicializados durante la creación anterior de un objeto, y posteriormente los objetos no estáticos. Se puede ver la prueba de esto en la salida del programa anterior. Es útil resumir el proceso de creación de un objeto. Considérese una clase llamada Perro: 1.

La primera vez que se cree un objeto de tipo Perro, o la primera vez que se acceda a un método estático o un campo estático de la clase Perro, el intérprete de Java debe localizar Perro.class, que lo hace buscando a través de las trayectorias de clases.


4: Inicialización y limpieza

157

Al cargar Perro.class (creando un objeto Class, del que se hablará más adelante), se ejecutan todos sus inicializadores estáticos. Por consiguiente, la inicialización sólo tiene lugar una vez, al cargar el objeto Class la primera vez. Cuando se crea un new Perro( ), el proceso de construcción de un objeto Perro asigna, en primer lugar, el espacio de almacenamiento suficiente para un objeto Perro del montículo. Este espacio de almacenamiento se pone a cero, poniendo automáticamente todos los datos primitivos del objeto Perro con sus valores por defecto (cero para los números y su equiva-

lente para los boolean o char) y las referencias a null. Se ejecuta cualquier inicialización que se dé en el momento de la definición de campos. Se ejecutan los constructores. Como se verá en el Capítulo 6, esto podría implicar de hecho una cantidad de actividad considerable, especialmente cuando esté involucrada la herencia .

Inicialización estática explícita Java permite agrupar todas las inicializaciones estáticas dentro de una "cláusula de construcción estática" (llamada a veces bloque estático) dentro de una clase. Tiene la siguiente apariencia: class Cuchara I static int i; static { i = 47;

Parece un método, pero es simplemente la palabra clave static seguida de un cuerpo de método. Este código, como otras inicializaciones estáticas, se ejecuta sólo una vez, la primera vez que se cree un objeto de esa clase o la primera vez que se acceda a un miembro estático de esa clase (incluso si nunca se llega a hacer un objeto de esa clase). Por ejemplo: / / : c04:StaticExplicito.java / / Inicialización explícita estática / / con la cláusula "static". class Taza { Taza (int marcador) { System.out .println ("Taza(" + marcador + " ) ") ; 1 void f (int marcador) System.out.println ("f(" + marcador + lf)lr); 1

class Tazas I


158

Piensa en Java

static Taza static Taza static { cl = new c2 = new

cl; c2; Taza(1); Taza(2);

Tazas ( ) { System.out .println ("Tazas( )

") ;

public class StaticExplicito { public static void main(String[] args) { System.out .println ("Dentro de main ( ) " ) ; Tazas-cl.f (99); / / (1)

1 / / static Tazas x / / static Tazas y 1 ///:-

=

=

new Tazas 0 ; new Tazas 0 ;

/ /

(2)

(2)

Los inicializadores estáticos de Tazas se ejecutan cuando se da el acceso al objeto estático c l en la línea marcada (l), si la línea (1) se marca como un comentario, y se quita el signo de comentario de las líneas marcadas como (2). Si tanto (1) como (2) se consideran comentarios, la inicialización estática de Tazas no se realizará nunca. Además, no importa si una o las dos líneas marcadas (2) dejan de ser comentarios; la inicialización sólo ocurre una vez.

Inicialización de instancias no estáticas Java proporciona una sintaxis similar para la inicialización de variables no estáticas de cada objeto. He aquí un ejemplo: / / : c04:Jarras.java / / Java "Inicialización de Instancias." class Jarra { Jarra (int marcador) { System.out .println ("Jarra( " void f (int marcador) { System.out .println ("f( "

public class Jarras Jarra cl; Jarra c2;

{

+

+ marcador +

marcador

+

") ") ;

") ") ;


4: Inicialización y limpieza

159

1 cl = new Jarra(1) ; c2 = new Jarra (2); System.out.println("c1 y c2 inicializadas");

1 Jarras ( ) { System.out .println ("Jarras( ) ") ;

1

public static void main(String[] args)

1

System.out .println ("Dentro de main ( ) Jarras x = new Jarras 0;

{ ") ;

1 1 ///:Se puede ver que la cláusula de inicialización de instancias: {

cl = new Jarra (1); c2 = new Jarra (2); System. out .println ("cl y c2 inicializadas") ;

1 tiene exactamente la misma apariencia que la cláusula de inicialización estática excepto porque no está la palabra clave static. Esta sintaxis es necesaria para dar soporte a la inicialización de clases internas anónimas (ver Capítulo 8).

Inicialización de arrays La inicialización de arrays en C suele ser fuente de errores y tediosa. C++ usa la inicialización agregada para hacerla más segura" Java no tiene "agregados" como C++,puesto que en Java todo es un objeto. Tiene arrays, y éstos se soportan con la inicialización de arrays. Un array es simplemente una secuencia, bien de objetos o bien de datos primitivos, todos del mismo tipo, empaquetados juntos bajo un único identificador. Los arrays se definen y utilizan con el operador de indexación entre corchetes [ l. Para definir un array simplemente hay que colocar corchetes vacíos seguidos del nombre del tipo de datos:

1

int

[]

al;

También se puede poner los corchetes tras el identificador para lograr exactamente el mismo significado:

1

int al [ ] ;

Thinking in C++, 2." edición, para obtener una descripción completa de la inicialización agregada.


160

Piensa en Java

Esto satisface las expectativas de los programadores de C y C++.El estilo anterior, sin embargo, es probablemente una sintaxis más sensata, puesto que dice que el tipo es "un array de int". Éste será el estilo que se use en este libro. El compilador no permite especificar el tamaño del array. Esto nos devuelve al aspecto de las "referencias". Todo lo que se tiene en este momento es una referencia a un array, para el que no se ha asignado espacio de almacenamiento. Para crear espacio de almacenamiento para el array es necesario escribir una expresión de inicialización. En el caso de los arrays, la inicialización puede apa-

recer en cualquier lugar del código, pero puede usarse un tipo especial de expresión de inicialización que debe situarse en el mismo lugar en que el que se crea el array. Esta inicialización especial e s un conjunto de valores encerrados entre llaves. Es el compilador el que se encarga de

la asignación de espacio (el equivalente a usar new). Por ejemplo: int [ ]

al

=

{

1, 2, 3, 4, 5

};

Por tanto ¿por qué puede definirse una referencia a un array sin un array? int

[

]

a2;

Bien, es posible asignar un array a otro en Java, por lo que puede decirse: a2

=

al;

Lo que se está haciendo realmente es copiar una referencia, como se demuestra a continuación: / / : c04:Arrays. java / / Arrays de datos primitivos. public class Arrays { public static void main (String[] args) int[l al = { 1, 2, 3, 4, 5 } ; int [] a2; a2 = al; for(int i = O; i < a2.length; i++) a2 [i]++; for (int i = O; i < al.length; i++) System.out.println( "al[" t i + "1 = " t al[i]);

{

Puede verse que se da a a l un valor de inicialización mientras que a a2, no; a2 se asigna más tarde -en este caso, a otro array. Aquí hay algo nuevo: todos los arrays tienen un miembro intrínseco (bien sean arrays de objetos o arrays de tipos primitivos) por el que se puede preguntar -pero no modificar- para saber cuántos elementos hay en el array. Este miembro es length. Dado que los arrays en Java, como en C y C++, empiezan a contar desde elemento O, el elemento más lejano que se puede indexar es length - 1.


4: Inicialización y limpieza

161

Si se sale de rango, no produce error, siendo esto la fuente de muchos errores graves. Sin embargo, Java le protege de esos problemas originando un error en tiempo de ejecución (una excepción,el tema del Capítulo 10) al intentar acceder más allá de los límites. Por supuesto, la comprobación de todos los accesos a arrays supone tiempo y código, y no hay manera de desactivarse, lo que significa que los accesos a arrays podrían ser una fuente de ineficiencia en un programa si se dan en una situación crítica. Los diseñadores de Java pensaron que este sacrificio merecía la pena en aras de la seguridad de Internet y la productividad del programador. ¿Qué ocurre si al escribir el programa se desconocen cuántos elementos son necesarios que tenga

el array? Simplemente se utiliza new para crear elementos del array. Aquí, new funciona incluso aunque se esté creando un array de datos primitivos (new no creará datos primitivos que no sean elementos de un array): / / : c04:NuevoArray.java / / Creando arrays con new import java.uti1. *; public class NuevoArray { static Random aleatorio = new Random(); static int pAleatorio (int modulo) { r e t u r r i Math. abs (aleatorio.nextInt ( ) ) % modulo public static void main (String[] args) int [] a; a = new int [pAleatorio (20)1 ; System.out.println( "longitud de = " + a.length) ; for(int i = O; i < a.length; i++) System.out.println( Wa[" + i + " 1 = " + a[il);

+ 1;

{

Dado que el tamaño del array se elige al azar (utilizando el método pAleatorio( )) está claro que la creación del array se está dando en tiempo de ejecución. Además, se verá en la salida de este programa que los elementos del array de tipos primitivos se inicializan automáticamente a valores "vacíos". (En el caso de valores numéricos y carácter, este valor es cero, y en el caso de los boolean, es false.) Por supuesto, el array también podría haberse definido e inicializado en la misma sentencia: int [] a

=

new int [pAleatorio(20)];

Si se está tratando con un array de objetos no primitivos, siempre es necesario usar new. Aquí, vuelve a surgir el tema de las referencias porque lo que se crea es un array de referencias. Considérese el tipo Integer, que es una clase y no un tipo primitivo: / / : c04:ObjetoClaseArray.java / / Creando un array de objetos no primitivos.


162

Piensa en Java

import java.uti1.*; public class ObjetoClaseArray { static Random aleatorio = new RandomO; static int pAleatorio (int modulo) { return Math. abs (aleatorio.nextInt ( ) ) % modulo 1 public static void main(String[] args) { Integer [ ] a = new Integer [pAleatorio (20)] ; System.out.println( "longitud de a = " + a.length) ; for(int i = O; i < a.length; i t + ) { a [il = new Integer (pAleatorio(500)) ; System.out.println( + i + "1 = " + a[il);

+

1;

W a [ "

1 1 1 ///:-

Aquí, incluso tras llamar a new para crear el array: Integer [ ]

a

=

new Integer [pAleatorio (20)];

se trata sólo de un array de referencias, y no se completa la inicialización hasta que se inicializa la propia referencia creando un nuevo objeto Integer: a [i]

=

new Integer (pAleatorio(500))

;

Si se olvida crear el objeto, sin embargo, se obtiene una excepción en tiempo de ejecución al intentar leer la localización vacía del array. Eche un vistazo a la creación del objeto String dentro de las sentencias de impresión. Puede observarse que la referencia al objeto Integer se convierte automáticamente para producir un String que representa el valor dentro del objeto. También es posible inicializar el array de objetos utilizando la lista encerrada entre llaves. Hay dos formas: / / : c04:InicializacionArray.java / / Inicialización de arrays. public class InicializacionArray { public static void main (String[] args) Integer[l a = i new Integer (1), new Integer (2), new Integer (3),

1;

{


4: Inicialización y limpieza

Integer[] b = new Integer [l new Integer (1), new Integer (2), new Integer (3),

163

{

};

1

1 ///:Esto es útil en ocasiones, pero es más limitado, pues se determina el tamaño del array en tiempo de compilación. La coma final de la lista de inicializadores es opcional. (Esta característica permite un mantenimiento más sencillo de listas largas.)

La segunda forma de inicializar arrays proporciona una sintaxis adecuada para crear y llamar a métodos que pueden producir el mismo efecto que las listas de parámetros variables de C (conocidas en este lenguaje como "parametros-variables").Éstas pueden incluir una cantidad de parámetros desconocida además de tipos desconocidos. Dado que todas las clases se heredan en última instancia de la clase raíz común Object (un tema del que se aprenderá más a medida que progrese el libro), se puede crear un método que tome un array de Object e invocarlo así: / / : c04:ParametrosVariables.java / / Utilizando la sintaxis de arrays para crear / / listas de parámetros variables.

1

class A

{

int i; 1

public class ParametrosVariables { static void f (Object[ l x) { for (int i = O; i < x.length; i++) System.out .println (x[i]) ;

1 public static void main(String[l args) { f(new Object[] { new Integer (47), new ParametrosVariables ( ) new Float (3.14), new Double (11.11) 1 ) ; f(new Object[] {"un", "dos", "tres" } ) ; f (new Object [] {new A ( ) , new A 0, new A ( ) } ) ;

,

1 1 ///:-

En este punto, no hay mucho que pueda hacerse con estos objetos desconocidos, y el programa usa la conversión automática String para hacer algo útil con cada Object. En el Capítulo 12, que cubre la identificación de tipos en tiempo de ejecución (Run-time type identification, R'TTI), se aprenderá a descubrir el tipo exacto de objetos así, de forma que se pueda hacer algo más interesante con ellos.


Piensa en Java

164

Arrays multidimensionales Java permite crear fรกcilmente arrays multidimensionales: / / : c04:ArrayMultidimensional.java / / creando arrays multidimensionales. import java.util.*;

public class ArrayMultidimensional { static Random aleatorio = new Random(); static int pAleatorio (int modulo) { return Math. abs (aleatorio.nextInt ( ) ) % modulo + 1;

1 static void visualizar (String S) System.out .println (S);

{

1 public static void main (String[] args) { int[l [ ] al = { { 1, 21 31 1 1 t 4 1 51 6 1 1 1 1; for(int i = O; i < al.length; i++) for (int j = O; j < al [i]. length; j++) visualizar ("al[ " + i + "1 [ " + j + 1 = + al[il [jl); / / array 3-D de longitud fija: int [] [][] a2 = new int [2][2][4]; for (int i = O; i < a2.length; i+t) for (int j = O; j < a2 [i]. length; jt+) for(int k = O; k < a2[i] [j].length; k++) visualizar ("a2[ " + i + "1 [ " + j + "][" + k + 1 = + a2[il[jl[kl); / / array 3-D con vectores de longitud variable: int [] [ ] [] a3 = new int [pAleatorio(7)] [][]; for (int i = O; i < a3.length; i++) { a3 [i] = new int [pAleatorio (5)] []; for(int j = O; j < a3[i] .length; j++) a3[il [jl = new int[pAleatorio(5)1; 11

11

17

11

1 for (int i = O; i < a3.length; i++) for (int j = O; j < a3 [i].length; j++) for(int k = O; k < a3[i] [j].length; k++)


4: Inicialización y limpieza

165

visualizar ("a3[ " + i + "1 [ " + j + "][" + k + 1 = + a3 [il [jl [kl ) ; / / Array de objetos no primitivos: Integer [][] a4 = { { new Integer (1), new Integer (2)} , { new I n t e g e r ( 3 ) , new I n t e g e r ( 4 ) } , { new Integer (S), new Integer (6)1 , 11

11

1; for(int i = O; i < a4.length; i++) for (int j = O; j < a4 [i]. length; j++) visualizar("a4[" + i + "1 [ " + j + " 1 = " + a4[i] [j]); Integer [][ ] a5; a5 = new Integer [3][]; for(int i = O; i < a5.length; i++) { a5[i] = new Integer[3]; for (int j = O; j < a5 [i].length; j++) a5 [i][ j ] = new Integer (i*j) ; 1 for(int i = O; i < a5.length; i++) for(int j = O; j < a5[i] .length; j++) visualizar ("a5[ " + i + "1 [ " + j + 1 = + a5[il [jl); 11

71

1 1 ///:-

El código utilizado para imprimir utiliza el método length, de forma que no depende de tamaños fijos de array. El primer ejemplo muestra un array multidimensional de tipos primitivos. Se puede delimitar cada vector del array por llaves:

Cada conjunto de corchetes nos introduce en el siguiente nivel del array. El segundo ejemplo muestra un array de tres dimensiones asignado con new. Aquí, se asigna de una sola vez todo el array: int [][][] a2

=

new int [2][2][4];

Pero el tercer ejemplo muestra que cada vector en los arrays que conforman la matriz pueden ser de cualquier longitud:


Piensa en Java

166

int [ ] [][] a3 = new int [pAleatorio (7)] [][]; for (int i = O; i < a3.length; i++) { a3 [i] = new int [pAleatorio (5)] []; for(int j = 0; j < a3 [i]. length; j++) a3 [i][ j ] = new int [pAleatorio (5)1 ;

1

El primer new crea un array con un primer elemento de longitud aleatoria, y el resto, indeterminados. El segundo new de dentro del bucle for rellena los elementos pero deja el tercer índice inde-

terminado hasta que se acometa el tercer new. Se verá en la salida que los valores del array que se inicializan automáticamente a cero si no se les da un valor de inicialización explícito. Se puede tratar con arrays de objetos no primitivos de forma similar, lo que se muestra en el cuarto ejemplo, que demuestra la habilidad de englobar muchas expresiones new entre llaves: Integer [][ 1 a4 = { { new Integer (1), new Integer { new Integer(3), new Integer { new Integer(5), new Integer

1; El quinto ejemplo muestra cómo se puede construir pieza a pieza un array de objetos no primitivos: Integer [l [l a5; a5 = new Integer [3][]; for(int i = O; i < a5.length; i++) { a5[i] = new Integer[3]; for (int j = O; j < a5 [i]. length; j++) a5 [i][ j] = new Integer (i*j) ; 1

El i*j es simplemente para poner algún valor interesante en el Integer.

Resumen El constructor, mecanismo aparentemente elaborado de inicialización, proporciona un importante mecanismo para realizar la inicialización. Cuando Stroustrup estaba diseñando C++, una de las primeras observaciones que hizo sobre la productividad de C era relativa a la inicialización de las variables erróneas que causan un porcentaje significativo de los problemas de programación. Estos tipos de fallos son difíciles de encontrar, y hay aspectos similares que pueden aplicarse a la limpieza errónea. Dado que los constructores permiten garantizar la inicialización correcta y la limpieza (el compilador no permitirá que un objeto se cree sin los constructores pertinentes), se logra un control y seguridad completos. En C++,la destrucción es bastante importante porque los objetos creados con new deben ser destruidos explícitamente. En Java, el recolector de basura libera automáticamente la memoria de todos


4: Inicialización y limpieza

167

los objetos, por lo que el método de limpieza equivalente es innecesario en Java en la mayoría de ocasiones. En los casos en los que no es necesario un comportamiento al estilo de un destructor, el recolector de basura de Java simplifica enormemente la programación, y añade un elevado y necesario nivel de seguridad a la gestión de memoria. Algunos recolectores de basura pueden incluso limpiar otros recursos como los gráficos y los manejadores de ficheros. Sin embargo, el recolector de basura añade un coste en tiempo de ejecución, cuyo gasto es difícil de juzgar, debido a la lentitud de los intérpretes de Java existentes en el momento de escribir el presente libro. Cuando cambie esto, se podrá descubrir si la sobrecarga del recolector d e basura excluirá e l u s o d e Java p a r a determinados

tipos de programas. (Uno de los aspectos es la falta de predicción del recolector de basura.) Dado que se garantiza la construcción de todos los objetos, de hecho, hay más aspectos que los aquí descritos. En particular, al crear nuevas clases usando la agregación o la herencia también se mantiene la garantía de construcción, aunque es necesaria cierta sintaxis para dar soporte a esto. Se aprenderá todo lo relativo a la agregación, la herencia y cómo afectan éstas operaciones a los constructores en los capítulos siguientes.

Las soluciones a determiriados ejercicios se encuentran en el documento The Thinking in Java Annotated Solution Guide,disponible a bajo coste en http://www.BruceEckel.com.

Crear una clase con el constructor por defecto (el que no tiene parámetros) que imprima un mensaje. Crear un objeto de esta clase. Añadir un constructor sobrecargado al Ejercicio 1, que tome un String como parámetro y lo imprima junto con el mensaje. Crear un array de referencias a objetos de la clase creada en el Ejercicio 2, pero no crear los objetos a asignar al array. Al ejecutar el programa, tomar nota de si se imprimen los mensajes de inicialización del constructor. Completar el Ejercicio 3 creando los objetos a asociar al array de referencias. Crear un array de objetos String y asignar una cadena de caracteres a cada elemento. Imprimir el array utilizando un bucle for. Crear una clase Perro con un método ladrar( ) sobrecargado. Este método debería sobrecargarse en base a varios tipos de datos primitivos, e imprimir distintos tipos de ladridos, aullidos, etc. dependiendo de la versión sobrecargada que se invoque. Escribir un método main( ) que llame a todas las distintas versiones. Modificar el Ejercicio 6 de forma que dos de los métodos sobrecargados tengan dos argumentos (de dos tipos distintos), pero en orden inverso entre sí. Verificar que funciona. Crear una clase sin constructor, y crear un objeto de esa clase en main( ) para verificar que el constructor por defecto se invoca automáticamente. Crear una clase con dos métodos. Dentro del primer método, invocar al segundo dos veces: la primera vez sin utilizar this, y la segunda, usando this.


168

Piensa en Java

Crear una clase con dos constructores (sobrecargados). Utilizando this, invocar al segundo constructor dentro del primero. Crear una clase con un método finalize( ) que imprima un mensaje. En main( ), crear un objeto de esa clase. Explicar el funcionamiento del programa. Modificar el Ejercicio 11 de forma que siempre se llame a finalize( ). Crear una clase llamada Tanque que pueda rellenarse y vaciarse, y que tenga una condición de muerte que tenga que estar vacía al eliminar el objeto. Escribir un finalize( ) que verifique esta condición de muerte. En el método main( ), probar los escenarios posibles que puedan ocurrir al usar Tanque. Crear una clase que contenga un int y un char no inicializados, e imprimir sus valores para verificar que Java realiza la inicialización por defecto. Crear una clase que contenga una referencia String sin inicializar. Demostrar que Java inicializa esta referencia a null. Crear una clase con un campo String que se inicialice en el momento de la definición y otra que inicialice el constructor. $uál es la diferencia entre los dos enfoques? Crear una clase con un campo estático String que se inicialice en el momento de la definición, y otra que sea inicializada por un bloque estático. Añadir un método estático que imprima ambos campos y demuestre que ambos se inicializan antes de usarse. Crear una clase con un String que se inicialice usando "inicialización de instancias". Describir un uso de esa característica (una descripción distinta de la que se especifica en este libro). Escribir un método que cree e inicialice un array bidimensional de datos de tipo double. El tamaño del array vendrá determinado por los parámetros del método, y los valores de inicialización vendrán determinados por un rango delimitado por sus valores superior e inferior, parámetros ambos también del método. Crear un segundo método que imprima el array generado por el primer método. En el método main( ) probar los métodos creando e imprimiendo varios arrays de distintos tamaños. Repetir el Ejercicio 19 para un array tridimensional. Comentar la línea marcada (1) en StaticExplicito.java y verificar que la cláusula de inicialización estática no es invocada. Ahora, quitar la marca de comentario de alguna de las líneas marcadas (2) y verificar que se invoca a la cláusula de inicialización estática. Ahora quitar la marca de comentario de la otra línea marcada (2) y verificar que la inicialización estática sólo se da una vez. Experimentar con Basura.java ejecutando el programa utilizando los argumentos "rec", "finalizar" o "todo". Repetir el proceso y ver si detecta patrones en la salida. Cambiar el código de forma que se llame a System.runFinalization( ) antes que a System.gc( ) y observar los resultados.


5: Ocultar la Una consideración primordial del diseño orientado a objetos es "la separación de aquellas cosas que varían de aquéllas que permanecen constantes". Esto es especialmente importante en el caso de las bibliotecas. El usuario (el programador cliente) de la biblioteca debe ser capaz de confiar en la parte que usa, y saber que no necesita reescribir el código si se lanza una nueva versión de esa biblioteca. Por otro lado, el creador de la biblioteca debe tener la libertad para hacer modificaciones y mejoras con la certeza de que el código del programador cliente no se verá afectado por estos cambios. Esto puede lograse mediante una convención. Por ejemplo, el programador de la biblioteca debe acordar no eliminar métodos existentes al modificar una clase de la biblioteca, dado que eso destruiría el código del programador cliente. El caso contrario sería más problemático. En el caso de un atributo ¿cómo puede el creador de la biblioteca saber qué atributos son los que los programadores clientes han usado? Esto también ocurre con aquellos métodos que sólo son parte de la implementación de la clase, pero que no se diseñaron para ser usados directamente por el programador cliente. Pero, ¿qué ocurre si el creador de la biblioteca desea desechar una implementación antigua y poner una nueva? El cambio de cualquiera de esos miembros podría romper el código de un programador cliente. Por consiguiente, el creador de la biblioteca se encuentra limitado, y no

puede cambiar nada. Para solucionar este problema, Java proporciona especificadores de acceso para permitir al creador de la biblioteca decir qué está disponible para el programador cliente, y qué no. Los niveles de control de acceso desde el "acceso máximo" hasta el "acceso mínimo" son public, protected, "friendly" (para el que no existe palabra clave), y private. Por el párrafo anterior podría pensarse que, al igual que el diseñador de la biblioteca, se deseará mantener "private" tanto como sea posible, y exponer únicamente los métodos que se desee que use el programador cliente. Esto es completamente correcto, incluso aunque frecuentemente no es intuitivo para aquéllos que programan en otros lenguajes (especialmente C), y se utilizan para acceder a todo sin restricciones. Para cuando acabe este capítulo, el lector debería convencerse del valor del control de accesos en Java. Sin embargo, el concepto de una biblioteca de componentes y el control sobre quién puede acceder a los componentes de esa biblioteca están completos. Todavía queda la cuestión de cómo se empaquetan los componentes para formar una unidad cohesiva. Esto se controla en Java con la palabra clave package, y los especificadores de acceso se ven en la medida en que una clase se encuentre en un mismo o distinto paquete. Por tanto, para empezar este capítulo, se aprenderá cómo ubicar los componentes de las bibliotecas en paquetes. Posteriormente, uno será capaz de comprender el significado completo de los especificadores de acceso.


170

Piensa en Java

El paquete: la uh-¡dad de biblioteca Un paquete es lo que se obtiene al utilizar la palabra clave import para importar una biblioteca completa, como en: import java.uti1. *;

Esto trae la biblioteca de utilidades entera, que es parte de la distribución estándar de Java. Dado que, por ejemplo, la clase ArrayList se encuentra en java.uti1 es posible especificar el nombre completo java.uti1. ArrayList (lo cual se puede hacer sin la sentencia import) o bien se puede simplemente decir ArrayList (gracias a la sentencia import).

Si se desea incorporar una única clase, es posible nombrarla sin la sentencia import: import java.util.ArrayList;

Ahora es posible hacer uso de ArrayList, aunque no estarán disponibles ninguna de las otras clases de java-util.

La razón de todas estas importaciones es proporcionar un mecanismo para gestionar los "espacios de nombres". Los nombres de todas las clases miembros están aislados unos de otros. Un método f( ) contenido en la clase A no colisionará con un método f( ) que tiene la misma lista de argumentos, dentro de la clase B. Pero, ¿qué ocurre con los nombres de clases? Supóngase que se crea una clase Pila que se instala en una máquina que ya tiene una clase Pila escrita por otra persona. Con Java en Internet, esto podría incluso ocurrir sin que el usuario lo sepa, dado que es posible que algunas clases se descarguen automáticamente en el proceso de ejecutar un programa Java. Esta potencial colisión de nombres justifica la necesidad de tener control sobre los espacios de nombre en Java, y de tener la capacidad de crear un nombre completamente único sin que importen las limitaciones de Internet. Hasta ahora, la mayoría de los ejemplos de este libro se incluían en un único fichero y están diseñados para un uso local, por lo que no han tenido que hacer uso de los nombres de paquetes. (En este caso el nombre de clase se ubica en el "paquete por defecto".) Ésta es ciertamente una opción, y con motivo de mantener la máxima simplicidad, se usará este enfoque siempre que sea posible en todo el resto del libro. Sin embargo, si se planifica crear bibliotecas o programas que se relacionan con otros programas Java de la misma máquina, hay que pensar en evitar las colisiones entre nombres de clases. Cuando se crea un fichero de código fuente en Java, se crea lo que comúnmente se denomina una unidad de compilación (en ocasiones se denomina una unidad de traducción). Cada una de estas unidades tiene un nombre que acaba en .java, y dentro de la unidad de compilación puede haber una única clase pública, sino, el compilador se quejará. El resto de clases de esa unidad de compilación, si es que hay alguna, quedan ocultas para todo lo exterior al paquete al no ser pública, y constituyen clases de "apoyo" para la clase pública principal.


5: Ocultar la implementación

171

Cuando se compila un fichero .java, se obtiene un fichero de salida que tiene exactamente el mismo nombre pero tiene extensión .class por cada clase del fichero .java. Por tanto, se puede acabar teniendo bastantes ficheros .class partiendo de un número pequeño de ficheros .java. Si se programa haciendo uso de un lenguaje compilado, puede que uno esté acostumbrado a que el compilador devuelva un fichero en un formato intermedio (generalmente un fichero "obj") que s e empaqueta junto con otros de su misma clase utilizando, bien un montador (para crear un fichero ejecutable) o una biblioteca. Java no funciona así. Un programa en acción es un compendio de ficheros .java, que pueden empaquetarse y comprimirse en un fichero JAR (utilizando la herramienta jar de Java). El intérprete de Java es el responsable de encontrar, cargar e interpretar estos ficheros1. Una biblioteca también es un conjunto de estos ficheros de clase. Cada fichero tiene una clase que es pública (no es obligatorio introducir una clase pública, pero lo habitual es hacerlo así), de forma que hay un componente por cada fichero. Si se desea indicar que todos estos componentes (que se encuentran en sus propios ficheros separados .java y .class) permanezcan unidos, es necesaria la intervención de la palabra clave package. Cuando se dice:

1

package mipaquete;

(al principio de un archivo), si se usa la sentencia package, ésta debe aparecer en la primera línea que no sea un comentario del fichero), se está indicando que esa unidad de compilación es parte de una biblioteca de nombre mipaquete. 0, dicho de otra forma, se está diciendo que el nombre de la clase pública incluida en esa unidad de compilación se encuentra bajo el paraguas del nombre, mipaquete, y si alguien quiere utilizar el nombre, deben, o bien especificar completamente el nombre

o bien usar la palabra clave import en combinación con mipaquete (utilizando las opciones descritas previamente). Fíjese que la convención para los nombres de paquete de Java dice que se usen únicamente letras minúsculas, incluso cuando hay más de una palabra. Por ejemplo, supóngase que el nombre del fichero es MiClase.java. Esto significa que puede haber una y sólo una clase pública en ese fichero, y el nombre de esa clase debe ser MiClase (incluidas las mayúsculas y minúsculas): package mipaquete; public class MiClase // . . .

{

Ahora, si alguien desea usar MiClase o, por cualquier motivo, cualquiera de las clases públicas de mipaquete, debe usar la palabra clave import para lograr que estén disponibles el/los nombres de mipaquete. La alternativa es dar el nombre completo:

1

rnipaquete.MiClase m

=

new mipaquete.MiClase ( )

' No hay nada en Java que obligue al uso de un chero ejecutable.

;

intérprete. Existen compiladores de código nativo Java que generan un único fi-


172

Piensa en Java

La palabra clave import puede lograr lo mismo pero de manera bastante más clara: import m i p a q u e t e

//

. . .

MiClase m

=

Miclase();

Merece la pena recordar que lo que las palabras clave package e import permiten hacer, como diseñador de bibliotecas, es dividir el espacio de nombres único y global, de forma que no se tengan colisiones de nombres, sin que importe cuánta gente se conecte a Internet y empiece a escribir clases en Java.

Creando nombres de paquete Únicos Podría observarse que, dado que un paquete nunca se llega a "empaquetar" en un fichero único, un mismo fichero podría estar constituidc por muchos ficheros .class, y esto podría ser fuente de desorden y confusión. Para evitarlo, algo lógico es ubicar todos los ficheros .java de un paquete particular en un mismo directorio; es decir, hacer uso de la estructura de ficheros jerárquica del sistema operativo y sacar provecho de ella. Ésta es una de las maneras en que Java referencia el problema del desorden; se verá otra manera después, cuando se presente la utilidad jar. La agrupación de los ficheros de paquete en un único subdirectorio soluciona otros dos problemas: la creación de nombres de paquete únicos, y la localización de esas clases que podrían estar enterradas en algún lugar de la estructura de directorios. Esto se logra, tal y como se presentó en el Capítulo 2, codificando camino de localización del fichero .class en el nombre del paquete. El compilador obliga a que esto sea así, pero por convención, la primera parte del nombre de un paquete es el nombre del dominio Internet del creador de la clase, eso sí, dado la vuelta. Dado que está garantizado que los nombres de dominio de Internet sean únicos, si se sigue esta convención se garantiza que el nombre del paquete sea único y, por consiguiente, nunca habrá colisiones de nombres (es decir, hasta que se pierde el nombre de dominio y alguien se hace con él y empieza a escribir código Java con los mismos nombres de ruta con los que lo hizo). Por supuesto, si se dispone de un nombre de dominio propio, es necesario fabricar en primer lugar una combinación única (como, por ejemplo, la formada por el nombre y apellidos) para crear nombres de paquete únicos. Si se ha decidido comenzar a publicar código Java merece la pena el esfuerzo, relativamente pequeño, de conseguir en primer lugar un nombre de dominio.

La segunda parte de este truco es la resolución del nombre de paquete en un directorio de la máquina, de forma que cuando se ejecuta un programa Java y necesita cargar el fichero .class (lo que ocurre dinámicamente, en el punto en el que el programa necesite crear un objeto de esa clase en particular, o la primera vez que se accede a un miembro estático de la clase), pueda localizar el directorio en el que reside el fichero .class. El intérprete de Java procede de la siguiente forma. En primer lugar, encuentra la variable de entorno CLASSPATH (establecida mediante el sistema operativo, a veces por parte del programa de instalación de Java, o una herramienta basada en Java de la propia máquina). CLASSPATH contiene uno o más directorios utilizados como raíz para la búsqueda de ficheros .class. A partir de esa raíz, el intérprete toma el nombre de paquete y reemplaza cada punto por una barra para generar un


5: Ocultar la implementación

173

nombre relativo a la raíz CLASSPATH (de forma que el paquete foo.bar.baz se convierte en foo\bar\baz o en foo/bar/baz en función del sistema operativo instalado). A continuación, se concatena este nombre con las distintas entradas de la variable CLASSPATH. Es en este momento cuando se busca por el archivo .class que coincida en nombre con la clase que se está intentando crear (también busca algunos directorios estándares relativos al directorio en el que reside el intérprete Java) . Para entenderlo, considérese mi nombre de dominio, que es bruceeckel.com. Dando la vuelta a esto, com.bruceecke1 establece el único nombre global a utilizar en todas mis clases. (La extensión com, edu, org, etc., se ponía en mayúsculas en las primeras versiones de los paquetes Java, pero esto se ha cambiado en Java 2, de forma que todo el nombre de paquete se escribe en minúsculas.) Posteriormente se puede subdividir este nombre diciendo que se quiere crear una biblioteca llamada simple, por lo que acabaremos con un nombre de paquete: package com.bruceeckel.simp1e;

Ahora, este nombre de paquete puede usarse como un espacio de nombre paraguas para los siguientes dos archivos: / / : com:bruceeckel:simple:Vector.java / / Creando un paquete. package com.bruceeckel.simple; public class Vector { public Vector ( ) { System.out.println( "com.bruceeckel.util.Vector");

1 1

///:-

Cuando uno crea sus propios paquetes, se descubre que la sentencia package debe ser la primera del archivo de código que no sea un comentario dentro del archivo. El segundo archivo es muy parecido: / / : com:bruceeckel:simple:Lista.java / / Creando un paquete. package com.bruceeckel.simp1e;

public class Lista { public Lista() { System.out.println( "com.bruceeckel.util.Lista");

Ambos ficheros se encuentran ubicados en el subdirectorio:


174

Piensa en Java

Si se empieza a recorrer esta trayectoria se puede componer el nombre de paquete com.bruceeckel.simple, pero ¿qué ocurre con la primera parte de la trayectoria? De esto se encarga la variable de entorno CLASSPATH:

Puede verse que la variable CLASSPATH puede contener más de un directorio de búsqueda, todos ellos alternativos. Sin embargo, hay una variación cuando se usan archivos JAR. Se debe poner el nombre del archivo JAR en la trayectoria de clases CLASSPATH, no sólo la trayectoria en la que se encuentra. Así, para un JAR de nombre uva.jar, esta variable será:

Una vez que se ha establecido correctamente el valor de esta variable, buscará el archivo en cualquiera de sus directorios: / / : c05:PruebaBiblioteca.java / / Utiliza la biblioteca. import com.bruceeckel.simple.*; public class PruebaBiblioteca { public static void main (String[] args) Vector v = new Vector ( ) ; List 1 = new List 0 ;

{

Cuando el compilador encuentra la sentencia import, empieza a buscar en los directorios especificados por CLASSPATH, buscando el subdirectorio com\bruceecker\simple, y buscando después los ficheros compilados de nombres adecuados (Vector.class para Vector y List.class para List). (Fíjese que, tanto las clases, como los métodos deseados de Vector y List, deben ser públicos). Establecer la variable CLASSPATH era tan problemático para los usuarios de Java principiantes (como lo era para mí cuando empecé) que Sun ha hecho el JDK de Java 2 algo más inteligente. Se descubrirá que, al instalarlo, incluso si no se establece un CLASSPATH, se podrán compilar y ejecutar programas básicos de Java. Para compilar y ejecutar el paquete código de este libro (disponible en el CD ROM empaquetado junto con este libro, o en www. BruceEckel.corn), sin embargo, se necesitará hacer algunas modificaciones al CLASSPATH (éstas se explican en el paquete de código fuente).

Colisiones ¿Qué ocurre si se importan dos bibliotecas vía * que incluyen los mismos nombres? Por ejemplo, supóngase que un programa hace: import com.bruceeckel.simple.*; import java.uti1. *;


5: Ocultar la implementación

175

Dado que java.util.* también contiene una clase Vector, esto causa una colisión potencial. Sin embargo, mientras no se escriba el código que, de hecho, cause la colisión, todo va bien -esto es bueno porque de otra forma, uno podría acabar tecleando multitud de código para evitar colisiones que nunca ocurrirían. La colisión ocurre si ahora se intenta crear un Vector: Vector v

=

new Vector

();

¿A qué Vector se refiere? El compilador no puede saberlo, y tampoco puede el lector. Por tanto el compilador se queja y obliga a especificar. Si se desea el Vector estándar de Java, por ejemplo, hay que decir: java.uti1 .Vector v

=

new java.uti1 .Vector ( )

;

Dado que esto (junto con la variable CLASSPATH) especifica completamente la localización de ese Vector, no hay necesidad de la sentencia import.java.util.*, a menos que se esté utilizando algo más de java.uti1.

Una biblioteca de herramientas a medida Con estos conocimientos, ahora cada uno puede crear sus propias bibliotecas de herramientas para reducir o eliminar el código duplicado. Considérese, por ejemplo, que se está creando un alias para System.out.println( ) para reducir el código a teclear. Éste podría ser parte de un paquete llamado herramientas: / / : com:bruceeckel:herramientas:P.java

/ / El atajo P.rint y P.rintln. package

com.bruceecke1.herramientas;

public class P { public static void rint(String S) { System.out .print (S);

1 public static void rintln(String S) System-out.println (S); 1 1 ///:-

{

Se puede usar este atajo para usar un String, bien con un retorno de carro al final (P.rintln( )) o sin él (P.rint( )). Se puede adivinar que este archivo debe estar ubicado en un directorio de los especificados en CLASSPATH, y que continúe por com/bruceeckel/herramientas. Una vez compilado, el fichero P.class puede usarse en cualquier lugar del sistema con una sentencia import / / : c05:PruebaHerramienta.java / / Utiliza la biblioteca herramientas.


176

l

Piensa en Java

import com.bruceeckel.herramientas.*; public class PruebaHerramienta { public static void main (String[] args) { P.rintln ( " ; Disponible de ahora en adelante ! " ) ; P.rintln("" + 100) ; / / Obligar a que sea un String P.rintln ( " " t 100L) ; P.rintln ( " " + 3.14159) ;

Obsérvese que se puede forzar a cualquier objeto a transformarse en una representación en forma de String, poniéndolos en una expresión String; en el caso anterior, se hace i?so de un truco: comenzar la expresión con un String vacío. Pero esto recuerda una observación interesante. Si se invoca a System.out.println(lO0),funciona sin tener que convertirlo a String. Con algo de sobrecarga, se puede conseguir que la clase P haga también esto (planteado como ejercicio al final del presente capítulo). Por tanto, de ahora en adelante, cuando construya una nueva utilidad, se puede añadir al directorio herramientas. (O al directorio util o herramientas de cada uno.)

Utilizar el comando import para cambiar el comportamiento Una característica que Java no ha heredado de C es la compilación condicional, que permite modificar un switch y obtener distintos comportaniieiitos sin necesidad de variar ninguna otra parte del

código. La razón por la que esta característica no se incluyó en Java es probablemente el hecho de que se utiliza en C fundamentalmente para resolver problemas de multiplataforma: se compilan distintas porciones de código en función de la plataforma para la que se está compilando cada código. Puesto que se pretende que Java sea multiplataforma automáticamente, una característica así no es necesaria. Sin embargo, hay otros usos de gran valor en la compilación condicional. Un uso muy común es la depuración de código. Los aspectos de depuración se habilitan durante el desarrollo, y se deshabilitan en el lanzamiento del producto. A Allen Holub (www.holub.com) se le ocurrió la idea de utilizar paquetes para simular la compilación condicional. Hizo uso de esta idea para crear una versión Java del mecanismo de afirmaciones -tan útil en C-, mediante el que se puede decir "esto debería ser verdad" o "esto debería ser falso" y si la sentencia no está de acuerdo con el afirmación, ya se averiguará. Este tipo de herramienta supone una gran ayuda durante la fase de depuración. He aquí la clase que se utilizará para depuración: / / : com:bruceeckel:herramientas:depurar:Afirmacion.java / / Herramienta de aserto para la depuración; package com.bruceeckel.too1s.debug;


5: Ocultar la implementación

177

public class Afirmacion { private static void error(String msg) { System.err .println (msg); 1 public final static void es-cierto(boo1ean exp) { if ( ! exp) error ("Fallo la af irmacion") ; 1 public final static void es-falso(boo1ean exp) { if (exp) error ("Fallo la afirmacion") ; 1 public final static void es-cierto (boolean exp, String mensaje) { if ( ! exp) error ("Fallo la af irmacion: " + mensaje) ; }

public final static void es-falso(boo1ean exp, String msg) { if (exp) error ("Fallo la afirmacion: "

+ mensaje) ;

1 1 ///:-

Esta clase simplemente encapsula pruebas de valores lógicos, que imprimen mensajes de error si fallan. En el Capítulo 10, se conocerá una herramienta más sofisticada para tratar con errores, denominada manejo de excepciones, pero el método error( ) será suficiente mientras tanto. La salida se imprime en el "flujo de datos" de la consola de error estándar escribiendo en System.err. Cuando se desee hacer uso de esta clase, basta con añadir en el programa la línea;

import com.bruceeckel.herramientas.depurar.*;

Para retirar las afirmaciones que pueda lanzar el código, se crea una segunda clase Afirmacion, pero en un paquete distinto: / / : com:bruceeckel:herramientas:depurar:Afirmacion.java / / Desactivar la salida de la afirmacion / / de forma que se pueda lanzar el programa. Package com.bruceeckel.herramientas; public class Afirmacion { public final static void es-cierto (boolean exp) public final static void es-falso (boolean exp) { public final static void es-cierto (boolean exp, String mensaje) { } public final static void es falso(boo1ean exp, String mensaje) { } 1 //Y-

{ } }


178

Piensa en Java

Ahora, si se cambia la sentencia import anterior a: import com.bruceeckel.herramientas.*;

El programa dejará de imprimir afirmaciones. He aquí un ejemplo: / / : c05:PruebaAfirmacion.java / / Demostrando la herramienta de afirmación. / / Comentar y quitar el comentario / / de la linea siguiente para cambiar / / el comportamiento del aserto: import com.herramientas.depurar.debug.*; / / import com.bruceeckel.herramientas.*;

public class PruebaAfirmacion { public static void main (String[] args) { afirmacion.es-cierto ( (2 + 2) == 5) ; afirmaci0n.e~ -falso((1 t 1) == 2) ; af irmacion.es-cierto ( (2 + 2) == 5, "2 + 2 == 5"); afirmaci0n.e~ falso( (1 + 1) == 2, "1 +1 ! = 2");

1 1 ///:-

Al cambiar el paquete que se importa, se cambia el código de la versión en depuración a la versión de producción. Esta técnica puede usarse para cualquier tipo de valor condicional.

Advertencia relativa a l uso de

paquetes

Merece la pena recordar que cada vez que se cree un paquete, implícitamente se está especificando una estructura de directorios al dar un nombre a un paquete. El paquete debe residir en el directorio indicado por su nombre, que debe ser un directorio localizable a partir de CLASSPATH. Experimentar con la palabra clave package, puede ser un poco frustrante al principio, puesto que, a menos que se adhiera al nombre del paquete la regla de trayectorias de directorios, se obtendrán numerosos mensajes en tiempo de ejecución que indican que no es posible localizar una clase en particular, incluso si esa clase reside en ese mismo directorio. Si se obtiene uno de estos mensajes, debe tratar de modificar la sentencia package, y cuando funcione se sabrá dónde residía el problema.

Modificadores de acceso en Java Al utilizarlos, los modificadores de acceso public, protected y private se ubican delante de cada definición de cada miembro de la clase, sea un atributo o un método. Cada modificador de acceso controla el acceso sólo para esa definición en particular. Éste es diferente a C++,lenguaje en el que


5: Ocultar la implementación

179

el controlador de acceso controla todas las definiciones que lo sigan hasta la aparición del siguiente modificador de acceso. De una manera u otra, todo tiene asignado algún tipo de modificador de acceso. En las secciones siguientes, se aprenderán los distintos tipos de accesos, comenzando por el acceso por defecto.

"Amistoso" ¿Qué ocurre si no se indica ningun tipo de especificador de acceso, como en todos los ejemplos anteriores de este capítulo? El acceso por defecto no tiene ninguna palabra clave asociada, pero generalmente se le denomina acceso "amistoso". Significa que todas las demás clases del paquete actual tienen acceso al miembro amistoso, pero de cara a todas las clases de fuera del paquete, el miembro aparenta ser privado. Dado que una unidad de compilación -un fichero- puede pertenecer sólo a un único paquete, todas las clases de una única unidad de compilación son automáticamente "Amistosas" entre sí. Por consiguiente, se dice que los elementos "Amistosos" tienen acceso paquete. El acceso amistoso permite agrupar clases relacionadas en un mismo paquete de forma que éstas puedan interactuar entre sí de manera sencilla. Al poner clases juntas en un paquete (garantizando por consiguiente el acceso mutuo "Amistoso" a sus miembros; por ejemplo, marcándolos como amigos) se "posee" el código de ese paquete. Tiene sentido que el único código que se posee debería tener acceso amistoso al resto de código propio. Podría decirse que el acceso amistoso da un significado o razón para agrupar juntas las clases de un paquete. En muchos lenguajes, la forma de organizar las definiciones de los ficheros puede ser obligatoria, pero en Java obliga a que cada uno las organice de manera sensata. Además, probablemente se excluirán las clases que no deberían tener acceso a las clases que están siendo definidas en el paquete actual.

La clase controla qué código tiene acceso a sus miembros. No hay ningún truco para "irrumpir" en ella. No se puede mostrar el código de otros paquetes y decir: "iHola, soy un amigo de Bob!", y esperar que se vean los miembros protegidos, "amistosos", y privados de Bob. La única manera de garantizar los accesos a un miembro es: Hacer el miembro público. Posteriormente, todo el mundo, en todas partes, podría acceder a él. Hacer el miembro amistoso no indicando ningún especificador de acceso, y poner las otras clases en el mismo paquete. Así, las otras clases pueden acceder al miembro. Como se verá en el Capítulo 6, cuando se presente la herencia, una clase heredada puede acceder a un miembro protegido al igual que a un miembro público (pero no a los miembros privados). Puede acceder a los miembros "amistosos" sólo si las dos clases se encuentran en el mismo paquete. Pero no hay que preocuparse de esto ahora. Proporcionar métodos "obtener/establecer" ('ket/set? que lean y cambien el valor. Éste es el enfoque más habitual en términos de POO, y es fundamental para los JavaBeans como se verá en el Capítulo 13.


180

Piensa en Java

public: acceso a interfaces Cuando se usa la palabra clave public, significa que la declaración de miembro que continúe inmediatamente a public estará disponible a todo el mundo, y en especial al programador cliente que hace uso de la biblioteca. Supóngase que se define un paquete postre, que contiene la siguiente unidad de compilación: / / : c05:postre:Galleta.java / / Crea una biblioteca. package c05.postre;

public class Galleta { public Galleta 0 i System.out.println("Constructor de Galleta");

1 void morder ( ) 1 ///:-

{

System-out.println ("morder");

}

Recuérdese que Galleta.java debe residir en un subdirectorio de nombre postre, en un directorio bajo c 0 5 (que se corresponde con el Capítulo 5 de este libro) que debe estar bajo uno de los directorios de CLASSPATH. No hay que cometer el error de pensar que Java siempre buscará en el directorio actual como uno de los directorios de partida para la búsqueda. Si no se tiene un '.' como una de las rutas del CLASSPATH, Java no buscará ahí. Ahora, si se crea un programa que haga uso de Galleta: //:

c05:Cena.java

/ / Hace uso de la biblioteca. import c05.postre.*; public class Cena { public Cena ( ) { System.out.println("Constructor cena");

1 public static void main(String[] args) { Galleta x = new Galleta() ; / / ! x.morder ( ) ; / / No se puede acceder

se puede crear un objeto Galleta, dado que su constructor es público y la clase es pública. (Se profundizará más tarde en el concepto de público.) Sin embargo, el método morder( ) es inaccesible dentro de Cena.java puesto que morder( ) es amistoso sólo dentro del paquete postre.


5: Ocultar

la implementación

181

El paquete por defecto Uno podría sorprenderse de descubrir que el código siguiente compila, incluso aunque aparenta transgredir las reglas: / / : c05:Tarta. java / / Accede a una clase de una / / unidad de compilación distinta. class Tarta { public static void main (String[] args) Pastel x = new Pastel 0 ; x.f

{

o;

1

En un segundo archivo del mismo directorio: / / : c05:Pastel. java / / La otra clase. class Pastel { void f O { System.out .println ("Pastel.f O 1 ///:-

") ; )

Inicialmente uno podría pensar que se trata de archivos completamente independientes, y sin embargo, Tarta es incluso capaz de crear un objeto Pastel, e invocar a su método f( ). (Fíjese que debe

tenerse el '.' en CLASSPATH para que los archivos se compilen.) Normalmente se pensaría que Pastel y f( ) son amistosos y por consiguiente, no están disponibles para Tarta. Son amistosos -hasta ahí es correcto. La razón por la que están disponibles en Tarta.java es que se encuentran en el mismo directorio y no tienen ningún nombre de paquete explícito. Java trata a los archivos así como si fueran parte implícita del "paquete por defecto" de ese directorio, y por consiguiente, amistoso para el resto de ficheros del directorio.

private: jeso no se toca! La palabra clave private significa que nadie puede acceder a ese miembro excepto a través de los métodos de esa clase. Otras clases del mismo paquete no pueden acceder a miembros privados, de forma que es como si se estuviera incluso aislando la clase contra uno mismo. Por otro lado, no es improbable que un paquete esté construido por varias personas que colaboran juntas, de forma que privado permite cambiar libremente ese miembro sin necesidad de preocuparse de si el cambio influirá a otras clases del mismo paquete. El acceso "amistoso" al paquete por defecto proporciona un nivel de ocultación bastante elevado; recuerde, un miembro "amistoso" es inaccesible para el usuario del paquete. Esto está bien, dado que el acceso por defecto es el que se usa normalmente (y el que se lograría si se olvida añadir


182

Piensa en Java

algún control de acceso). Por consiguiente, uno generalmente pensaría en lo referente al acceso a los miembros de un programa, que habría que hacer éstos explícitamente públicos, y como resultado, puede que inicialmente no se piense en usar la palabra clave private a menudo. (Lo cual es distinto en C++.) Sin embargo, resulta que el uso consistente de private es muy importante, especialmente cuando está involucrada la ejecución multihilo. (Como se verá en el Capítulo 14.) He aquí un ejemplo del uso de private: / / : cO5 :Helado.java / / Demuestra el uso de la palabra clave "private".

class Vainilla { private Vainilla ( ) { } static Vainilla prepararvainilla0 return new Vainilla ( ) ; 1

{

\

public class Helado { public static void main(String[] args) { / / ! Vainilla x = new Vainilla() ; Vainilla x = Vainilla.prepararVainilla0;

1 1

111:-

Esto muestra un ejemplo de cómo el modificador privado resulta útil: se podría querer controlar cómo se crea un objeto y evitar que alguien pueda acceder directamente a un constructor particular (o a todos ellos). En el ejemplo de arriba, no se puede crear un objeto Vainilla a través de su constructor; por el contrario, debe invocarse al método prepararvainilla( )'. Puede declararse privado cualquier método del que tengamos la seguridad de que no es más que un método "ayudante" para esa clase, para asegurar que no se use accidentalmente en ningún otro lugar del paquete, y por consiguiente, prohibir a uno mismo cambiar o eliminar el método. Construir un método privado garantiza que se conserve esta opción.

Lo mismo es válido para un campo privado dentro de una clase. A menos que se deba exponer la implementación subyacente (lo cual es una situación mucho más rara de lo que se podría pensar), deberían hacerse privados todos los campos. Sin embargo, sólo porque una referencia a un objeto sea privado dentro de su clase, no es imposible que cualquier otro objeto pueda tener una referencia pública al mismo objeto. (Apéndice A para aspectos relativos al "uso de alias".)

* Hay otro efecto en este caso: dado que el constructor por defecto es el único definido, y éste es privado, evitará la herencia de esta clase. (Un aspecto que se detallará en el Capitulo 6.)


5: Ocultar la implementación

183

protected: "un tipo de amistades" Entender el especificador de acceso protegido supone ir algo más allá. En primer lugar, uno debería ser consciente de que no necesita entender esta sección para continuar a lo largo de este libro hasta llegar a la herencia (Capítulo 6). Pero de manera comparativa, he aquí una breve descripción y ejemplo utilizando protected.

La palabra clave protected está relacionada con un concepto denominado herencia, que toma una clase existente y le añade nuevos miembros sin tocar la clase ya existente, a la que se denomina clase base. También se puede cambiar el comportamiento de los miembros existentes de la clase. Para heredar de una clase existente, se dice que la nueva clase hereda de una ya existente, como: class E00 extends Bar

{

El resto de la definición de la clase es exactamente igual. Si se crea un nuevo paquete y se hereda desde una clase de otro paquete, los únicos miembros a los que se tiene acceso son los miembros públicos del paquete original. (Por supuesto, si se lleva a cabo la herencia dentro del mismo paquete, se tiene el acceso de paquete normal a todos los miembros "amistosos".) Algunas veces, el creador de la clase base desea tomar un miembro particular y garantizar el acceso a las clases derivadas, pero no a todo el mundo. Esto es lo que hace el modo protegido. Si se hiciera referencia de nuevo al fichero Galleta.java, la siguiente clase no puede acceder al miembro "amistoso": / / : c05:GalletaChocolate.java / / No puede acceder a un miembro amistoso. //

de otra clase.

import c05.postre.*; public class GalletaChocolate extends Galleta public Galletachocolate() { System.out.println( "Constructor de GalletaChocolate");

{

1 public static void main (String[] args) { GalletaChocolate x = new Galletachocolate(); / / ! x.morder0; / / No se puede acceder a morder.

Una de las cosas más interesantes de la herencia es que si existe un método morder( ) en la clase Galleta, también existe en cualquier clase heredada de Galleta. Pero dado que morder( ) es "amistoso" para los otros paquetes, no podrá utilizarse en éstos. Por supuesto, se puede hacer que sea público, pero entonces todo el mundo tendría acceso y quizás eso no es lo que se desea. Si se cambia la clase Galleta, como sigue: public class Galleta

{


184

Piensa en Java

public Galleta() { System.out.println("Constructor de galletas"); 1 protected void morder ( ) { System-out.println ("morder1') ;

entonces morder( ) sigue teniendo acceso "amistoso" dentro del paquete postre, pero también es accesible a cualquiera que herede de Galleta. Sin embargo, no es público.

Interfaz e implementación El control de accesos se suele denominar ocultación de la información. Al hecho de envolver datos y miembros dentro de las clases, en combinación con el ocultamiento de la información, se le suele denominar encapsulación3.El resultado es un tipo de datos con sus propias características y comportamientos. El control de accesos pone límites dentro de un tipo de datos por dos razones importantes. La primera es establecer qué es lo que pueden y lo que no pueden usar los programadores cliente. Se pueden construir los mecanismos internos dentro de la estructura sin tener que preocuparse de que los programadores clientes traten de manipular accidentalmente las interioridades como parte de la interfaz, que es lo que deberían estar usando. Esto nos presenta directamente en la segunda razón, que es separar la interfaz de la implementación. Si la estructura se utiliza en un conjunto de programas, los programadores clientes no pueden hacer nada más que enviar mensajes al interfaz público, entonces es posible cambiar cualquier cosa que no sea público (por ejemplo, "amistoso", protegido o privado) sin necesidad de requerir modificaciones en el código cliente. Ahora nos encontramos en el mundo de la programación orientada a objetos, donde una clase describe, de hecho, "una clase de objetos", tal y como se describiría una clase de pescados o una clase de pájaros. Cualquier objeto que pertenezca a esta clase compartirá estas características y comportamientos. La clase es una descripción de lo que parecen y de cómo se comportan los objetos de este tipo. En el lenguaje original de POO, Simula-67, la palabra clave class se utilizaba para describir un nuevo tipo de datos. La misma palabra clave se ha venido utilizando en la mayoría de lenguajes orientados a objetos. Éste es el punto más importante de todo el lenguaje: la creación de nuevos tipos de datos que son más que simples cajas contenedoras de datos y métodos.

La clase es el concepto fundamental en Java. Es una de las palabras clave que no se pondrá en negrita en este libro -pues resultaría molesto hacerlo con una palabra que se repite tan a menudo.

Sin embargo, la gente suele denominar "encapsulación" únicamente al ocultamiento de información.


5: Ocultar la implementación

185

Por claridad, puede que se prefiera un estilo de creación de clases que ponga los miembros públicos al principio, seguidos de los miembros protegidos, amistosos y privados. La ventaja es que el usuario de la clase puede ir leyendo de arriba hacia abajo y ver primero lo que más le importa (los miembros públicos, que es a los que puede acceder desde fuera del archivo) y dejar de leer cuando encuentre los miembros no públicos, que son parte de la implementación interna. public class x { public void pub1 ( ) { public void pub2 ( ) { public void pub3 O { private void privl() private void priv2 ( ) private void priv3 ( ) private int i; / / - - -

. . . */} . . . */} * . . . */} { /* . . . */ { * . . . */ * *

/*

{

. . . */

} } }

1

Esto la hará simplemente un poco más fácil de leer, puesto que la interfaz y la implementación siguen estando entremezclados. Es decir, sigue siendo necesario ver el código fuente -la implementación, porque está justo ahí, dentro de la clase. Además, la documentación en forma de comentarios soportada por javadoc (descrito en el Capítulo 2) resta la importancia de la legibilidad del código para el programador cliente. Mostrar la interfaz al consumidor de una clase es verdaderamente el trabajo del navegador de clases o class browser, una herramienta cuyo trabajo es mirar en todas las clases disponibles y mostrar lo que se puede hacer con ellas (por ejemplo, qué miembros están disponibles) de forma útil. Para cuando se lea el presente texto, cualquier buena herramienta de desarrollo Java debería incluir este tipo de navegadores.

Acceso a clases En Java, los especificadores de acceso pueden usarse también para determinar qué clases estarán disponibles dentro de una biblioteca para los usuarios de esa biblioteca. Si se desea que una clase esté disponible para un programador cliente, se coloca la palabra clave public en algún lugar antes de la llave de apertura del cuerpo de la clase. Esto controla si el programador cliente puede incluso crear objetos de esa clase. Para controlar el acceso a una clase, debe aparecer el especificador antes de la palabra clave class. Por consiguiente, se puede decir: public class Componente

{

Ahora, si el nombre de la biblitoeca es mibiblioteca, cualquier programador cliente puede acceder a Componente diciendo import mibiblioteca.Componente;

import mibiblioteca.*;


186

Piensa en Java

Sin embargo, hay un conjunto de restricciones extra: 1. Solamente puede haber una clase pública por cada unidad de compilación o fichero. La idea es que cada unidad de compilación tenga una única interfaz pública representada por esa clase pública. Puede tener tantas clases "amistosas" de soporte como se desee. Si se quiere tener más de una clase pública dentro de una unidad de compilación, el compilador mostrará un mensaje de error. 2.

El nombre de la clase pública debe coincidir exactamente con el nombre del archivo que contenga la unidad de compilación, incluyendo las mayúsculas. Por tanto, para componente, el nombre del archivo debe ser Componente.java y no componente.java o COMPONENTE.java. De nuevo, si éstos tampoco coinciden, se obtendrá también un error de tiempo de compilación.

3.

Es posible, aunque no habitual, que exista alguna unidad de compilación sin ninguna clase pública. En este caso, se puede dar al archivo el nombre que se desee.

¿Qué ocurre si se tiene una clase dentro de mibiblioteca que se está utilizando para llevar a cabo las tareas que hace Componente o cualquier otra clase pública de mibiblioteca? Nadie desea llegar hasta el punto de tener que crear documentación para el programador cliente, y pensar que algún tiempo después podría desearse cambiar completamente las cosas y arrancar todas esas clases, para sustituirlas por otras. Para tener esta flexibilidad, hay que asegurar que ningún programador cliente se vuelva dependiente de unos detalles de implementación particulares incluidos dentro de mibiblioteca. Para lograr esto, simplemente se quita la palabra public de la clase, en cuyo caso se convierte en "amistosa". (La clase puede usarse únicamente dentro de ese paquete.) Fíjese que una clase no puede ser privada (pues esto la convertiría en inaccesible para alguien que no sea la propia clase), ni protegida4. Por tanto, sólo se tienen dos opciones para los accesos a clases: "amistosa" o pública. Si no se desea que nadie más tenga acceso a esa clase, se pueden hacer todos los constructores privados, evitando así que nadie más que uno mismo pueda crear un objeto de esa clase" dentro de un miembro estático de la clase. He aquí un ejemplo: / / : c05:Almuerzo.java / / Muestra el funcionamiento de los especificadores de acceso a clases. / / Hace una clase verdaderamente privada / / con constructores privados: class Sopa { private Sopa O { } / / (1) Permitir la creación a través de un método estático: public static Sopa hacersopa0 { return new Sopa();

1 / (2) Crear un objeto estático nuevo y / / devolver una referencia bajo demanda. De hecho, una clase interna puede ser privada o protegida, pero se trata de un caso especial. Éstos se presentarán en el Capítulo 7. También se puede hacer esto por herencia (Capitulo 6) desde esa clase.


5: Ocultar la implementación

187

/ / (El patrón "singular") : private static Sopa psl = new Sopa(); public static Sopa acceso() { return psl; J

public void f ( )

{ )

class Bocadilo { / / Usa Almuerzo void f ( ) { new Almuerzo ( ) ; }

/ / Sólo se permite una clase pública por fichero: public class Almuerzo { void prueba() { / / ;Esto no se puede hacer! Constructor privado: / / ! Sopa privl = new Sopa ( ) ; Sopa priv2 = Sopa.hacersopa ( ) ; Bocadillo fl = new Bocadillo ( ) ; Sopa.acceso ( ) . f ( ) ;

Hasta ahora, la mayoría de métodos devolvían void o un tipo primitivo, por lo que la definición: public static Sopa acceso() return psl;

{

podría parecer algo confusa a primera vista. La palabra antes del nombre del método (acceso) indica qué devuelve el método. Hasta la fecha, ésta ha sido la mayoría de las veces vacía (void), que quiere decir que no se devuelve nada. Pero también se puede devolver una referencia a un objeto, que es lo que ocurre aquí. Este método devuelve una referencia a un objeto de la clase Sopa.

La clase Sopa muestra como evitar la creación directa de una clase haciendo privados todos los constructores. Recuérdese que si no se crea al menos un constructor explícitamente, se creará automáticarnente el constructor por defecto (un constructor sin parámetros). Si se escribiera el constructor por defecto, éste no se creará automáticamente. Al hacerlo privado, nadie puede crear un objeto de esa clase. Pero ahora ¿cómo puede alguien usarla? El ejemplo de arriba presenta dos o p ciones. En primer lugar se crea un método estático que crea un nuevo objeto Sopa y devuelve una referencia al mismo. Esto podría ser útil si se desea hacer alguna operación extra con la Sopa antes de devolverla, o si se desea mantener la cuenta de cuántos objetos Sopa crear (quizás para restringir la población de objetos de este tipo).

La segunda opción usa lo que se denomina un patrón de diseño, que se describe en Thinking in Patterns with Jaua, descargable de www.BruceEckel.com. Este patrón en particular se denomina un "singular", porque sólo permite la creación de un único objeto. El objeto de clase Sopa se crea como


188

Piensa en Java

un miembro estático privado de Sopa, por lo que hay uno y sólo uno, y solamente se puede conseguir a través del método público de nombre acceso( ). Como se mencionó previamente, si no se desea poner un modificador de acceso para el acceso a una clase, éste es por defecto "amistoso". Esto significa que cualquier otra clase del paquete puede crear un objeto de esa clase, pero no desde fuera del paquete. (Recuérdese que todos los archivos del mismo directorio que no tengan declaraciones explícitas de paquete son implícitamente parte del paquete por defecto de ese directorio.) Sin embargo, si un miembro estático de esa clase es público, el programador cliente puede seguir accediendo al miembro estático incluso aunque no pueda crear un objeto de esa clase.

Resumen En cualquier relación es importante tener unos límites que sean respetados por todas las partes involucradas. Cuando se crea una biblioteca, se establece una relación con el usuario de esa biblioteca -el programador cliente- que es otro programador, que en vez de esto, se encarga unir diverso código para construir una aplicación, o bien de utilizar su biblioteca para construir una aplicación aún más grande. Sin reglas, los programadores cliente pueden hacer lo que quieran con todos los miembros de una clase, incluso si se desea que no manipulen directamente algunos de estos miembros. Todo aparece desnudo al mundo. Este capítulo revisaba cómo se construyen clases a partir de bibliotecas; en primer lugar, se explica cómo se empaquetan clases dentro de una biblioteca, y en segundo, cómo controla la clase el acceso a sus miembros. Se estima que un proyecto de programación en C se empieza a romper entre las 50K y las lOOK 1íneas porque C tiene un único "espacio de nombres" y los nombres empiezan a colisionar, causando una sobrecarga extra de gestión. En Java, la palabra clave package, el esquema de nombrado de paquetes (package) y la palabra clave import dan un control completo sobre los nombres, de manera que se evita de manera sencilla el aspecto de posibles colisiones entre nombres. Hay dos razones por las que controlar el acceso a los miembros. El primero es mantener las manos de los usuarios alejadas de lo que no deberían tocar; las herramientas que son necesarias para las maquinaciones internas de los tipos de datos, pero no forman parte de la interfaz que los usuarios necesitan para resolver sus problemas. Por tanto, hacer los métodos y campos privados es un servicio a los usuarios porque pueden ver fácilmente qué es importante para ellos y qué pueden ignorar. Esto simplifica su grado de entendimiento de la clase.

La segunda y más importante razón para controlar el acceso es permitir al diseñador de bibliotecas cambiar los funcionamientos internos de la clase sin tener que preocuparse de cómo afectará esto al programador cliente. Uno podría construir una clase inicialmente de una forma, y después descubrir que reestructurando el código se logra un aumento considerable de velocidad. Si la interfaz y la implementación están claramente separados y protegidos, se puede acometer este cambio sin forzar al usuario a reescribir su código.


5: Ocultar la implementación

189

Los modificadores de accesos dan en Java un control muy valioso al creador de la clase. Los usuarios de la clase pueden ver clara y exactamente qué es lo que pueden usar y qué ignorar. Y lo que es más importante, la capacidad para asegurar que ningún usuario se vuelva dependiente de ninguna parte de la implementación subyacente de una clase. Si se conoce ésta, como creador de la misma, se puede cambiar la implementación subyacente con el conocimiento de que ningún programador cliente se verá afectado por los cambios, pues éstos no pueden acceder a esa parte de la clase. Cuando se tiene la capacidad de cambiar la implementación subyacente, no sólo se puede mejorar su diseño más tarde, sino que también se tiene la libertad de cometer errores. Sin que importe lo cuidadosamente que se haga la planificación y el diseño, se cometerán errores. Sabiendo que cometer estos errores significan seguro que uno experimentará más, aprenderá mejor y acabará antes su proyecto.

La interfaz pública de una clase es lo que el usuario de hecho, ve, de forma que conseguir que es lo más importante de una clase es acabar haciéndola "bien" durante el análisis y el diseño. E incluso eso permite alguna libertad de acción de cara al cambio. Si no se logra una interfaz la primera vez,

se pueden añadir nuevos métodos, siempre que no se elimine ninguno que los programadores cliente se hayan podido usar en sus códigos.

Ejercicios Las soluciones a determinados ejercicios se encuentran en el documento The Thinking in Java Annotated Solution Guide, disponible a bajo coste en www.BruceEckel.com.

Escribir un programa que cree un objeto ListaArray sin exportaciones explícitas de java.util.*. En la sección "El paquete: la unidad de biblioteca", cambiar los códigos de fragmento relacionados con mipaquete en un conjunto de ficheros Java compilables y ejecutables. En la sección "Colisiones", cambiar los fragmentos de código por un programa, y verificar que verdaderamente se dan colisiones. Generalizar la clase P definida en este capítulo añadiendo todas las versiones sobrecargadas de rint( ) y rintln( ) necesarias para manejar todos los tipos básicos de Java. Cambiar la sentencia import de PruebaAñrmacion.java para habilitar y deshabilitar el mecanismo de afirmaciones. Crear una clase con miembros de datos y métodos públicos, privados, protegidos y "amistosos". Crear un objeto de esa clase y ver quí. tipo de mensajes de compilación se obtienen al intentar acceder a todos los miembros de la clase. Ser conscientes de que las clases del mismo directorio son parte del paquete "por defecto". Crear una clase con datos protegidos. Crear una segunda clase en el mismo archivo con un método que manipule los datos protegidos de la primera clase.


Piensa en Java

Cambiar la clase Galleta como se especifica en la sección "protected:.tipo de amistad.". Verificar que morder( ) no es público. En la sección titulada "Acceso a clases" se encontrarán fragmentos de código que describen mibilioteca y Componente. Crear esta biblioteca y posteriormente crear un Componente en una clase que no sea parte del paquete mibiblioteca. Crear un nuevo directorio y editar la variable CLASSPATH para que incluya ese nuevo directorio. Copiar el archivo P.class (producido al compilar com.burceeckel.herramientas.P.java) al nuevo directorio y cambiar los nombres del fichero, la clase P y los nombres de los métodos. (A lo mejor también se desea añadir alguna salida adicional para ver cómo funciona.) Crear otro programa en un directorio diferente que haga uso de la nueva clase. Siguiendo la forma del ejemplo Almuerzo.java, crear una clase denominada GestorConexion que gestione un array fijo de objetos Conexión. El programador cliente no debe ser capaz de crear explícitamente objetos Conexión, sino que solamente puede crear objetos a través de un método estático de GestorConexion. Cuando el GestorConexion se quede sin objetos, devolverá una referencia null. Probar las clases de main( ). Crear el siguiente fichero en el directorio c05/local (presumiblemente en el CLASSPATH): / / / : c05:local:ClaseEmpaquetada.java package c05.local; class ClaseEmpaquetada { public ClaseEmpaquetada ( ) { System.out.println ( "Creando una clase empaquetada") ; }

1/ / / :

-

Posteriormente, crear el directorio siguiente en un directorio distinto: / / / : c05:exterior:Exterior.java package c05.exterior; import c05.local.*; public class Exterior { public static void main (String[] args) { ClaseEmpaquetada ce = new ClaseEmpaquetada();

Explicar por qué el compilador genera un error. ~Hacerque la clase Exterior sea parte del paquete c05.local cambiaría algo?


6: Reutilizando clases Una de las características más atractivas de Java es la reutilización de código. Pero para ser revolucionario, es necesario poder hacer muchísimo más que copiar código y cambiarlo. Este es el enfoque que se utiliza en los lenguajes procedurales como C, pero no ha funcionado muy bien. Como todo en Java, la solución está relacionada con la clase. Se reutiliza código creando nuevas clases, pero en vez de crearlas de la nada, se utilizan clases ya existentes que otra persona ya ha construido y depurado. El truco es usar clases sin manchar el código existente. En este capítulo se verán dos formas de lograrlo. La primera es bastante directa: simplemente se crean objetos de las clase existente dentro de la nueva clase. A esto se le llama composición, porque la clase nueva está compuesta de objetos de clases existentes. Simplemente se está reutilizando la funcionalidad del código, no su forma. El segundo enfoque es más sutil. Crea una nueva clase como un tipo de una clase ya existente. Literalmente se toma la forma de la clase existente y se le añade código sin modificar a la clase ya existente. Este acto mágico se denomina herencia, y el compilador hace la mayoría del trabajo. La herencia es una de las clases angulares de la programación orientada a objetos y tiene implicaciones adicionales como se verá en el Capítulo 7. Resulta que mucha de la sintaxis y comportamiento son similares, tanto para la herencia, como para la composición (lo cual tiene sentido porque ambas son formas de construir nuevos tipos a partir de tipos existentes). En este capítulo, se aprenderá sobre estos mecanismos de reutilización de código.

Sintaxis de la composición Hasta ahora, la composición se usaba con bastante frecuencia. Simplemente se ubican referencias a objetos dentro de nuevas clases. Por ejemplo, suponga que se desea tener un objeto que albergue varios objetos de tipo cadena de caracteres, un par de datos primitivos, y un objeto de otra clase. En el caso de los objetos no primitivos, se ponen referencias dentro de la nueva clase, pero se definen los datos primitivos directamente: / / : c06:Aspersor.java / / Composición para la reutiliación de código. class FuenteAgua { private String S; FuenteAgua ( ) { System.out .println ("FuenteAgua( ) 11); s = new String ("Construida"); }

1

public String toString()

{

return S;

}


192

Piensa en Java

public class Aspersor { private String valvulal, valvula2, FuenteAgua fuente; int i; float f; void escribir() { System.out .println ("valvulal = " System.out .println ("valvula2 = " System. out .println ("valvula3 = " System.out.println("valvula4 = " System.out.println("i = " + i); System.out.println("f = " t f); System. out .println ("fuente = " +

valvula3, valvula4;

+ valvulal) ; + valvula2) ;

+

valvula3) ;

t valvula4);

fuente) ;

1 public static void main(String[] args) t Aspersor x = new Aspersor ( ) ; x.escribir ( ) ;

1 1 111:Uno de los métodos definidos en FuenteAgua( ) es especial: toString( ). Se aprenderá más adelante que todo objeto no primitivo tiene un método toStnng( ), y es invocado en situaciones especiales cuando el compilador desea obtener un objeto como cadena de caracteres. Por tanto, en la expresión: System. out .println ("fuente

=

" + fuente);

el compilador ve que se está intentando añadir un objeto String ("fuente =") a un objeto FuenteAgua. Esto no tiene sentido porque sólo se puede "añadir" un String a otro String, por lo que dice: "jconvertiré fuente en un Stnng invocando a toString( )! Después de hacer esto se pueden combinar los dos objetos de tipo String y pasar el String resultante a Sytem.out.println( ). Siempre que se desee este comportamiento con una clase creada, sólo habrá que escribir un método toString( ).

A primera vista, uno podría asumir -siendo Java tan seguro y cuidadoso como es- que el compilador podría construir automáticamente objetos para cada una de las referencias en el código de arriba; por ejemplo, invocando al constructor por defecto para FuenteAgua para inicializar fuente. La salida de la sentencia de impresión es de hecho: valvulal valvula2 valvula3 valvula4 i = o f

=

= = = =

null null null null

0.0

fuente

=

null


6: Reutilización de clases

193

Los datos primitivos son campos de una clase que se inicializan automáticamente a cero, como se indicó en el Capítulo 2. Pero las referencias a objetos se inicializan a null, y si se intenta invocar a métodos de cualquiera de ellos, se sigue obteniendo una excepción. De hecho, es bastante bueno (Y útil) poder seguir imprimiéndolos sin lanzar excepciones. Tiene sentido que el compilador no sólo cree un objeto por defecto para cada referencia, porque eso conllevaría una sobrecarga innecesaria en la mayoría de los casos. Si se quieren referencias inicializadas, se puede hacer: 1.

En el punto en que se definen los objetos. Esto significa que siempre serán inicializados antes de invocar al constructor.

2.

En el constructor de esa clase.

3.

Justo antes de que, de hecho, se necesite el objeto. A esto se le llama inicialización perezosa. Puede reducir la sobrecarga en situaciones en las que no es necesario crear siempre el objeto.

A continuación, se muestran los tres enfoques: / / : c06:Banio. java / / Inicialización de constructores con composición.

class Jabon { private String S; Jabon ( ) { System.out .println ("Jabon( ) " ) ; s = new String ("Construido");

1 public String toString0

{

return

S;

}

1 public class Banio { private String / / Inicializando en el momento de la definición: sl = new String ("Contento"), s2 = "Contento", s 3 , s4; Jabon pastilla; int i; float juguete; Banio ( ) { System.out .println ("Dentro del banio ( ) " ) ; s 3 = new String("Gozo"); i = 47; juguete = 3.14f; pastilla = new J a b o n o ;

1


194

Piensa en Java

void escribir() { / / Inicialización tardía: if (s4 == null) s4 = new String ("Temporal"); System.out.println("s1 = " + sl) ; System.out.println("s2 = " + s2); System.out.println("s3 = " + s3); System.out.println("s4 = " + s4); System.out.println("i = " + i); System.out .println ("juguete = " + juguete) ; System.out .println ("pastilla = " + pastilla) ; public static void main (String[] args) Banio b = new Banio(); b. escribir ( ) ;

{

1 1 ///:Fíjese que en el constructor Banio se ejecuta una sentencia antes de que tenga lugar ninguna inicialización. Cuando no se inicializa en el momento de la definición, sigue sin haber garantías de que se lleve a cabo ningún tipo de inicialización antes de que se envíe un mensaje a una referencia a un objeto -excepto la inevitable excepción en tiempo de ejecución. He aquí la salida del programa: Dentro del Banio ( ) Jabon ( ) S1 = Contento S2 = Contento

S3

=

Gozo

S4 = Gozo 1 = 47 Juguete = 3.14 pastilla = Construido

Cuando se invoca al método escribir( ) éste rellena s4 para que todos los campos estén inicializados correctamente cuando se usen.

Sintaxis de la herencia La herencia es una parte integral de Java (y de todos los lenguajes de PO0 en general). Resulta que siempre se está haciendo herencia cuando se crea una clase, pero a menos que se herede explícitamente de otra clase, se hereda implícitamente de la clase raíz estándar de Java Object.

La sintaxis para la composición es obvia, pero para llevar a cabo herencia se realiza de distinta forma. Cuando se hereda, se dice: "Esta clase nueva es como esa clase vieja". Se dice esto en el códi-


6: Reutilización de clases

195

go dando el nombre de la clase, como siempre, pero antes de abrir el paréntesis del cuerpo de la clase, se pone la palabra clave extends seguida del nombre de la clase base. Cuando se hace esto, automáticamente se tienen todos los datos miembro y métodos de la clase base. He aquí un ejemplo: / / : c06:Detergente.java / / Sintaxis y propiedades de la herencia.

class ProductoLimpieza { private String s = new String("Producto de Limpieza"); public void aniadir(String a) { s += a; } public void diluir() { aniadir(" diluir() " ) ; } public void aplicar ( ) { aniadir ( " aplicar ( ) " ) ; } public void frotar ( ) { aniadir ( " fregar ( ) " ) ; } p u b l i c void e s c r i b i r ( ) { System. o u t . p r i n t l n ( S ) ; } public static void rnain(String[] args) { ProductoLimpieza x = new ProductoLimpieza(); x.diluir ( ) ; x.aplicar ( ) ; x. frotar ( ) ; x. escribir ( ) ;

1 1 public class Detergente extends ProductoLimpieza { / / Cambiar un método: public void frotar ( ) { aniadir ( " Detergente. frotar ( ) " ) ; super.frotar ( ) ; / / Llamar a la versión de la clase base

1 / / Añadir métodos al interfaz: public void aclarar ( ) { aniadir ( " aclarar ( ) " ) ; / / Probar la nueva clase: public static void main(String[] args) { Detergente x = new Detergente(); x.diluir ( ) ; x.aplicar ( ) ; x. frotar ( ) ; x. aclarar ( ) ; x. escribir ( ) ; Systern.out .println ("Probando la clase base: " ) ProductoLimpicza.main(args);

}

;

1 1 ///:Esto demuestra un gran número de aspectos. En primer lugar, en el método aniadir( ) de la clase ProductoLimpieza, se concatenan Cadenas de caracteres a S utilizando el operador +=, que


196

Piensa en Java

es uno de los operadores (junto con '+') que los diseñadores de Java "sobrecargaron" para que funcionara con Cadenas de caracteres. Segundo, tanto ProductoLimpieza como Detergente contienen un método main( ). Se puede crear un método main( ) por cada clase que uno cree, y se recomienda codificar de esta forma, de manera que todo el código de prueba esté dentro de la clase. Incluso si se tienen muchas clases en un programa, sólo se invocará al método main( ) de la clase invocada en la línea de comandos. (Dado que main( ) es público, no importa si la clase a la que pertenece es o no pública.) Por tanto, en este caso, cuando se escriba java Detergente, se invocará a Detergente.main( ). Pero también se puede hacer que ProductoLimpieza invoque a ProductoLimpieza.main( ), incluso aunque ProductoLimpieza no sea una clase pública. Esta técnica de poner un método main( ) en cada clase permite llevar a cabo pruebas para cada clase de manera sencilla. Y no es necesario eliminar el método main( ) cuando se han acabado las pruebas; se puede dejar ahí por si hubiera que usarlas para otras pruebas más adelante. Aquí, se puede ver que Detergente.main( ) llama a ProductoLimpieza.main( ) explícitamente, pasándole los mismos argumentos de la línea de comandos (sin embargo, se podría pasar cualquier array de Cadenas de caracteres). Es importante que todos los métodos de ProductoLimpieza sean públicos. Recuerde que si se deja sin poner cualquier modificador de miembro, el miembro será por defecto "amistoso", lo cual permite acceder sólo a los miembros del paquete. Por consiguiente, dentro de este paquete, cualquiera podría usar esos métodos si no hubiera modificador de acceso. Detergente no tendría problemas, por ejemplo. Sin embargo, si se fuera a heredar desde ProductoLimpieza una clase de cualquier otro paquete, ésta sólo podría acceder a las clases públicas. Por tanto, al planificar la herencia, como regla general, deben hacerse todos los campos privados y todos los miembros públicos. (Los miembros protegidos también permiten accesos por parte de clases derivadas; esto se aprenderá más adelante.) Por supuesto, en los casos particulares hay que hacer ajustes, pero ésta es una regla útil. Fíjese que ProductoLimpieza tiene un conjunto de métodos en su interfaz: aniadir( ), diluir( ), aplicar( ), frotar( ) y escribir( ). Dado que Detergente se hereda de ProductoLimpieza (mediante la palabra clave extends) automáticamente se hace con estos métodos en su interfaz, incluso aunque no se encuentren explícitamente definidos en Detergente. Se puede pensar que la herencia, por tanto, es una reutilización del interfaz. (La implementación también se hereda, pero esto no es lo importante.) Como se ha visto en frotar( ), es posible tomar un método que se haya definido en la clase base y modificarlo. En este caso, se podría desear llamar al método desde la clase base dentro de la nueva versión. Pero dentro de frotar( ) no se puede simplemente invocar a frotar( ), dado que eso produciría una llamada reciirsiva, que no es lo que se desea. Para solucionar este problema Java lierie la palabra clavc super que hace referencia a la "superclase" de la cual ha heredado la clase actual. Por consiguiente, la expresión super.frotar( ) llama a la versión que tiene la clase base del método frotar( ).

Al heredar, uno no se limita a usar los métodos de la clase base. También se pueden añadir nuevos métodos a la clase derivada, exactamente de la misma manera que se introduce un método en una clase: simplemente se definen. El método aclarar( ) es un ejemplo de esta afirmación.


6: Reutilización de clases

197

En Detergente.main( ) se puede ver que, para un objeto Detergente, se puede invocar a todos los métodos disponibles, también en ProductoLimpieza y en Detergente (por ejemplo, aclarar( )).

Inicializando la clase base Dado que ahora hay dos clases involucradas -la clase base y la clase derivada- en vez de simplemente una, puede ser un poco confuso intentar imaginar el objeto resultante producido por una clase derivada. Desde fuera, parece que la nueva clase tiene la misma interfaz que la clase base, y quizás algunos métodos y campos adicionales. Pero la herencia no es una simple copia de la interfaz de la clase base. Cuando se crea un objeto de la clase derivada, éste contiene dentro de él un sub objeto de la clase base. Este subobjeto es el mismo que si se hubiera creado un objeto de la clase base en sí. Es simplemente que, desde fuera, el subobjeto de la clase base está envuelto dentro del 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 garantizarlo: llevar a cabo la inicialización en el constructor, invocando al constructor de la clase base, que tiene todo el conocimiento y privilegios apropiados para llevar a cabo la inicialización de la clase base. Java inserta automáticamente llamadas al constructor de la clase base en el constructor de la clase derivada. El ejemplo siguiente muestra este funcionamiento con tres niveles de herencia: / / : c06:Animacion.java / / Llamadas al constructor durante la herencia class Arte { Arte 0 { System.out.println("Constructor de arte");

class Dibujo extends Arte { Dibujo ( ) { System.out .println ("Constructor de dibujo") ;

public class Animacion extends Dibujo { Animacion ( ) { System.out.println("Constructor de animacion");

1 public static void main (String[] args) Animacion x = new Animaciono;

{


198

Piensa en Java

La salida de este programa muestra las llamadas automáticas: Constructor de arte Constructor de dibujo Constructor de animacion

Se puede ver que la construcción se da desde la base "hacia fuera", de forma que se inicializa la clase base antes de que los constructores de la clase derivada puedan acceder a ella. Incluso si no se crea un constructor para Animacion( ), el compilador creará un constructor por defecto que invoque al constructor de la clase base.

Constructores con parametros El ejemplo de arriba tiene constructores por defecto; es decir, no tienen ningún parámetro. Para el compilador es fácil invocarlos porque no hay ningún problema que resolver respecto al paso de parámetros. Si una clase no tiene parámetros por defecto, o si se desea invocar a un constructor de una clase base que tiene parámetros, hay que escribir explícitamente la llamada al constructor de la clase base usando la palabra clave super y la lista de parámetros apropiada: / / : c06:Ajedrez. java / / Herencia, constructores y parámetros. class Juego { Juego (int i) { System.out.println("Constructor de juego");

1

1 class JuegoMesa extends Juego { JuegoMesa(int i) { super (i); System.out.println("Constructor de JuegoMesa");

1 1 public class Ajedrez extends JuegoMesa { Ajedrez ( ) { super (11); System.out.println("Constructor de Ajedrez");

1 public static void main (String[] args) Ajedrez x = new Ajedrez ( ) ;

1 1 ///:-

{


6: Reutilización de clases

199

Si no se invoca al constructor de la clase base de JuegoMesa( ), el compilador se quejará al no poder encontrar un constructor de la forma Juego( ). Además, la llamada al constructor de la clase base debe ser lo primero que se haga en el constructor de la clase derivada. (El compilador así lo recordará cuando no se haga correctamente.)

Capturando excepciones del constructor base Como se acaba de indicar, el compilador obliga a ubicar la llamada al constructor de la clase base, primero dentro del cuerpo del constructor de la clase derivada. Esto simplemente quiere decir que no puede aparecer nada antes de esta llamada. Como se verá en el Capítulo 10, esto también evita que un constructor de una clase derivada capture excepciones que provengan de una clase base. Esto puede suponer un inconveniente en algunas ocasiones.

Combinando la composición y la herencia Es muy frecuente usar la composición y la herencia juntas. El ejemplo siguiente muestra la creación de una clase más compleja, utilizando tanto la herencia como la composición, junto con la inicialización necesaria del constructor: / / : c06:PonerMesa.java / / Combinando la composición y la herencia. class Plato { Plato(int i) { System.out.println("Constructor de plato"); 1

1 class PlatoCena extends Plato { PlatoCena(int i) { super (i); System.out.println( "Constructor de PlatoCena");

1 1

class Utensilio { Utensilio(int i) { System.out.println("Constructor de utensilio");

1 1

1

class Cuchara extends Utensilio

{


200

Piensa en Java

Cuchara(int i) { super (i); System.out .println ("Constructor de cuchara") ; }

1 class Tenedor extends Utensilio { Tenedor(int i) { super (i); System.out.println("Constructor de tenedor");

class Cuchillo extends Utensilio { Cuchilllo(int i) { super (i); System.out.println("Constructor de cuchillo");

1 / / Una manera costrumbrista de hacer algo: class Costumbre { Costumbre(int i) { System.out.println("Constructor de costumbre"); 1

1 public class PonerMesa extends Costumbre Cuchara cc; Tenedor tnd; Cuchillo cch; Platocena pc; PonerMesa(int i) { super (i + 1) ; cc = new Cuchara(i + 2); tnd = new Tenedor (i + 3) ; cch = new Cuchillo(i + 4) ; pc = new PlatoCena(i + 5) ; System.out.println( "Constructor de PonerMesa") 1 public static void main (String [ ] PonerMesa x = new PonerMesa(9 1 1 ///:-

{


6: Reutilización de clases

201

Cuando el compilador obliga a inicializar la clase base, y requiere que se haga justo al principio del constructor, no se asegura de que inicialicemos los objetos miembro, por lo que es conveniente prestar especial atención a esto.

Garantizar u n a b u e n a l i m p i e z a Java no tiene el concepto de método destructor de C++. Este método se invoca automáticamente al destruir un objeto. La razón de su ausencia es probablemente que en Java lo habitual es simplemente olvidarse de esos objetos, más que destruirlos, permitiendo que el recolector de basura reclame esta memoria cuando sea necesario. En muchas ocasiones, esto es bueno, pero hay veces en las que una clase tiene que hacer algunas actividades durante su vida que requieren de limpieza. Como se mencionó en el Capítulo 4, no se puede saber cuándo se invocará al recolector de basura, o incluso, si éste será invocado. Por tanto, si se desea que se limpie algún espacio para una clase, hay que escribir explícitamente un método especial que lo haga, y asegurarse de que el programador cliente sepa que hay que invocar a este método. Por encima de esto -como se describe en el Capítulo 10 ("Manejo de Errores con Excepciones")- hay que protegerse de las excepciones poniendo este tipo de limpieza en una cláusula finally. Considere un ejemplo de un sistema de diseño asistido por computador que dibuja en la pantalla: / / : cO6:SistemaDAC.java / / Asegurando una limpieza adecuada. import java.uti1.*; class Forma { Forma(int i) { System.out.println("Constructor de forma"); 1

void limpiar ( ) { System.out .println ("Limpieza de forma") ;

1

class Circulo extends Forma { Circulo (int i) { super (i); System.out.println("Dibujando un circulo");

1 void limpiar() { System.out.println("Borrando un circulo"); super. limpiar ( ) ; 1

1


202

Piensa en Java

class Triangulo extends Forma { Triangulo(int i) { super (i); System.out.println("Dibujando un triangulo");

1 void limpiar() { System.out .println ("Borrando un triangulo") ; super. limpiar ( ) ;

class Linea extends Forma { private int inicio, fin; Linea (int inicio, int fin) { super (inicio); this . inicio = inicio; this. fin = fin; System.out .println ("Dibujando una linea: " inicio + ", " + fin) ;

+

1 void limpiar() { System.out .println ("Borrando una linea: " + inicio + ", " + fin) ; super. limpiar ( ) ;

1 1 public class SistemaDAC extends Forma { private Circulo c; private Triangulo t; private Linea[] lineas = new Linea[lO] ; SistemaDAC(int i) { super(i + 1); for(int j = O; j < 10; j++) lineas [j] = new Linea (j, j*j) ; c = new Circulo (1); t = new Triangulo (1); System.out.println("Constructor combinado");

1 void limpiar() { System.out.println("SistemaDAC.limpiar()"); / / El orden de eliminaci贸n es inverso al / / orden de inicializaci贸n t.limpiar ( ) ; c.limpiar ( ) ;


6: Reutilización de clases

203

for(int i = 1ineas.length - 1; i >= O; i--) lineas [i]. limpiar ( ) ; super. limpiar ( ) ; public static void main (String[] args) { SistemaDAC x = new SistemaDAC (47); try 1 / / Código y manejo de excepciones. . . } finally { x. limpiar ( ) ;

1

Todo en este sistema es algún tipo de Forma (que en sí es un tipo de Objeto dado que está implícitamente heredada de la clase raíz). Cada clase redefine el método limpiar( ) de Forma además de invocar a la versión de ese método de la clase base haciendo uso de super. Las clases Forma específicas -Círculo, Triángulo y Línea- tienen todas constructores que "dibujan", aunque cualquier método invocado durante la vida del objeto podría ser el responsable de hacer algo que requiera de limpieza. Cada clase tiene su propio método limpiar( ) para restaurar cosas a la forma en que estaban antes de que existiera el objeto. En el método main( ) se pueden ver dos palabras clave nuevas, y que no se presentarán oficialmente hasta el capítulo 10: try y finally. La palabra clave try indica que el bloque que sigue (delimitado por llaves) es una región vigilada, lo que quiere decir que se le da un tratamiento especial. Uno de estos tratamientos especiales consiste en que el código de la cláusula finally que sigue a esta región vigilada se ejecuta siempre, sin que importe cómo se salga del bloque try. (Con el manejo de excepciones, es posible dejar un bloque try de distintas formas no ordinarias.) Aquí, la cláusula finally dice: "Llama siempre a limpiar( ) para x, sin que importe lo que ocurra". Estas palabras claves se explicarán con detalle en el Capítulo 10. Fíjese que en el método de limpieza hay que prestar atención también al orden de llamada de los métodos de limpieza de la clase base y los objetos miembros, en caso de que un subobjeto dependa de otro. En general, se debería seguir la forma ya impuesta por el compilador de C++ para sus destructores: en primer lugar se lleva a cabo todo el trabajo de limpieza específico a nuestra clase, en orden inverso de creación. (En general, esto requiere que los elementos de la clase base sigan siendo accesibles.) Después, se llama al método de limpieza de la clase base, como se ha demostrado aquí. Puede haber muchos casos en los que el aspecto de la limpieza no sea un problema; simplemente se deja actuar al recolector de basura. Pero cuando es necesario hacerlo explícitamente se necesita tanto diligencia como atención.

Orden de recolección de basura No hay mucho en lo que se pueda confiar en lo referente a la recolección de basura. Puede que ni siquiera se invoque nunca al recolector de basura. Cuando se le invoca, puede reclamar objetos en el orden que quiera. Es mejor no confiar en la recolección de basura para nada que no sea reclamar


204

Piensa en Java

memoria. Si se desea que se dé una limpieza, es mejor que cada uno construya sus propios métodos de limpieza, y no confiar en el método finalize( ). (Como se mencionó en el Capítulo 4, puede obligarse a Java a invocar a todos los métodos finalize( ).)

Ocultación de nombres Sólo los programadores de C++ podrían sorprenderse de la ocultación de nombres, puesto que funciona distinto en ese lenguaje. Si una clase base de Java tiene un nombre de método sobrecargado varias veces, la redefinición de ese nombre de método en la clase derivada no esconderá ninguna de las versiones de la clase base. Por consiguiente, la sobrecarga funciona independientemente de si el método se definió en el nivel actual o en una clase base: //: // // //

c06:Ocultar. lava Sobrecargando un nombre de método de una clase base en una clase derivada que no oculta las versiones de la clase base.

class Homer { char realizar(char c) { System.out.println("realizar(char)"); return 'd'; 1

float realizar (float f) { System.out .println ("realizar(float)") ; return 1.0f;

1 1

class Milhouse

{ }

class Bart extends Homer { void realizar (Milhouse m)

{ }

1 class Ocultar { public static void main (String[] args) { Bart b = new Bart(); b. realizar (1); / / realizar (float) usado b.realizar('xV); b.realizar(1.0f); b.realizar (new Milhouse ( ) ) ;

1 1 ///:-


6: Reutilización de clases

205

Como se verá en el siguiente capítulo, es bastante más común reescribir métodos del mismo nombre utilizando exactamente el mismo nombre, parámetros y tipo de retorno que en la clase base. De otra manera pudiera ser confuso (que es la razón por la que C++no permite esto, para evitar que se haga lo que probablemente es un error).

Elección entre composición y herencia Tanto la composición como la herencia, permiten ubicar subobjetos dentro de una nueva clase. Habría que preguntarse por la diferencia entre ambas, y cuándo elegir una en vez de la otra.

La composición suele usarse cuando se quieren mantener las características de una clase ya existente dentro de la nueva, pero no su interfaz. Es decir, se empotra un objeto de forma que se puede usar para implementar su funcionalidad en la nueva clase, pero el usuario de la nueva la clase ve la interfaz que se ha definido para la nueva clase en vez de la interfaz del objeto empotrado. Para lograr este efecto, se empotran objetos privados de clases existentes dentro de la nueva clase. En ocasiones, tiene sentido permitir al usuario de la clase acceder directamente a la composición de la nueva clase; es decir, hacer a los objetos miembro públicos. Los objetos miembro usan por sí mismos la ocultación de información, de forma que esto es seguro. Cuando el usuario sabe que se

está ensamblando un conjunto de partes, construye una interfaz más fácil de entender. Un objeto coche es un buen ejemplo: / / : c06:Coche. java / / Composición con objetos públicos. class Motor { public void arrancar ( ) public void acelerar 0 public void parar ( ) { }

{ } { }

1 class Rueda { public void inflar (int psi)

class Ventana { public void subir ( ) public void bajar ( )

{ }

{ }

{ }

1 class Puerta { public Ventana ventana public void abrir ( ) { } public void cerrar ( ) {

=

}

new Ventana();


206

Piensa en Java

public class Coche { public Motor motor = new Motor(); public Rueda[] rueda = new Rueda[41; public Puerta izquierda = new Puerta(), derecha = new Puerta(); / / 2-puerta public Coche ( ) { for(int i = O; i < 4; i++) rueda [ i] = new Rueda ( ) ; public static void main (String[] args) Coche coche = new Coche ( ) ; coche.izquierda.ventana.subir(); coche. rueda [O].inflar (72);

{

Dado que la composición de un coche es parte del análisis del problema ('y no simplemente parte del diseño subyacente), hacer sus miembros públicos ayuda al entendimiento por parte del programador cliente de cómo usar la clase, y requiere menos complejidad de código para el creador de la clase. Sin embargo, hay que ser consciente de que éste es un caso especial y en general los campos se harán privados. Cuando se hereda, se toma una clase existente y se hace una versión especial de la misma. En general, esto significa que se está tomando una clase de propósito general y especializándola para una necesidad especial. Simplemente pensando un poco se verá que no tendría sentido componer un coche utilizando un objeto vehículo -un coche no contiene un vehículo, es un vehículo. La relación esun se expresa con herencia, y la relación tiene-un se expresa con composición.

Protegido (protected) Ahora que se ha presentado el concepto de herencia, tiene sentido finalmente la palabra clave protected. En un mundo ideal, los miembros privados siempre serían irrevocablemente privados, pero en los proyectos reales hay ocasiones en las que se desea hacer que algo quede oculto del mundo en general, y sin embargo, permitir acceso a miembros de clases derivadas. La palabra clave protected es un nodo de pragmatismo. Dice: "Esto es privado en lo que se refiere al usuario de la clase, pero está disponible para cualquiera que herede de esta clase o a cualquier otro de este paquete. Es decir, protegido es automáticamente "amistoso" en Java.

La mejor conducta a seguir es dejar los miembros de datos privados -uno

siempre debería preservar su derecho a cambiar la implementación subyacente. Posteriormente se puede permitir acceso controlado a los descendientes de la clase a través de los métodos protegidos: //:

c06:Malvado.java / / La palabra clave protected. import java.uti.1. *;


6: Reutilización de clases

class Villano { private int i; protected int leer ( ) { return i; } protected void poner(int ii) { i = ii; public Villano(int ii) { i = ii; } public int valor(int m) { return m*i; }

207

}

1 public class Malvado extends Villano { private int j; public Malvado(int jj) { super(jj); j public void cambiar(int x)

{

poner(x):

jj.r 1

=

1

1 ///:-

Se puede ver que cambiar( ) tiene acceso a poner( ) porque es protegido.

Desarrollo incremental Una de las ventajas de la herencia es que soporta el desarrollo incremental permitiendo introducir nuevo código sin introducir errores en el código ya existente. Esto también aísla nuevos fallos dentro del nuevo código. Pero al heredar de una clase funcional ya existente y al añadirle nuevos atributos y métodos (y redefiniendo métodos ya existentes), se deja el código existente -que alguien más podría estar utilizando- intacto y libre de errores. Si se da un fallo, se sabe que éste se encuentra en el nuevo código, que es mucho más corto y sencillo de leer que si hubiera que modificar el cuerpo del código existente. Es bastante sorprendente la independencia de las clases. Ni siquiera se necesita el código fuente de los métodos para reutilizar el código. Como máximo, simplemente habría que importar el paquete. (Esto es cierto, tanto en el caso de la herencia, como en el de la composición.) Es importante darse cuenta de que el desarrollo de un programa es un proceso incremental, al igual que el aprendizaje humano. Se puede hacer tanto análisis como se quiera, pero se siguen sin conocer todas las respuestas cuando comienza un proyecto. Se tendrá mucho más éxito y una realimentación mucho más inmediata- si empieza a "crecer" el proyecto como una criatura evolucionaria, orgánica, en vez de construirlo de un tirón como si fuera un rascacielos de cristal. Aunque la herencia puede ser una técnica útil de cara a la experimentación, en algún momento, una vez que las cosas se estabilizan es necesario echar un nuevo vistazo a la jerarquía de clases definida intentando encajarla en una estructura con sentido. Recuérdese que bajo todo ello, la herencia simplemente pretende expresar una relación que dice: "Esta nueva clase es un tipo de esa otra clase". Al programa no deberían importarle los bits, sino el crear y manipular objetos de varios tipos para expresar un modelo en términos que provengan del espacio del problema.


208

Piensa en

Java

Conversión hacia arriba El aspecto más importante de la herencia no es que proporcione métodos para la nueva clase. Es la relación expresada entre la nueva clase y la clase base. Esta relación puede resumirse diciendo: "La nueva clase es un tipo de la clase existente". Esta descripción no es simplemente una forma elegante de explicar la herencia -está soportada directamente por el lenguaje. Como ejemplo, considérese una clase base denominada Instrumento que representa los instrumentos musicales, y una clase derivada denominada Viento. Dado que la herencia significa que todos los métodos de la clase base también están disponibles para la clase derivada, cualquier mensaje que se pueda enviar a la clase base podrá ser también enviado a la clase derivada. Si la clase Instrumento tiene un método tocar( ), también lo tendrán los instrumentos

Viento. Esto significa que se puede decir con precisión que un objeto Viento es también un tipo de Instrumento. El ejemplo siguiente muestra cómo soporta este concepto el compilador: / / : c06:Viento.java / / Herencia y conversión hacia arriba. import java-util.*;

class Instrumento { public void tocar ( ) { } static void afinar (Instrumento i) // ... i.tocar ( ) ;

{

1

/ / Los objetos de viento son instrumentos / / porque tienen la misma interfaz: class Viento extends Instrumento { public static void main (String[] args) { Viento flauta = new Viento(); Instrumento.afinar(f1auta); / / Conversión hacia arriba 1 1 ///:-

Lo interesante de este ejemplo es el método afinar( ), que acepta una referencia a Instrumento. Sin embargo, en Viento.main( ), se llama al método afinar( ) proporcionándole una referencia a Viento. Dado que Java tiene sus particularidades en la comprobación de tipos, parece extraño que un método que acepte un tipo llegue a aceptar otro tipo, hasta que uno se da cuenta de que un objeto Viento es también un objeto Instrumento, y no hay método al que pueda invocar afinar( ) para un Instrumento que no esté en Viento. Dentro de afinar( ), el código funciona para Instrumento y cualquier cosa que se derive de Instrumento, y al acto de convertir una referencia a Viento en una referencia a Instrumento se le denomina hacer conversión hacia arriba.


6: Reutilización de clases

209

¿Por que "conversión hacia arriba"? La razón para el término es histórica, y se basa en la manera en que se han venido dibujando tradicionalmente los diagramas de herencia: con la raíz en la parte superior de la página, y creciendo hacia abajo. (Por supuesto, se puede dibujar un diagrama de cualquier manera que uno considere útil.) El diagrama de herencia para Viento.java es, por consiguiente:

l-7

Instrumento

Viento

La conversión de clase derivada a base se mueve hacia arriba dentro del diagrama de herencia, por lo que se denomina conversión hacia arriba. Esta operación siempre es segura porque se va de un tipo más específico a uno más general. Es decir, la clase derivada es un superconjunto de la clase base. Podría contener más métodos que la clase base, pero debe contener al menos los métodos de ésta última. Lo único que puede pasar a la interfaz de clases durante la conversión hacia arriba es que pierda métodos en vez de ganarlos. Ésta es la razón por la que el compilador permite la conversión hacia arriba sin ningún tipo de conversión especial u otras notaciones especiales. También se puede llevar a cabo lo contrario a la conversión hacia arriba, denominado conversión hacia abajo, pero implica el dilema en el que se centra el Capítulo 12.

De nuevo composición frente a herencia En la programación orientada a objetos, la forma más probable de crear código es simplemente empaquetando juntos datos y métodos en una clase, y usando los objetos de esa clase. También se utilizarán clases existentes para construir nuevas clases con composición. Menos frecuentemente, se usará la herencia. Por tanto, aunque la herencia captura gran parte del énfasis durante el aprendizaje de POO, esto no implica que se deba hacer en todas partes en las que se pueda. Por el contrario, se debe usar de una manera limitada, sólo cuando está claro que es útil. Una de las formas más claras de determinar si se debería usar composición o herencia es preguntar si alguna vez habrá que hacer una conversión hacia arriba desde la nueva clase a la clase base. Si se debe hacer una conversión hacia arriba, entonces la herencia es necesaria, pero si no se necesita, se debería mirar más con detalle si es o no necesaria. El siguiente capítulo (polimorfismo) proporciona una de las razones de más peso para una conversión hacia arriba, pero si uno recuerda preguntar: ''¿Necesito una conversión hacia arriba?" obtendrá una buena herramienta para decidir entre la composición y la herencia.


210

Piensa en Java

palabra clave final La palabra clave final de Java tiene significados ligeramente diferentes dependiendo del contexto, pero en general dice: "Esto no puede cambiarse". Se podría querer evitar cambios por dos razones: diseño o eficiencia. Dado que estas dos razones son bastante diferentes, es posible utilizar erróneamente la palabra clave final. Las secciones siguientes discuten las tres posibles ubicaciones en las que se puede usar final: para datos, métodos y clases.

Para datos Muchos lenguajes de programación tienen una forma de indicar al compilador que cierta parte de código es "constante". Una constante es útil por varias razones:

1.

Puede ser una constante en tiempo de compilación que nunca cambiará.

2.

Puede ser un valor inicializado en tiempo de ejecución que no se desea que se llegue a cambiar.

En el caso de una constante de tiempo de compilación, el compilador puede "manejar" el valor constante en cualquier cálculo en el que se use; es decir, se puede llevar a cabo el cálculo en tiempo de compilación, eliminando parte de la sobrecarga de tiempo de ejecución. En Java, estos tipos de constantes tienen que ser datos primitivos y se expresan usando la palabra clave final. A este tipo de constantes se les debe dar un valor en tiempo de definición. Un campo que es estático y final sólo tiene un espacio de almacenamiento que no se puede modificar.

Al usar final con referencias a objetos en vez de con datos primitivos, su significado se vuelve algo confuso. Con un dato primitivo, final convierte el valor en constante, pero con una referencia a un objeto, final hace de la referencia una constante. Una vez que la referencia se inicializa a un objeto, ésta nunca se puede cambiar para que apunte a otro objeto. Sin embargo, se puede modificar el objeto en sí; Java no proporciona ninguna manera de convertir un objeto arbitrario en una constante. (Sin embargo, se puede escribir la clase, de forma que sus objetos tengan el efecto de ser constantes.) Esta restricción incluye a los arrays, que también son objetos. He aquí un ejemplo que muestra el funcionamiento de los campos final: / / : c06:DatosConstantes.java / / El efecto de final en campos.

class Valor { int i = 1;

1


6: Reutilización de clases

211

/ / Pueden ser constantes de tiempo de compilación final int il = 9; static final int VAL-DOS = 99; / / Típica constante pública: public static final int VAL-TRES = 39; / / No pueden ser constantes en tiempo de compilación: final int i4 = (int)(Math.random()*20) ; static final int i5 = (int)(Math.random ( ) *20) ; Valor vl = new Valor ( ) ; final Valor v2 = new Valor(); static final Valor v3 = new Valor ( ) ;

/ / Arrays: final int[] a

=

{

1, 2, 3, 4, 5, 6

};

public void escribir(String id) { System.out.println( ' + i4 + id + 11: 11 + "i4 = 1 i5 = 11 + i5);

1 public static void main(String[] args) { DatosConstantes fdl = new DatosConstantes(); / / ! fdl.il++; / / Error: no se puede cambiar el valor fdl.v2.i++; / / ¡El objeto no es constante! fdl .vl = new Valor ( ) ; / / OK -- no es final for(int i = O; i < fdl.a.length; i++) fdl.a[il++; / / ;El objeto no es una constante! ! fdl .v2 = new Valor 0 ; / / Error: No se puede ! fdl.v3 = new Valor ( ) ; / / cambiar ahora la referencia ! fd1.a = new int[3]; fdl .escribir("fdl") ; System.out.println("Creando un nuevo DatosConstantes"); DatosConstantes fd2 = new DatosConstantes(); fdl .escribir("fdl"); fd2 .escribir("fd2"); }

1 ///:-

Dado que i l y VALDOS son datos primitivos final con valores de tiempo de compilación, ambos pueden usarse como constantes de tiempo de compilación y su uso no difiere mucho. VAL-TRES es la manera más usual en que se verán definidas estas constantes: pública de forma que puedan ser utilizadas fuera del paquete, estática para hacer énfasis en que sólo hay una, ydinal para indicar que es una constante. Fíjese que los datos primitivo static final con valores iniciales constantes (es decir, las constantes de tiempo de compilación) se escriben con mayúsculas por acuerdo,


212

Piensa en Java

además de con palabras separadas por guiones bajos (es decir, justo como las constantes de C, que es de donde viene el acuerdo). La diferencia se muestra en la salida de una ejecución: fdl: i4 = Creando un fdl: i4 = fd2: i4 =

15; i5 = 9 nuevo DatosConstante 15; i5 = 9 10: i5 = 9

Fíjese que los valores de i4 para fdl y fd2 son únicos, pero el valor de i5 no ha cambiado al crear el segundo objeto DatosConstante. Esto es porque es estático y se inicializa una vez en el momento de la carga y no cada vez que se crea un nuevo objeto. Las variables de v l a v4 demuestran el significado de una referencia final. Como se puede ver en main( ), justo porque v2 sea final, no significa que no se pueda cambiar su valor. Sin embargo, no s e puede reubicar v 2 a un nuevo objeto, precisamente porque e s final. Eso e s lo que final significa

para una referencia. También se puede ver que es cierto el mismo significado para un array, que no es más que otro tipo de referencia. (No hay forma de convertir en final las referencias a array en sí.) Hacer las referencias final parece menos útil que hacer final a las primitivas.

Constantes blancas Java permite la creación de constantes blancas, que son campos declarados como final pero a los que no se da un valor de inicialización. En cualquier caso, se debe inicializar una constante blanca antes de utilizarla, y esto lo asegura el propio compilador. Sin embargo, las constantes blancas proporcionan mucha mayor flexibilidad en el uso de la palabra clave final puesto que, por ejemplo, un campo final incluido en una clase puede ahora ser diferente para cada objeto, y sin embargo, sigue reteniendo su cualidad de inmutable. He aquí un ejemplo: / / : c06:CostanteBlanca.java / / Miembros de datos "Constantes blancas". class Elemento

{

}

class ConstanteBlanca { final int i = 0; / / Constante inicializada final int j; / / Constante blanca final Elemento p; / / Referencia a constante blanca / / Las constantes blancas DEBEN inicializarse / / en el constructor: ConstanteBlanca ( ) ( j = 1; / / Inicializar la la constante blanca p = new Elemento O ; 1 ConstanteBlanca (int x) ( j = x; / / Inicializar la constante blanca p = new Elemento();


6: Reutilización de clases

public static void main(String[] args) { ConstanteBlanca bf = new ConstanteBlanca ( )

213

;

1 1 ///:Es obligatorio hacer asignaciones a constantes, bien con una expresión en el momento de definir el campo o en el constructor. De esta forma, se garantiza que el campo constante se inicialice siempre antes de ser usado.

Parámetros de valor constante Java permite hacer parámetros constantes declarándolos con la palabra final en la lista de parámetros. Esto significa que dentro del método no se puede cambiar aquello a lo que apunta la referencia al parámetro: / / : c06:ParametrosConstante.java / / Utilizando "final" con parámetros de métodos. class ~rtilugio { public void girar ( ) 1

{ )

public class Parametrosconstante { void con(fina1 Artilugio g) { / / ! g = new Artilugio(); / / Ilegal -- g es constante void sin(Arti1ugio g) { g = new Artilugio(); //' OK -- g no es constante g.girar ( ) ;

/ / void f (final int i) { itt; } / / No puede cambiar / / Sólo se puede leer de un tipo de dato primitivo: int g(fina1 int i) { return i + 1; } public static void main(String[] args) { Parametrosconstante bf = new ParametrosConstante(); bf . sin (null); bf .con (null); 1 1 ///:-

Fíjese que se puede seguir asignando una referencia null a un parámetro constante sin que el compilador se dé cuenta, al igual que se puede hacer con un parámetro no constante. Los métodos f( ) y g( ) muestran lo que ocurre cuando los parámetros primitivos son constante: se puede leer el parámetro pero no se puede cambiar.


214

Piensa en Java

Métodos constante Hay dos razones que justifican los métodos constante. La primera es poner un "bloqueo" en el método para evitar que cualquier clase heredada varíe su significado. Esto se hace por razones de diseño cuando uno se quiere asegurar de que se mantenga el comportamiento del método durante la herencia, evitando que sea sobreescrito.

La segunda razón para los métodos constante es la eficiencia. Si se puede hacer un método constante se está permitiendo al compilador convertir cualquier llamada a ese método en llamadas rápidas. Cuando el compilador ve una llamada a un método constante puede (a su discreción) saltar el modo habitual de insertar código para llevar a cabo el mecanismo de invocación al método (meter los argumentos en la pila, saltar al código del método y ejecutarlo, volver al punto del salto y e l i i n a r los parámetros de la pila, y manipular el valor de retorno) o, en vez de ello, reemplazar la llamada al método con una copia del código que, de hecho, se encuentra en el cuerpo del método. Esto elimina la sobrecarga de la llamada al método. Por supuesto, si el método es grande, el código comienza a aumentar de tamaño, y probablemente no se aprecien ganancias de rendimiento en la sustitución, puesto que cualquier mejora se verá disminuida por la cantidad de tiempo invertido dentro del método. Está implícito el que el compilador de Java sea capaz de detectar estas situaciones, y elegir sabiamente. Si embargo, es mejor no confiar en que el compilador sea capaz de hacer esto siempre bien, y hacer un método constante sólo si es lo suficientemente pequeño o se desea evitar su modiicación explícitamente.

constante y privado Cualquier método privado de una clase es implícitamente constante. Dado que no se puede acceder a un método privado, no se puede modificar (incluso aunque el compilador no dé un mensaje de error si se intenta modificar, no se habrá modificado el método, sino que se habrá creado uno nuevo). Se puede añadir el modificador final a un método privado pero esto no da al método ningún significado extra. Este aspecto puede causar confusión, porque si se desea modificar un método privado (que es implícitamente constante) parece funcionar: / / : c06:AparienciaModificacionConstante.java

/ / Sólo parece que se puede modificar / / un método privado o privado constante. class ConConstantes { / / Idéntico a únicamente "privado": private final void f ( ) { Systern.out.println("ConConstantes.fO"); / / También automáticamente "constante": private void g() { System.out .println ("ConConstantes.g ( ) " ) 1

;


6: Reutilización de clases

215

class ModificacionPrivado extends Conconstante { private final void f ( ) { System.out.println("ModificacionPrivado.f()");

1 private void g() { System.out .println ("ModificacionPrivado.g ( )

") ;

1

class ModificacionPrivado2 extends ModificacionPrivado { public final void f ( ) { System.out.println("ModificacionPrivado2.f()");

1 public void g() { System.out.println("ModificacionPrivado2.g()");

1 1 public class AparienciaModificacionConstante { public static void main (String[] args) { ModificacionPrivado2 op2 = new ModificacionPrivado2(); op2.f 0 ; op2.90; / / Se puede hacer conversión hacia arriba: ModificacionPrivado op = op2; / / Pero no se puede invocar a los métodos: / / ! 0p.fO; / / ! 0p.gO; / / Lo mismo que aquí: ConCostantes wf = op2; / / ! wf.f(); / / ! wf.90;

La "modificación" sólo puede darse si algo es parte de la interfaz de la clase base. Es decir, uno debe ser capaz de hacer conversión hacia arriba de un objeto a su tipo base e invocar al mismo método (la esencia de esto se verá más clara en el siguiente capítulo). Si un método es privado, no es parte de la interfaz de la clasc base. Es simplemente algún código oculto dentro de la clase, y simplemente tiene ese nombre, pero si se crea un método público, protegido o "amistoso" en la clase derivada, no hay ninguna conexión con el método que pudiese llegar a tener ese nombre en la clase base. Dado que un método privado es inalcanzable y a efectos invisible, no influye en nada más que en la organización del código de la clase para la que se definió.


216

Piensa en Java

Clases constantes Cuando se dice que una clase entera es constante (precediendo su definición de la palabra clave final) se establece que no se desea heredar de esta clase o permitir a nadie más que lo haga. En otras palabras, por alguna razón el diseño de la clase es tal que nunca hay una necesidad de hacer cambios, o por razones de seguridad no se desea la generación de subclases. De manera alternativa, se pueden estar tratando aspectos de eficiencia, y hay que asegurarse de que cualquier actividad involucrada con objetos de esta clase sea lo más eficiente posible. / / : c06:Jurasico.java / / Convirtiendo una clase entera en final.

class CerebroPequenio

{ }

final class Dinosaurio { int i = 7; int j = 1; CerebroPequenio x = new CerebroPequenio ( ) void f ( ) { }

;

/ / ! class SerEvolucionado extends Dinosaurio

{ }

/ / error: No pueda heredar de la clase constante 'Dinosaurio' public class Jurasico { public static void main (String[] args) Dinosaurio n = new Dinosaurio ( ) ; n.f 0 ; n.i = 40; n.j++;

{

Fíjese que los atributos pueden ser constantes o no, como se desee. Las mismas reglas se aplican a los atributos independientemente de si la clase se ha definido como constante. Definiendo la clase como constante simplemente evita la herencia -nada más. Sin embargo, dado que evita la herencia, todos los métodos de una clase constante son implícitamente constante, puesto que no hay manera de modificarlos. Por tanto, el compilador tiene las mismas opciones de eficiencia como tiene si se declara un método explícitamente constante. Se puede añadir el especificador constante a un método en una clase constante, pero esto no añade ningún significado.


6: Reutilización de clases

217

Precaución con constantes Puede parecer sensato hacer un método constante mientras se está diseñando una clase. Uno podría sentir que la eficiencia es muy importante al usar la clase y que nadie podría posiblemente desear modificar estos métodos de ninguna manera. En ocasiones esto es cierto. Pero hay que ser cuidadoso con las suposiciones. En general, es difícil anticipar cómo se reutilizará una clase, especialmente en el caso de clases de propósito general. Si se define un método como constante se podría evitar la posibilidad de reutilizar la clase a través de la herencia en otros proyectos de otros programadores simplemente porque su uso fuera inimaginable.

La biblioteca estándar de Java es un buen ejemplo de esto. En particular, la clase Vector de Java 1.0/1.1 se usaba comúnmente y podría haber sido incluso más útil si, en aras de la eficiencia, no se hubieran hecho constante todos sus métodos. Es fácil de concebir que se podría desear heredar y superponer partiendo de una clase tan fundamentalmente útil, pero de alguna manera, los diseñadores decidieron que esto no era adecuado. Esto es irónico por dos razones. La primera, que la clase Stack hereda de Vector, lo que significa que un Stack es un Vector, lo que no es verdaderamente cierto desde el punto de vista lógico. Segundo, muchos de los métodos más importantes de Vector, como addElement( ) y elementAt( ) están sincronizados (synchronized). Como se verá en el Capítulo 14, esto incurre en una sobrecarga considerable que probablemente elimine cualquier ganancia proporcionada por final. Esto da credibilidad a la teoría de que los programadores suelen ser normalmente malos a la hora de adivinar dónde deberían intentarse las optimizaciones. Es muy perjudicial que haya un diseño tan poco refinado en una biblioteca con la que todos debemos trabajar. (Afortunadamente, la biblioteca de Java 2 reemplaza Vector por ArrayList, que se comporta mucho más correctamente. Desgraciadamente, se sigue escribiendo mucho código nuevo que usa la biblioteca antigua.) También es interesante tener en cuenta que Hashtable, otra clase de biblioteca estándar importante, no tiene ningún método constante. Como se mencionó en alguna otra parte de este libro, es bastante obvio que algunas clases se diseñaron por unas personas y otras por personas completamente distintas. (Se verá que los nombres de método de Hashtable son mucho más breves que los de Vector, lo cual es otra prueba de esta afirmación.) Este es precisamente el tipo de aspecto que no debería ser obvio a los usuarios de una biblioteca de clases. Cuando los elementos son inconsistentes, simplemente el usuario final tendrá que trabajar más. Otra alabanza más al valor del diseño y de los ensayos de código. (Fíjese que la biblioteca de Java 2 reemplaza Hashtable por HashMap.)

Carga

clases

inicialización

En lenguajes más tradicionales, los programas se cargan de una vez como parte del proceso de arranque. Éste va seguido de la inicialización y posteriormente comienza el programa. El proceso de inicialización en estos lenguajes debe controlarse cuidadosamente de forma que el orden de inicialización de los datos estáticos no cause problemas. C++, por ejemplo, tiene problemas si uno de los datos estáticos espera que otro dato estático sea válido antes de haber inicializado el segundo.


218

Piensa en Java

Java no tiene este problema porque sigue un enfoque diferente en la carga. Dado que todo en Java es un objeto, muchas actividades se simplifican, y ésta es una de ellas. Como se aprenderá más en profundidad en el siguiente capítulo, el código compilado de cada clase existe en su propio archivo separado. El archivo no se carga hasta que se necesita el código. En general, se puede decir que "El código de las clases se carga en el momento de su primer uso". Esto no ocurre generalmente hasta que se construye el primer objeto de esa clase, pero también se da una carga cuando se accede a un dato o método estático. El momento del primer uso es también donde se da la inicialización estática. Todos los objetos estáticos y el bloque de código estático se inicializarán en orden textual (es decir, el orden en que se han escrito en la definición de la clase) en el momento de la carga. Los datos estáticos, por su-

puesto, se inicializan únicamente una vez.

Inicialización con herencia Ayuda a echar un vistazo a todo el proceso de inicialización, incluyendo la herencia, para conseguir una idea global de lo que ocurre. Considérese el siguiente código: / / : cO6 :Escarabajo.java / / El proceso de inicialización completo. class Insecto { int i = 9; int j; Insecto() { visualizar("i = " + i + ", j = " + j); j = 39; 1 static int xl = visualizar("static 1nsecto.xl inicializado"); static int visualizar(String S) { System.out .println (S); return 47;

public class Escarabajo extends Insecto { int k = visualizar ("Escarabajo.k inicializado") ; Escarabajo ( ) { visualizar("k = " + k); visualizar("j = " + j); 1 static int x2 = visualizar("static escarabajo.xZ inicializado"); public static void main (String[] args) {


6: Reutilización de clases

visualizar ("Constructor de Escarabajos Escarabajo b = new Escarabajo ( ) ;

219

") ;

La salida de este programa es: static 1nsecto.xl inicializado static Escarabajo.x2 inicializado Constructor de Escarabajos i = 9, j Escarabaj0.k inicializado

k

=

47

j

=

39

=

O

Lo primero que ocurre al ejecutar E s c a r a b a j o bajo Java e s que s e intenta acceder a Escarabajo.main( ) (un método estático), de forma que el cargador sale a buscar el código compilado de la clase Escarabajo (que resulta estar en un fichero denominado Escarabajo.class). En el proceso de su carga, el cargador se da cuenta de que tiene una clase base (que es lo que indica la palabra clave extends), y por consiguiente, la carga. Esto ocurrirá tanto si se hace como si no un objeto de esa clase. (Intente comentar la creación del objeto si se desea demostrar esto.) Si la clase base tiene una clase base, las segunda clase base se cargaría también, y así sucesivamente. Posteriormente, se lleva a cabo la inicialización estática de la clase base raíz (en este caso Insecto), y posteriormente la siguiente clase derivada, y así sucesivamente. Esto es importante porque la inicialización estática de la clase derivada podría depender de que se inicialice adecuadamente el miembro de la clase base. En este momento, las clases necesarias ya han sido cargadas de forma que se puede crear el objeto. Primero, se ponen a sus valores por defecto todos los datos primitivos de este objeto, y las referencias a objetos se ponen a null -esto ocurre en un solo paso poniendo la memoria del objeto a ceros binarios. Después se invoca al constructor de la clase base. En este caso, la llamada es automática, pero también se puede especificar la llamada al constructor de la clase base (como la primera operación en el constructor de Escarabajo( )) utilizando super. La construcción de la clase base sigue el mismo proceso en el mismo orden, como el constructor de la clase derivada. Una vez que acaba el constructor de la clase base se inicializan las variables de instancia en orden textual. Finalmente se ejecuta el resto del cuerpo del constructor.

Resumen Tanto la herencia como la composición, permiten crear un nuevo tipo a partir de tipos ya existentes. Generalmente, sin embargo, se usa la composición para reutilizar tipos ya existentes como parte de la implementación subyacente del nuevo tipo, y la herencia cuando se desee reutilizar la interfaz. Dado que la clase derivada tiene la interfaz de la clase base, se le puede hacer una conversión hacia arriba hasta la clase base, lo que es crítico para el polimorfismo, como se verá en el siguiente capítulo.


220

Piensa en Java

A pesar del gran énfasis que la programación orientada a objetos pone en la herencia, al empezar un diseño debería generalmente preferirse la composición durante el primer corte, y la herencia sólo cuando sea claramente necesaria. La composición tiende a ser más flexible. Además, al utilizar la propiedad añadida de la herencia con un tipo miembro, se puede cambiar el tipo exacto y, por tanto, el comportamiento de aquellos objetos miembro en tiempo de ejecución. Por consiguiente, se puede cambiar el comportamiento del objeto compuesto en tiempo de ejecución. Aunque la reutilización de código mediante la composición y la herencia es útil para el desarrollo rápido de proyectos, generalmente se deseará rediseñar la jerarquía de clases antes de permitir a otros programadores llegar a ser dependientes de ésta. La meta es una jerarquía en la que cada clase tenga

un uso específico y no sea demasiado grande (agrupando tanta funcionalidad sena demasiado difícil de manejar) ni demasiado pequeño (no se podría usar por sí mismo o sin añadirle funcionalidad).

Ejercicios Las soluciones a determinados ejercicios se encuentran en el documento The Thinking in Java Annotated Solution Guide, disponible a bajo coste en http://www.BruceEckel.com.

Crear dos clases, A y B, con constructores por defecto (listas de parámetros vacías) que se anuncien a sí mismas. Heredar una nueva clase C a partir de A, y crear un miembro de la clase B dentro de C. No crear un constructor para C. Crear un objeto de la clase C y observar los resultados. Modificar el Ejercicio 1 de forma que A y B tengan constructores con parámetros en vez de constructores por defecto. Escribir un constructor para C y llevar a cabo toda la inicialización dentro del constructor C. Crear una clase simple. Dentro de una segunda clase, definir un campo para un objeto de la primera clase. Utilizar inicialización perezosa para instanciar este objeto. Heredar una nueva clase de la clase Detergente. Superponer frotar( ) y añadir un nuevo método denominado esterilizar( ). Tomar el archivo Animacion.java y comentar el constructor de la clase Animación. Explicar qué ocurre. Tomar el archivo Ajedrez.java y comentar el constructor de la clase Ajedrez. Explicar qué ocurre. Probar que se crean constructores por defecto por parte del compilador. Probar que los constructores de una clase base (a) siempre son invocados y, (b) se invocan antes que los constructores de la clase derivada. Crear una clase base con sólo un constructor distinto del constructor por defecto, y una clase derivada que tenga, tanto un constructor por defecto, como uno que no lo sea. En los constructores de la clase derivada, invocar al de la clase base.


6: Reutilización de clases

221

Crear una clase llamada Raíz que contenga una instancia de cada clase (que también se deben crear) denominadas Componentel, Componente2, y Componente3. Derivar una clase Tallo a partir de Raíz que también contenga una instancia de cada "componente". Todas las clases deberían tener constructores por defecto que impriman un mensaje relativo a ellas. Modificar el Ejercicio 10 de forma que cada clase sólo tenga un constructor que no sea por defecto. Añadir una jerarquía correcta de métodos limpiar( ) a todas las clases del Ejercicio 11. Crear una clase con un método sobrecargado tres veces. Heredar una nueva clase, añadir una nueva sobrecarga del método y mostrar que los cuatro métodos están disponibles para la clase derivada. En Coche.java añadir un método revisar( ) a Motor e invocar a este método en el método main( ). Crear una clase dentro de un paquete. La clase debería contener un método protegido. Fuera del paquete, intentar invocar al método protegido y explicar los resultados. Ahora heredar de la clase e invocar al método protegido desde dentro del método de la clase derivada. Crear una clase llamada Anfibio. Desde ésta, heredar una clase llamada Rana. Poner métodos apropiados en la clase base. En el método main( ), crear una Rana y hacer una conversión hacia Anfibio. Demostrar que todos los métodos siguen funcionando. Modificar el Ejercicio 16 de forma que Rana superponga las definiciones de métodos de la clase base (proporciona nuevas definiciones usando los mismos nombres de método). Fijarse en lo que ocurre en el método main( ). Crear una clase con un campo estático constante y un campo constante, y demostrar la diferencia entre los dos. Crear una clase con una referencia constante blanca a un objeto. Llevar a cabo la inicialización de la constante blanca final dentro del método (no en el constructor) justo antes de usarlo. Demostrar que debe inicializarse la constante antes de usarlos, y que no puede cambiarse una vez inicializada. Crear una clase con un método constante. Heredar desde esa clase e intentar superponer ese método. Crear una clase constante e intentar heredar de ella. Probar que la carga de clases sólo se da una vez. Probar que la carga puede ser causada, bien por la creación de la primera instancia de esa clase, o por el acceso a un miembro estático. En EscarabajoJava, heredar un tipo específico de escarabajo de la clase Escarabajo, siguiendo el mismo formato que el de la clase existente. Hacer un seguimiento y explicar la salida.


7: Polimorfismo El polimorfismo es la tercera característica esencial de los lenguajes de programación orientados a objetos, después de la abstracción de datos y la herencia. Proporciona otra dimensión de separación de la interfaz de la implementación, separa el qué del cómo. El polimorfismo permite una organización de código y una legibilidad del mismo mejorada, además de la creación de programas ampliables que pueden "crecer", no sólo durante la creación original del proyecto sino también cuando se deseen añadir nuevas características. La encapsulación crea nuevos tipos de datos mediante la combinación de características y comportamientos. La ocultación de la implementación separa la interfaz de la implementación haciendo los detalles privados. Este tipo de organización mecánica tiene bastante sentido para alguien con un trasfondo procedural de programación. Pero el polimorfismo tiene que ver con la separación en términos de tipos. En el capítulo anterior se vio como la herencia permite el tratamiento de un objeto como si fuera de sus propio tipo o del tipo base. Esta característica es crítica porque permite que varios tipos (derivados de un mismo tipo base) sean tratados como si fueran uno sólo, y un único fragmento de código se puede ejecutar de igual forma en todos los tipos diferentes. La llamada a un método polimórfico permite que un tipo exprese su distinción de otro tipo similar, puesto que ambos se derivan del mismo tipo base. Esta distinción se expresa a través de diferencias en comportamiento de los métodos a los que se puede invocar a través de la clase base.

En este capítulo, se aprenderá lo relacionado con el polimorfismo (llamado también reubicación dinámica, reubicación tardía o reubicación en tiempo de ejecución) partiendo de la base, con ejemplos simples que prescinden de todo, menos del comportamiento polimórfico del programa.

De nuevo la conversión hacia arriba En el Capítulo 6 se vio cómo un objeto puede usarse con su propio tipo o como un objeto de su tipo base. Tomar una referencia a un objeto y tratarla como una referencia a su clase base se denomina conversión hacia arriba, debido a la forma en que se dibujan los árboles de herencia, en los que la clase base se coloca siempre en la parte superior. También se vio que surge un problema, como se aprecia en: / / : c07:musica:Musica.java / / Herencia y conversión hacia arriba. class Nota { private int valor; private Nota(int val) { valor = val; public static final Nota DO-MAYOR = new Nota(O), DO-SOSTENIDO = new Nota (1),

}


224

Piensa en Java

SI BEMOL

=

new Nota(2);

1 // E~C. class Instrumento { public void tocar(Nota n) { System.out .println ("Instrumento.tocar ( ) ");

1 1 / / Los objetos de viento son instrumentos / / dado que tienen la misma interfaz: class Viento extends Instrumento { / / Redefinir el metodo interfaz: public void tocar(Nota n) { System.out .println ("Viento.tocar ( ) ");

1 1 public class Musica { public static void afinar(1nstrumento i) // ... i.tocar(Nota.DO-SOSTENIDO);

{

1 public static void main(String[] args) { Viento flauta = new Viento ( ) ; afinar (flauta); / / Conversión hacia arriba

1 1 ///:-

El método Musica.afinar( ) acepta una referencia a Instrumento, pero también cualquier cosa que se derive de Instrumento. En el método main( ), se puede ver que ocurre esto pues se pasa una referencia Viento a afinar( ), sin que sea necesaria ninguna conversión. Esto es aceptable; la interfaz de Instrumento debe existir en Viento, puesto que Viento se hereda de Instrumento. Hacer una conversión hacia arriba de Viento a Instrumento puede "estrechar" esa interfaz, pero no puede reducirlo a nada menos de lo contenido en la interfaz de Instrumento.

Olvidando el tipo de objeto Este programa podría parecer extraño. 2Por qué debería alguien olvidar intencionadamente el tipo de objeto? Esto es lo que ocurre cuando se hace conversión hacia arriba, y parece que podría ser mucho más directo si afinar( ) simplemente tomara una referencia Viento como argumento. Esto presenta un punto esencial: si se hiciera esto se necesitaría escribir un nuevo método afinar( ) para cada tipo de I n s t r u m e n t o del sistema. Supóngase que se sigue este razonamiento y se añaden los instrumentos de Cuerda y Metal:


7 : Polimorfismo

/ / : c07:musica2:Musica2.java / / Sobrecarga en vez de conversi贸n hacia arriba.

class Nota { private int valor; private Nota(int val) { valor public static final Nota DO-MAYOR = new Nota (O), DO-SOSTENIDO = new Nota(l), SI-BEMOL = new Nota(2); 1 / / Etc.

=

val;

}

class Instrumento { public void tocar(Nota n) { System.out.println("Instrumento.tocar()"); }

1 class Viento extends Instrumento { public void tocar(Nota n) { System.out .println ("Viento.tocar ( )

") ;

class Cuerda extends Instrumento { public void tocar(Nota n) { System.out .println ("Cuerda.tocar ( )

") ;

1

class Metal extends Instrumento { public void tocar (Nota n) { System.out .println ("Metal.tocar ( )

") ;

1 public class Musica2 { public static void afinar(Vient0 i) i.tocar (Nota.DO-MAYOR) ;

{

}

public static void afinar(Cuerda i) i .tocar (Nota.DO-MAYOR) ;

{

1

public static void afinar(Meta1 i) i.tocar (Nota.DO-MAYOR) ;

{

225


226

Piensa en Java

public static void main (String[] args) { Viento flauta = new Viento(); Cuerda violin = new Cuerda(); Metal trompeta = new Metal ( ) ; afinar(f1auta); / / Sin conversión hacia arriba afinar (violin); afinar (trompeta);

1 1 ///:-

Esto funciona, pero hay un inconveniente: se deben escribir métodos específicos de cada tipo para cada clase Instrumento que se añada. En primer lugar esto significa más programación, pero también quiere decir que si se desea añadir un método nuevo como afinar( ) o un nuevo tipo de Instrumento, se tiene mucho trabajo por delante. Añadiendo el hecho de que el compilador no emitirá ningún mensaje de error si se olvida sobrecargar alguno de los métodos, el hecho de trabajar con tipos podría convertirse en inmanejable. ¿No sería muchísimo mejor si simplemente se pudiera escribir un único método que tomara como parámetro la clase base, y no cualquiera de las clases específicas derivadas? Es decir, ¿no sería genial que uno se pudiera olvidar de que hay clases derivadas, y escribir un código que sólo tratara con la clase base? Esto es exactamente lo que permite hacer el polimorfismo. Sin embargo, la mayoría de programadores que provienen de lenguajes procedurales, tienen problemas para entender el funcionamiento de esta caracterítica.

El cambio La dificultad con Musica.java se puede ver ejecutando el programa. La salida es Viento.tocar( ) Ésta es, ciertamente, la salida deseada, pero no parece tener sentido que funcione de esa forma. Obsérvese el método afinar( ): public static void afinar (Instrumento i)

{

// ... i .tocar (Nota.DO-MAYOR) ; 1

Recibe una referencia a Instrumento. Por tanto, ¿cómo puede el compilador saber que esta referencia a Instrumento apunta a Viento en este caso, y no a Cuerda o Metal? El compilador de hecho no puede saberlo. Para lograr un entendimiento más profundo de este aspecto, es útil echar un vistazo al tema de la ligadura.


7: Polimorfismo

227

La ligadura en las llamadas a métodos La conexión de una llamada a un método se denomina ligadura. Cuando se lleva a cabo la ligadura antes de ejecutar el programa (por parte del compilador y el montador, cuando lo hay) se denomina ligadura temprana. Puede que este término parezca extraño pues nunca ha sido una opción con los lenguajes procedurales. Los compiladores de C tienen un único modo de invocar a un método utilizando la ligadura temprana. La parte confusa del programa de arriba no se resuelve fácilmente con la ligadura temprana pues el compilador no puede saber el método correcto a invocar cuando sólo tiene una referencia a un

Instrumento. La solución es la ligadura tardía, que implica que la correspondencia se da en tiempo de ejecución, basándose en el tipo de objeto. La ligadura tardia se denomina también dinámica o en tiempo de ejecución. Cuando un lenguaje implementa la ligadura tardía, debe haber algún mecanismo para determinar el tipo de objeto en tiempo de ejecución e invocar al método adecuado. Es decir, el compilador sigue sin saber el tipo de objeto, pero el mecanismo de llamada a métodos averigua e invoca al cuerpo de método correcto. El mecanismo de la ligadura tardía varía de un lenguaje a otro, pero se puede imaginar que es necesario instalar algún tipo de información en los objetos. Toda ligadura de métodos en Java se basa en la ligadura tardía a menos que se haya declarado un método como constante. Esto significa que ordinariamente no es necesario tomar decisiones sobre si se dará la ligadura tardía, sino que esta decisión se tomará automáticamente. ¿Por qué declarar un método como constante? Como se comentó en el capítulo anterior, evita que nadie superponga el método. Todavía más importante, "desactiva" ligadura dinámica, o mejor, es que, le dice al compilador que este tipo de ligadura no es necesaria. Esto permite al compilador generar código ligeramente más eficiente para llamadas a métodos constantes. Si embargo, en la mayoría de los casos no se obtendrá ninguna mejora global de rendimiento del programa, por lo que es mejor usar métodos constantes únicamente como una decisión de diseño, y no para intentar mejorar el rendimiento.

Produciendo el comportamiento adecuado Una vez que se sabe que toda la ligadura de métodos en Java se da de forma polimórfica a través de ligadura tardía, se puede escribir código que trate la clase base y saber que todas las clases derivadas funcionarán correctamente al hacer uso de ese mismo código. Dicho de otra forma, se "envía un mensaje a un objeto y se deja que éste averigüe la opción correcta a realizar". El ejemplo clásico de PO0 es el ejemplo de los "polígonos". Éste se usa frecuentemente porque es fácil de visualizar, pero desgraciadamente puede confundir a los programadores novatos, haciéndoles pensar que la PO0 sólo se usa en programación de gráficos, y esto no es cierto. El ejemplo de los polígonos tiene una clase base denominada Polígono y varios tipos derivados: Círculo, Cuadrado y Triángulo, etc. La razón por la que el ejemplo funciona tan bien es porque se puede decir sin problema "un círculo es un tipo de polígono" y se entiende. El diagrama de herencia muestra las relaciones:


228

Piensa en Java

L-4 Polígono

Conversión "hacia arriba" en el diagrama4

de herencias

dibujar( ) borrar( )

l

1 1 1 1

Círculo

1 1

Manejador

1

Cuadrado

1 1

Triángulo

dibujar( ) borrar( )

de círculo

La conversión hacia arriba podría darse en una sentencia tan simple como: Poligono s

=

new Circulo();

Aquí, se crea un objeto Círculo y la referencia resultante se asigna directamente a un Polígono, lo que podría parecer un error (asignar un tipo a otro); y sin embargo, está bien porque un Círculo es un Polígono por herencia. Por tanto, el compilador se muestra de acuerdo con la sentencia y no muestra ningún mensaje de error. Supóngase que se invoca a uno de los métodos de la clase base (que han sido superpuestos en clases derivadas) :

De nuevo, se podría esperar que se invoque al método dibujar( ) de Polígono porque se trata, después de todo, de una referencia a Polígono -por tanto, ¿cómo podría el compilador saber que tiene que hacer otra cosa? Y sin embargo, se invoca al Círculo.dibujar( ) correcto debido a la ligadura tardía (polimorfismo). El ejemplo siguiente hace lo propio de una manera ligeramente distinta: / / : c07:Poligonos.java / / Polimorfismo en Java.

class Poligono { void dibujar() void borrar ( ) {

{ } }

i

class Circulo extends Poliqono { void dibujar() { System.out .println ("Circulo.dibujar( )

") ;


7: Polimorfismo

1 void borrar ( ) { System.out .println ("Circulo.borrar ( )

") ;

class Cuadrado extends Poligono { void dibujar ( ) { System.out.println ("Cuadrado.dibujar ( )

") ;

1 void borrar ( ) { System.out .println ("Cuadrado.borrar ( )

") ;

1 1 class Triangulo extends Poligono { void dibujar ( ) { System.out.println("Triangulo.dibujar()");

1 void borrar ( ) { System.out.println("Triangulo.borrar()");

1 1 public class Poligonos { public static PoligonoAleatorio ( ) { switch ( (int)(Math.random ( ) * 3) ) { default: case O: return new Circulo ( ) ; case 1: return new Cuadrado ( ) ; case 2 : return new Triangulo ( ) ;

1

1 public static void main (String[] args) { Poligono [] S = new Poligono [9]; / / Rellenar el array con Polígonos: for(int i = O; i < s.length; i+t) S [i] = PoliqonoAleatorio ( ) ; / / Hacer llamadas a métodos polimórficos: for (int i = O; i < s.length; i++) S [i].dibujar ( ) ; 1

1

/ / / : m

229


230

Piensa en Java

La clase base Polígono establece la interfaz común a cualquier cosa heredada de Polígono -es decir, se pueden borrar y dibujar todos los polígonos. La clase derivada superpone estas definiciones para proporcionar un comportamiento único para cada tipo específico de polígono. La clase principal Polígonos contiene un método estático llamado poligonoAleatorio( ) que produce una referencia a un objeto Polígonos seleccionado al azar cada vez que se le invoca. Fíjese que se realiza una conversión hacia arriba en cada sentencia return que toma una referencia a un Círculo, Cuadrado o Triángulo, y lo envía fuera del método con tipo de retorno Polígonos. Así, al invocar a este método no se tendrá la opción de ver de qué tipo específico es el valor devuelto, dado que siempre se obtendrá simplemente una referencia a Polígono. El método main( ) contiene un array de referencias Polígono rellenadas mediante llamadas a poligonoAleatorio( ). En este punto se sabe que se tienen objetos de tipo Polígono, pero no se sabe nada sobre nada más específico que eso (y tampoco el compilador). Sin embargo, cuando se recorre este array y se invoca ,al método dibujar( ) para cada uno de sus objetos, mágicamente se da el comportamiento correcto específico de cada tipo, como se puede ver en un ejemplo de salida: Circulo.d i b u j a r ( )

Triangulo. dibujar () Circulo. d i b u ja r ( ) Circulo. dibujar () Circulo.dibujar () Cuadrado. d i b u j a r ( ) Triangulo. dibuj a r ( ) Cuadrado. d i b u j a r ( ) Cuadrado. d i b u j a r ( )

Por supuesto, dado que los polígonos se eligen cada vez al azar, cada ejecución tiene resultados distintos. El motivo de elegir los polígonos al azar es abrirse paso por la idea de que el compilador no puede tener ningún conocimiento que le permita hacer las llamadas correctas en tiempo de compilación. Todas las llamadas a dibujar( ) se hacen mediante ligadura dinámica.

Extensi bilidad Ahora, volvamos al ejemplo de los instrumentos musicales. Debido al polimorfismo, se pueden añadir al sistema tantos tipos como se desee sin cambiar el método afinar( ). En un programa PO0 bien diseñado, la mayoría de métodos deberían seguir el modelo de afinar( ) y comunicarse sólo con la interfaz de la clase base. Un programa así es extensible porque se puede añadir nueva funcionalidad heredando nucvos tipos dc datos de la clase base común. Los métodos que manipulan la interfaz de la clase base no necesitarán ningún cambio si se desea acomodarlos a las nuevas clases. Considérese qué ocurre si se toma el ejemplo de los instrumentos y se añaden nuevos métodos a la clase base y varias clases nuevas. He aquí el diagrama:


7: Polimorfismo

231

Instrumento void tocar() String que() void ajustar()

1

Viento

Percusión

void tocar() String que() void ajustar() '

void tocar() String que() void ajustar()

1

Cuerda void tocar() String que() void ajustar()

4

1

r-?

Maderaviento

Metal

void tocar() String que()

void tocar() void ajustar()

Todas estas nuevas clases funcionan correctamente con el viejo método afinar( ) sin tocarlo. Incluso si afinar( ) se encuentra en un archivo separado y se añaden métodos de la interfaz de Instrumento, afinar( ) funciona correctamente sin tener que volver a compilarlo. He aquí una implementación del diagrama de arriba: / / : c07:musica3:Musica3.java / / Un programa extensible. import java.util.*; class Instrumento { public void tocar() { System.out .println ("Instrumento.tocar ( ) ") ;

1 public String que() { return "Instrumento";

1 public void ajustar ( )

{ }

1 class Viento extends Instrumento

{


232

Piensa en Java

public void tocar ( ) { System.out .println ("Viento.tocar ( )

") ;

1 public String que() { return "Viento"; public void ajustar ( ) { }

}

1

class Percusion extends Instrumento { public void tocar() { System.out .println ("Percusion.tocar ( )

") ;

}

public String que() { return "Percusion"; public void ajustar0 { )

}

}

class Cuerda extends Instrumento { public void tocar ( ) { System.out .println ("Cuerda.tocar ( ) " ) ; 1 public String que ( ) { return "Cuerda"; 1 public void ajustar ( ) { } 1 '

class Metal extends Viento { public void tocar() { System.out .println ("Metal.tocar ( )

") ;

1 public void ajustar ( ) { System.out .println ("Metal.ajustar ( ) ");

class Maderaviento extends Viento public void tocar 0 { System.out.println("Maderaviento.tocar()"); J

public String que()

{

return "Maderaviento"; 1

public class Miisica3 { / / No le importa el tipo por lo que los nuevos tipos / / que se aniadan al sistema seguirรกn funcionando bien: static void afinar (Instrumento i) [ // ... i.tocar ( ) ;


7: Polimorfismo

static void af inarTodo (Instrumento[ ] e) for (int i = O; i < e.length; i++) afinar (e[i]) ;

233

{

public static void main (String[] args) { Instrumento[] orquesta = new Instrumento[5]; int i = 0; / / Conversión hacia arriba durante inserción en el array: orquesta [i++] = new Viento ( ) ; orquesta [i++]

orquesta [itt] orquesta [i++]

=

new Percusion ( ) ;

new Cuerda ( ) ; = new Metal ( ) ; orquesta [i++] = new Maderaviento ( ) a£inarTodo (orquesta); =

;

Los nuevos métodos son que( ), que devuelve una referencia a una cadena de caracteres con una descripción de la clase, y ajustar( ), que proporciona alguna manera de ajustar cada instrumento. En main( ), cuando se coloca algo dentro del array Instrumento se puede hacer una conversión hacia arriba automáticamente a Instrumento. Se puede ver que el método afinar( ) ignora por completo todos los cambios de código que hayan ocurrido alrededor, y sigue funcionando correctamente. Esto es exactamente lo que se supone que proporciona el polimorfismo. Los cambios en el código no causan daño a partes del programa que no deberían verse afectadas. Dicho de otra forma, el polimorfismo es una de las técnicas más importantes que permiten al programador "separar los elementos que cambian de aquellos que permanecen igual".

Superposición frente

sobrecarga

Tomemos un enfoque distinto del primer enfoque de este capítulo. En el programa siguiente, se cambia la interfaz del método tocar( ) en el proceso de sobrecarga, lo que significa que no se ha superpuesto el método, sino que se ha sobrecargado. El compilador permite sobrecargar métodos de forma que no haya quejas. Pero el comportamiento no es probablemente lo que se desea. He aquí un ejemplo: / / : c07:ErrorViento.java / / Cambiando la interfaz accidentalmente.

class NotaX { public static final int DO-MAYOR-C = O, DO-SOSTENIDO

=

1, SI-BEMOL

=

2;


234

Piensa en Java

class InstrumentoX { public void tocar (int NotaX) { System.out.println("InstrumentoX.tocar()");

1

class VientoX extends InstrumentoX { / / Cambia la interfaz del método: public void tocar(NotaX n) { System.out .println ("VientoX.tocar (NotaX n) ") ;

1

public class Errorviento { public static void afinar (InstrumentoX i) { // ... i .tocar (NoteX.DO-MAYOR) ; 1 public static void main(String[] args) { VientoX flauta = new VientoX(); ;No es el comportamiento deseado ! afinar (flauta); / 1 } ///:-

Hay otro aspecto confuso en este caso. En InstrumentoX, el método tocar( ) toma un dato entero con el identificador NotaX. Es decir, incluso aunque NotaX es un nombre de clase, también puede usarse como identificador sin problemas. Pero en VientoX, tocar( ) toma una referencia a NotaX que tiene un identificador n. (Aunque podría incluso decirse tocar(NotaX NotaX) sin que diera error.) Por consiguiente parece que el programador pretendía superponer tocar( ) pero equivocó los tipos del método. El compilador, sin embargo, asumió que se pretendía una sobrecarga y no una superposición. Fíjese que si se sigue la convención de nombres estándar de Java, el identificador de parámetros sería notaX ('n' minúscula), que lo distinguiría del nombre de la clase. En afinar, se envía al InstrumentoX i el mensaje tocar( ), con uno de los miembros de NotaX (DO-MAYOR) como parámetro. Dado que NotaX contiene definiciones int, se invoca a la versión ahora sobrecargada del método tocar( ), y dado que ése no ha sido superpuesto, se emplea la versión de la clase base.

La salida es:

Ciertamente esto no parece ser una llamada a un método polimórfico. Una vez que se entiende lo que está ocurriendo, se puede solventar el problema de manera bastante sencilla, pero imagínese lo


difícil que podría ser encontrar el fallo cuando se encuentre inmerso en un programa de tamaño significativo.

Clases y métodos abstractos En todos los ejemplos de instrumentos, los métodos de la clase base Instrumento eran métodos "falsos". Si se llega a invocar alguna vez a estos métodos daría error. Esto es porque la intención de Instrumento es simplemente crear una interfaz común para todas las clases que se derivan del mismo.

La única razón para establecer esta interfaz común es que ésta se pueda expresar de manera distinta para cada subtipo diferente. Establece una forma básica, de forma que se puede decir qué tiene en comun con todas las clases derivadas. Otra manera de decir esto es llamar a la clase Instrumento, una clase base abstracta (o simplemente clase abstracta). Se crea una clase abstracta cuando se desea manipular un conjunto de clases a través de una interfaz común. Todos los métodos de clases derivadas que encajen en la declaración de la clase base se invocarán utilizando el mecanismo de ligadura dinámica. (Sin embargo, como se vio en la sección anterior, si el nombre del método es el mismo que en la clase base, pero los parámetros son diferentes, se tiene sobrecarga, lo cual probablemente no es lo que se desea.) Si se tiene una clase abstracta como Instrumento, los objetos de esa clase casi nunca tienen significado. Es decir, Instrumento simplemente tiene que expresar la interfaz, y no una implementación particular, de forma que no tiene sentido crear objetos de tipo Instrumento, y probablemente se desea evitar que ningún usuario llegue a hacerlo. Esto se puede lograr haciendo que iodos los métodos de Instrumento muestren mensajes de error, pero de esta forma se retrasa la información hasta tiempo de ejecución, y además es necesaria una comprobación exhaustiva y de confianza por parte del usuario. Siempre es mejor capturar los problemas en tiempo de ejecución. Java proporciona un mecanismo para hacer esto, denominado el método abstracto'. Se trata de un método incompleto; tiene sólo declaración faltándole los métodos. La sintaxis para una declaración de método abstracto es: abstract void f ( ) ;

Toda clase que contenga uno o más métodos abstractos, se califica de abstracto. (De todos modos, el compilador emite un mensaje de error). Si una clase abstracta está incompleta, ¿qué se supone que debe hacer el compilador cuando alguien intenta crear un objeto de esa clase? No se puede crear un objeto de una clase abstracta de forma segura, por lo que se obtendrá un mensaje de error del compilador. De esta manera, el compilador asegura la ptireza de la clase abstracta, y no hay que preocuparse de usarla mal. Si se hereda de una clase abstracta y se desea hacer objetos del nuevo tipo, hay que proporcionar definiciones de métodos para todos los métodos que en la clase base eran abstractos. Si no se hace así (y uno puede elegir no hacerlo) entonces la clase derivada también será abstracta y el compilador obligará a calificar esa clase con la palabra clave abstract. ' Para los programadores de C++, esto es análogo a lafunción virtual pura

de C++.


236

Piensa en Java

Es posible crear una clase abstracta sin incluir ningún método abstracto en ella. Esto es útil cuando se desea una clase en la que no tiene sentido tener métodos abstractos, y se desea evitar que existan instancias de esa clase.

La clase Instrumento puede convertirse fácilmente en una clase abstracta. Sólo serán abstractos alguno de los métodos, puesto que hacer una clase abstracta no fuerza a hacer abstractos todos sus métodos. Quedará del siguiente modo: abstract Instrumento

void tocar(); String que() /* ...*/ }; void ajustar();

1

I extends

Viento

I

1

I

extends

Percusión void tocar() String que() void ajustar()

void tocar() String que() void ajustar()

extends Maderaviento

1

I

extends

1

void tocar() String que() void ajustar()

extends

1

Metal1

String que()

void ajustar()

He aquí el código de la orquesta modificado para que use clases y métodos abstractos: / / : c07:musica4:Musica4.java / / Clases y m6todos abstractos. import java.uti1. *; abstract class Instrumento { int i; / / almacenamiento asignado a cada uno public abstract void tocar ( ) ; public Strinq q u e ( ) { return "Instrumento"; 1 public abstract void ajustar();

1


7: Polimorfismo

class Viento extends Instrumento { public void tocar() { System.out .println ("Viento.tocar ( )

") ;

1 public String que() { return "Viento"; public void ajustar ( ) { ]

]

1 class Percusion extends Instrumento { public void tocar ( ) { Systern.out.println("Percusion.tocar()");

1 public String que() { public void ajustar ( )

return "Percusion";

}

{ }

1 class Cuerda extends Instrumento {

public void tocar ( ) { System.out .println ("Cuerda.tocar ( )

") ;

1 public String que ( ) { return "Cuerda"; } public void ajustar ( ) { }

1 class Metal extends Viento { public void tocar ( ) { System.out .println ("Metal.tocar ( ) " ) 1 public void ajustar ( ) { System.out.println ("Metal.ajustar ( )

;

") ;

1 class Maderaviento extends Viento { public void tocar() { System.out .println ( "Maderaviento.tocar ( ) ") ; }

public String que()

{

return "Maderaviento";

]

public class Musica4 { / / No le importa el tipo, por lo que los nuevos tipos / / que se aniadan al sistema seguirรกn funcionando correctamente: static void afinar(1nstrumento i) {

237


238

Piensa en Java

// ... i.tocar ( ) ;

1 static void afinarTodo (Instrumento [] e) { for(int i = O; i < e.length; i++) af inar (e[i]) ; 1 public static void main (String[] args) { Instrumento [ ] orquesta = new Instrumento [S]; int i = 0; / / Conversión hacia arriba durante la inversión en el array: orquesta [i++]. = new Viento ( ) ; orquesta [i++] = new Percusion ( ) ; orquesta [i++] = new Cuerda ( ) ; orquesta [i++] = new Metal ( ) ; orquesta [i++] = new Maderaviento ( ) ; afinarTodo (orquesta);

1 1 ///:-

Se puede ver que realmente no hay cambios más que en la clase base. Ayuda crear clases y métodos abstractos porque hacen que esa abstracción de la clase sea explícita, e indican, tanto al usuario, como al compilador cómo se tiene que usar.

Constructores

polimorfismo

Como es habitual, los constructores son distintos de otros tipos de métodos. Esto también es cierto cuando se ve involucrado el polimorfismo. Incluso aunque los constructores no sean polimórficos (aunque se puede tener algún tipo de "constructor virtual", como se verá en el Capítulo 12), es importante entender la forma en que trabajan los constructores en jerarquías complejas y con polimorfismo. Esta idea ayudará a evitar posteriores problemas.

Orden de llamadas a constructores El orden de las llamadas a los constructores se comentó brevemente en el Capítulo 4, y de nuevo en el Capítulo 6, pero esto fue antes de introducir el polimorfismo. En el constructor de una clase derivada siempre se invoca a un constructor de la clase base, encadenando la jerarquía de herencias de forma que se invoca a un co~istructorde cada clase base. Esto tiene sentido porque el constructor tiene un trabajo especial: ver que el objeto se ha construido correctamente. Una clase derivada tiene acceso, sólo a sus propios miembros, y no a aquéllos de la clase base (cuyos miembros suelen ser generalmente privados). Sólo el constructor de la clase base tiene el conocimiento adecuado y el acceso correcto para inicializar sus propios elementos. Por consiguiente, es esencial que se llegue a invocar a todos los constructores, si no, no se construiría


7: Polimorfismo

239

el objeto entero. Ésta es la razón por la que el compilador realiza una llamada al constructor por cada una de las clases derivadas. Si no se llama explícitamente al constructor de la clase base en el cuerpo del constructor de la clase derivada, llamará al constructor por defecto. Si no hay constructor por defecto, el compilador se quejará. (En el caso en que una clase no tenga constructores, el compilador creará un constructor por defecto automáticamente.) Echemos un vistazo a un ejemplo que muestra los efectos de la composición, la herencia y el polimorfismo en el orden de construcción: / / : c07:Bocadillo.java / / Orden de llamadas a constructores. class Comida { Comida ( ) { System.out .println ("Comida( ) 1 class Pan { Pan 0 { System.out .println ("PanO

") ;

") ;

}

1

1 class Queso { Queso ( ) { System.out .println ("Queso0

") ;

class Lechuga { Lechuga ( ) { System.out .println ("Lechuga( ) ") ;

class Almuerzo extends Comida { Almuerzo ( ) { System.out .println ("Almuerzo( )

" ) ;}

class AlmuerzoPortable extends Almuerzo { AlmuerzoPortable ( ) { System.out.println("AlmuerzoPortable 0 " ) ;

class Bocadillo extends AlmuerzoPortable Pan b = new Pan(); Queso c = new Queso 0 ; Lechuga 1 = new Lechuga ( ) ; Bocadillo ( ) { System.out .println ("Bocadillo( ) " ) ; 1

{

}


240

Piensa en Java

public static void main (String[] args) new Bocadillo ( ) ; 1 1 ///:-

{

Este ejemplo crea una clase compleja a partir de las otras clases, y cada clase tiene un constructor que la anuncia a sí misma. La clase principal es Bocadillo, que refleja tres niveles de herencia (cuatro, si se cuenta la herencia implícita de Object) y tres objetos miembro. Cuando se crea un objeto Bocadillo en el método m&( ), la salida es: Comida ( ) Almuerzo ( ) AlmuerzoPortable ( ) Pan ( ) Queso ( ) Lechuga ( ) Bocadillo ( )

.

Esto significa que el orden de las llamadas al constructor para un objeto completo es como sigue: 1.

Se invoca al constructor de la clase base. Este paso se repite recursivamente de forma que se construya primero la raíz de la jerarquía, seguida de la siguiente clase derivada, etc. y así hasta que se llega a la última clase derivada.

2.

Se llama a los inicializadores de miembros en el orden de declaración.

3.

Se llama al cuerpo del constructor de la clase derivada.

El orden de las llamadas a los constructores es importante. Cuando se hereda, se sabe todo lo relativo a la clase base y se puede acceder a cualquier miembro público y protegido de la clase base. Esto significa que debemos ser capaces de asumir que todos los miembros de la clase base sean válidos cuando se está en la clase derivada. En un método normal, la construcción ya ha tenido lugar, de forma que se han construido todos los miembros de todas las partes del objeto. Dentro del constructor, sin embargo, hay que ser capaz de asumir que se han construido todos los miembros que se usan. La única garantía de esto es que se llame primero al constructor de la clase base. Después, en el constructor de la clase derivada, se inicializarán todos los miembros a los que se puede acceder en la clase base. "Saber que son válidos todos los miembros" dentro del constructor es otra razón para, cuando sea posible, inicializar todos los objetos miembro (es decir, los objetos ubicados en la clase utilizando la composición) al definir la clase (por ejemplo, b, c y 1en el ejemplo anterior). Si se sigue esta práctica, se ayudará a asegurar que se han inicializado todos los miembros de la clase base y los objetos miembro del objeto actual. Desgraciadamente, esto no gestiona todos los casos, como se verá en la siguiente sección.

Herencia Cuando se usa composición para crear una clase nueva, no hay que preocuparse nunca de finalizar los objetos miembros de esa clase. Cada miembro es un objeto independiente, y por consiguiente, será eliminado por el recolector de basura de modo independiente. Con la herencia, sin embargo,


7: Polimorfismo

241

hay que superponer el método finalize( ) de la clase derivada si se tiene alguna limpieza especial a realizar como parte de la recolección de basura. Cuando se superpone el método finalize( ) en una clase heredada, es importante que recordemos invocar a la versión de la clase base de finabe( ), puesto que de otra forma no se finalizará la clase base. El siguiente ejemplo lo prueba: / / : c07:Rana.java / / Probando finalize con herencia. class HacerFinalizacionBase { public static boolean indicador

=

false;

1

class Caracteristica ( String S; Caracteristica (String c) { S = c; System.out.println( "Creando Caracteristica " + S) ; 1 protected void finalize ( ) { System.out.println( "finalizando Caracteristica " + S); 1

class CriaturaViviente { Caracteristica p = new Caracteristica ("esta vivo") ; CriaturaViviente ( ) { System.out.println("CriaturaViviente()"); 1 protected void finalizeo throws Throwable { System.out.println( "Finalizando Criaturaviviente"); / / ;Llamar a la versión de la clase base al FINAL! if(HacerFinalizacionBase.indicador) super. finalize ( ) ; 1 1 class Animal extends CriaturaViviente ( Caracteristica p = new Caracteristica ("tiene corazon") ; Animal() (


242

Piensa en Java

System.out .println ("Animal( )

") ;

protected void finalize() throws Throwable System.out.println("Finalizando Animal"); if(HacerFinalizacionBase.indicador) super.finalize ( ) ;

{

class Anfibio extends Animal { Caracteristica p = new Caracteristica("puede vivir en el agua"); Anfibio ( ) { System.out .println ("Anfibio( ) " ) ; 1 protected void finalize ( ) throws Throwable { System.out.println("Finalizando Anfibio"); if(HacerFinalizacionBase.indicador) super.finalize ( ) ; }

}

public class Rana extends Anfibio Rana 0 System.out.println("Rana ( ) ");

{

}

protected void f inalize ( ) throws Throwable System.out.println("Fina1izando Rana"); if(HacerFinalizacionBase.indicador) super. finalize ( ) ;

{

public static void main (String[] args) { if (args. length ! = O & & args [O] .equals ("finalizar") ) HacerFinalizacionBase.indicador = true; else System.out .println ("No finalizando las bases") ; new Rana(); / / Se convierte en basura autom谩ticamente System.out .println ( " iAdi03 ! " ) ; / / Forzar la invocaci贸n de todas las f u r i c i o r i e s : System.gc 0 ;

1 1 ///:-


7: Polimorfismo

243

La clase HacerFinalizacionBase simplemente guarda un indicador que informa a cada clase de la jerarquía si debe llamar a super.finalize( ). Este indicador se pone a uno con un parámetro de línea de comandos, de forma que se puede ver el comportamiento con y sin finalización de la clase base. Cada clase de la jerarquía también contiene un objeto miembro de clase Característica. Se verá que independientemente de si se llama a los finalizadores de la clase base, siempre se finalizan los objetos miembros Característica. Cada finalize( ) superpuesto debe tener acceso, al menos a los miembros protegidos puesto que

el método finalhe( ) de la clase Object es protegido y el compilador no permitirá reducir el acceso durante la herencia. ("Amistoso" es menos accesible que protegido.) En Rana.main( ), se configura el indicador de HacerFinaiizacionBase y se crea un único objeto Rana. Recuerde que el recolector de basura -y en particular la finalización- podrían no darse para algún objeto en particular, por lo que para fortalecer la limpieza, la llamada a System.gc( ) dispara el recolector de basura, y por consiguiente, la finalización. Sin la finalización de la clase base, la salida es: No finalizando las bases Creando Caracteristica esta vivo CriaturaViviente ( ) Creando Caracteristica tiene corazon Animal ( ) Creando Caracteristica puede vivir en el agua Anfibio ( ) Rana ( ) i Adios ! finalizando Rana finalizando Caracteristica esta vivo finalizando Caracteristica tiene corazon finalizando Caracteristica puede vivir en el agua

Se puede ver que, sin duda, no se llama a los finalizadores para las clases base de Rana (los miembros objeto son finalizados, como se esperaba). Pero si se añade el parámetro "finalizar" en la línea de comandos, se tiene: Creando Caracteristica esta vivo CriaturaViviente ( ) Creando Caracteristica tiene corazon Animal ( ) Creando Caracteristica puede vivir en el agua Anfibio ( ) Rana í ; Adios ! finalizando Rdnd finalizando Anfibio finalizando CriaturaViviente Finalizando Caracteristica esta vivo


244

Piensa en Java

finalizando Caracteristica tiene corazon finalizando Caracteristica puede vivir en el agua

Aunque el orden en que finalizan los objetos miembro es el mismo que el de su creación, técnicamente el orden de finalización de los objetos no se especifica. Con las clases base, sin embargo, se tiene control sobre el orden de finalización. El mejor a usar es el que se muestra aquí, que es el inverso al orden de inicialización. Siguiendo la forma que se usa en los destructores de C++, se debería hacer primero la finalización de la clase derivada, y después la finalización de la clase base. Esto e s porque la finalización de la clase derivada podría llamar a varios métodos de la clase base

que requieran que los componentes de la clase base sigan vivos, por lo que no hay que destruirlos prematuramente.

Comportamiento de métodos polimórficos dentro de constructores La jerarquía de llamadas a constructores presenta un interesante dilema. ¿Qué ocurre si uno está dentro de un constructor y se invoca a un método de ligadura dinámica del objeto que se está construyendo? Dentro de un método ordinario se puede imaginar lo que ocurrirá -la llamada que conlleva una ligadura dinámica se resuelve en tiempo de ejecución, pues el objeto no puede saber si pertenece a la clase dentro de la que está el método o a alguna clase derivada de ésta. Por consistencia, se podría pensar que esto es lo que debería pasar dentro de los constructores. Éste no es exactamente el caso. Si se invoca a un método de ligadura dinámica dentro de un constructor, se utiliza la definición superpuesta de ese método. Sin embargo, el efecto puede ser bastante inesperado, y puede ocultar errores difíciles de encontrar. Conceptualmente, el trabajo del constructor es crear el objeto (lo que es casi una proeza). Dentro de cualquier constructor, el objeto entero podría formarse sólo parcialmente -sólo se puede saber que se han inicializado los objetos de clase base, pero no se puede saber qué clases se heredan. Una llamada a un método de ligadura dinámica, sin embargo, se "sale" de la jerarquía de herencias. Llama a un método de una clase derivada. Si se hace esto dentro del constructor, se llama a un método que podría manipular miembros que no han sido aún inicializados -lo que ocasionará problemas. Se puede ver este problema en el siguiente ejemplo: / / : c07:ConstructoresMultiples.java / / Los constructores y el polimorfismo / / no producen lo que cabría esperar. abstract class Grafica { abstract void dibujo ( ) ; Grafica ( ) { System.out .println ( " G