231
PEDRO SALGUEIRO GÓMEZ 2010

Fundamentos de JAVA

Embed Size (px)

DESCRIPTION

Un gran manual de JAVA. Muy fácil de entender.

Citation preview

Page 1: Fundamentos de JAVA

PEDRO SALGUEIRO GÓMEZ 2010

Page 2: Fundamentos de JAVA
Page 3: Fundamentos de JAVA

Tabla de contenidos I - LA TECNOLOGÍA JAVA ............................................. 7

1. Introducción a Java. ................................................................ 7 1.1. Esquema de compilación y ejecución de un programa java. 7 1.2. Tecnologías Java. ................................................................ 8 1.3. Kits de desarrollo de Java. ................................................... 9

2. Programas de referencia del JDK. ......................................... 9 2.1. Compilador de programas Java: «javac» ............................. 9 2.2. Intérprete de bytecodes Java: «java» ................................. 10 2.3. Intérprete «jre». ................................................................. 11 2.4. Depurador «jdb». ............................................................... 12 2.5. Analizador «javah». ........................................................... 12 2.6. Desensamblador «javap». .................................................. 12 2.7. Javadoc. ............................................................................. 13

2.7.1. Descripción.................................................................. 13 2.7.2. Doclets. ........................................................................ 13 2.7.3. Comentar el código fuente. .......................................... 13 2.7.4. HTML estándar. .......................................................... 13 2.7.5. Marcas de «javadoc». .................................................. 14 2.7.6. Marcas de documentación de clases e interfaces. ........ 14 2.7.7. Marcas de documentación de campos. ........................ 14 2.7.8. Marcas de documentación de constructores y métodos.15 2.7.9. Opciones. ..................................................................... 15 2.7.10. Opciones con doclet estándar. ................................... 16

2.8. Appletviewer. .................................................................... 16 2.9. Compresor «jar». ............................................................... 16

II - PROGRAMACIÓN CON JAVA ................................ 18 1. Estructura de un programa java .......................................... 18

1.1. Estructura básica de un programa Java. ............................. 18 1.2. Paquetes. ............................................................................ 19

1.2.1. Introducción. ............................................................... 19 1.2.2. Acceso a otros paquetes («import»)............................. 19 1.2.3. Importaciones estáticas («import static»). ................... 19 1.2.4. Nomenclatura de paquetes. .......................................... 20 1.2.5. Ejecución de clases identificadas mediante una ruta de

paquetes. ................................................................................ 20 1.2.6. Variable de entorno «CLASSPATH». ......................... 21 1.2.7. Paquetes estándar de Java. ........................................... 21

2. Sintaxis básica ........................................................................ 21 2.1. Identificadores. .................................................................. 21 2.2. Palabras clave y reservadas. .............................................. 22 2.3. Comentarios y documentación en Java. ............................. 22 2.4. Variables y tipos de datos. ................................................. 22 2.5. Literales. ............................................................................ 23 2.6. Operadores......................................................................... 24

2.6.1. Introducción. ............................................................... 24 2.6.2. Operadores aritméticos. ............................................... 25 2.6.3. Operadores relacionales y condicionales. .................... 25 2.6.4. Operadores a nivel de bits. .......................................... 26 2.6.5. Operadores de asignación. ........................................... 26 2.6.6. Operador ternario «if-then-else».................................. 26 2.6.7. Operador «instanceof». ................................................ 26 2.6.8. Moldeo (casting) de operadores................................... 27

2.7. Estructuras de programación. ............................................ 27 2.7.1. Sentencias de salto. ...................................................... 27 2.7.2. Sentencias de bucle. .................................................... 28 2.7.3. Control de errores y excepciones. ................................ 29 2.7.4. Control general del flujo. ............................................. 29

3. Almacenamiento de datos en memoria ................................ 30 3.1. Arrays. ............................................................................... 30

3.1.1. Arrays de una dimensión. ............................................ 30 3.1.2. Arrays multidimensionales. ......................................... 31

3.2. Cadenas de caracteres. ....................................................... 31 3.2.1. La clase «String». ........................................................ 31 3.2.2. Funciones de conversión de strings. ............................ 33 3.2.3. Diferencia entre variables string y variables de tipo

primitivo. ............................................................................... 33 III - ORIENTACIÓN A OBJETOS .................................. 34

1. Fundamentos de la orientación a objetos en Java ............... 34

1.1. Clases y objetos. ................................................................ 34 1.1.1. ¿Qué es una clase? ....................................................... 34 1.1.2. ¿Qué es un objeto? ....................................................... 34 1.1.3. Ejemplo de clases y objetos. ........................................ 34

1.2. Campos, propiedades, métodos y eventos. ......................... 34 1.2.1. ¿Qué es un campo? ...................................................... 35 1.2.2. ¿Qué es una propiedad? ............................................... 35 1.2.3. ¿Qué es un método? ..................................................... 35 1.2.4. ¿Qué es un evento? ...................................................... 35 1.2.5. Ejemplo de campos, propiedades, métodos y eventos.. 35

1.3. ¿Qué es encapsulación? ..................................................... 35 1.3.1. ¿En qué consiste encapsular los datos? ........................ 35 1.3.2. Beneficios de la encapsulación. ................................... 35

1.4. ¿Qué es sobrecarga? ........................................................... 36 1.4.1. ¿Por qué se sobrecarga? ............................................... 36 1.4.2. Ejemplo de sobrecarga. ................................................ 36

1.5. Definición de una clase. ..................................................... 36 1.5.1. Cómo definir una nueva clase. ..................................... 36 1.5.2. Niveles de acceso. ........................................................ 37 1.5.3. Modificadores de tipos de clases. ................................ 37 1.5.4. Cómo añadir campos y métodos a las clases. ............... 37 1.5.5. Cómo añadir constructores a las clases. ....................... 39 1.5.6. Cómo compartir miembros. ......................................... 39 1.5.7. Inicializadores. ............................................................. 40 1.5.8. Finalizadores. ............................................................... 40

1.6. Declaración de métodos. .................................................... 41 1.6.1. Sintaxis de un método. ................................................. 41 1.6.2. Valor de retorno de un método. ................................... 41 1.6.3. Nombre del método. .................................................... 41 1.6.4. Métodos de instancia. .................................................. 41 1.6.5. Métodos estáticos. ........................................................ 41 1.6.6. Paso de parámetros. ..................................................... 42 1.6.7. Argumentos variables. ................................................. 42

1.7. Tipos enumerados (enums). ............................................... 43 1.7.1. Declaración de enumeraciones. .................................... 43 1.7.2. Declarar constructores, métodos y variables dentro de

un tipo enumerado. ................................................................ 44 1.7.3. Comparación entre tipos enumerados. ......................... 44

1.8. Creación y destrucción de objetos. ..................................... 45 1.8.1. Declaración de objetos. ................................................ 45 1.8.2. Utilización de objetos. ................................................. 45 1.8.3. Destrucción de objetos (el recolector de basura). ......... 45

1.9. Factores en el diseño: cohesión y acoplamiento. ................ 46 1.9.1. Cohesión. ..................................................................... 46 1.9.2. Acoplamiento. .............................................................. 46

2. UM, el Lenguaje Unificado de Modelado ............................. 47 2.1. Ventajas e inconvenientes de UML. .................................. 47 2.2. Diagramas UML. ............................................................... 47

2.2.1. Diagramas de clases. .................................................... 47 2.2.2. Diagramas de objetos. .................................................. 48 2.2.3. Diagramas de interacción. ............................................ 49 2.2.4. Otros diagramas UML. ................................................ 50 2.2.5. Diagramas UML para representar aspectos físicos del

sistema. .................................................................................. 51 2.3. Relaciones entre clases: Diagramas de clases UML. ......... 52

2.3.1. Asociación. .................................................................. 52 2.3.2. Dependencia. ............................................................... 54 2.3.3. Herencia (generalización y especialización). ............... 55

3. Herencia y jerarquía de clases. ............................................. 55 3.1. ¿Qué es herencia? .............................................................. 55

3.1.1. Ejemplo de herencia. .................................................... 56 3.1.2. Beneficios de la herencia. ............................................ 56

3.2. Cómo definir clases base y clases derivadas. ..................... 56 3.2.1. Definición de una clase base. ....................................... 56 3.2.2. Definición de una clase derivada. ................................ 56 3.2.3. Ejemplo de herencia. .................................................... 57 3.2.4. Clases no heredables. ................................................... 57

3.3. Sobreescritura o reescritura de miembros. ......................... 57 3.3.1. Definición de miembros en clases derivadas. .............. 57

Page 4: Fundamentos de JAVA

3.3.2. Ejemplo de miembros reescritos. ................................. 58 3.3.3. Definición de miembros en la clase base como

reescribibles........................................................................... 58 3.3.4. Reescritura en la clase derivada. .................................. 58 3.3.5. Diferencias entre sobrecarga y reescritura de un

método. .................................................................................. 59 3.3.6. Referencias «this» y «super». ...................................... 59 3.3.7. Ejemplo de reescritura de métodos. ............................. 59

3.4. Clases y métodos abstractos. ............................................. 60 3.4.1. Cómo crear clases abstractas. ...................................... 60 3.4.2. Cómo crear métodos abstractos. .................................. 60

3.5. Control de acceso. ............................................................. 60 3.6. La clase «Object». ............................................................. 61 3.7. Polimorfismo. .................................................................... 62

4. Interfaces. ............................................................................... 63 4.1. ¿Qué son las interfaces? .................................................... 64

4.1.1. Sintaxis. ....................................................................... 64 4.1.2. Interfaces vs. herencia. ................................................ 64 4.1.3. Interfaces vs. abstracción. ............................................ 64 4.1.4. Beneficios de las interfaces. ........................................ 64

4.2. Cómo crear una interfaz .................................................... 65 4.2.1. Definición de una interfaz. .......................................... 65 4.2.2. Miembros de una interfaz. ........................................... 65 4.2.3. Implementación de las interfaces................................. 65 4.2.4. Implementación de varias interfaces............................ 66 4.2.5. Jerarquías de interfaces. ............................................... 66 4.2.6. Consideraciones........................................................... 66

4.3. Clases e interfaces internas. ............................................... 67 4.3.1. Clases e interfaces internas static. ............................... 67 4.3.2. Clases internas miembro. ............................................. 67 4.3.3. Clases internas locales. ................................................ 68 4.3.4. Clases anónimas. ......................................................... 68

5. Gestión de errores y excepciones en Java ............................ 69 5.1. Introducción. ...................................................................... 69 5.2. Excepciones (clase «Exception»). ..................................... 70

5.2.1. Generar excepciones en Java. ...................................... 70 5.2.2. Gestión de excepciones. .............................................. 70 5.2.3. Excepciones predefinidas. ........................................... 70 5.2.4. Crear excepciones personalizadas. .............................. 71

5.3. Errores (clase «Error»). ..................................................... 71 5.3.1. Gestión de errores. ....................................................... 71

5.4. Aseveraciones (Assertions). .............................................. 71 5.4.1. Introducción. ............................................................... 71 5.4.2. Uso de «assert». ........................................................... 71 5.4.3. Compilación. ............................................................... 72 5.4.4. Activación/Desactivación de las aseveraciones. .......... 72 5.4.5. Ejemplos de uso........................................................... 72 5.4.6. Conclusiones. .............................................................. 73

IV - CLASES ÚTILES DE JAVA ..................................... 74 1. Colecciones clásicas. .............................................................. 74

1.1. Introducción. ...................................................................... 74 1.2. Tipos genéricos. ................................................................. 74

1.2.1. Tipado de clases aplicando genéricos. ......................... 74 1.2.2. Moldeo y polimorfismo aplicando genéricos. ............. 74 1.2.3. Conversión de cualquier clase o interfaz a genérica. ... 75 1.2.4. Aplicar genéricos sobre clases genéricas. .................... 75

1.3. Interfaz «Enumeration». .................................................... 76 1.4. Vectores. ............................................................................ 76

1.4.1. La clase «Vector». ....................................................... 76 1.4.2. Actualizaciones en un vector. ...................................... 77 1.4.3. Ordenación en un vector. ............................................. 77 1.4.4. Búsquedas en un vector. .............................................. 77

1.5. Conjunto de bits (clase «BitSet»). ..................................... 78 1.6. Pilas (clase «Stack»). ......................................................... 78 1.7. Mapas o diccionarios. ........................................................ 78

1.7.1. La clase «Hashtable». .................................................. 78 1.7.2. Actualizaciones en un mapa. ....................................... 79 1.7.3. Ordenación de un mapa. .............................................. 79 1.7.4. Búsquedas en un mapa. ............................................... 79

2. Nuevas colecciones. ................................................................ 80 2.1. Introducción. ...................................................................... 80

2.2. Métodos comunes a las colecciones. .................................. 81 2.3. Recorrido de colecciones: interfaces «Iterator» y

«ListIterator». ........................................................................... 81 2.4. Ordenación de colecciones: interfaces «Comparable» y

«Comparator». .......................................................................... 82 2.4.1. Interfaz «Comparable»................................................. 82 2.4.2. Interfaz «Comparator». ................................................ 83

2.5. Listas (interfaz «List»). ...................................................... 83 2.5.1. Clase «ArrayList». ....................................................... 83 2.5.2. Clase «LinkedList». ..................................................... 83 2.5.3. Actualizaciones en una lista. ........................................ 83 2.5.4. Ordenación en una lista. ............................................... 84 2.5.5. Búsquedas en una lista. ................................................ 84

2.6. Colas (interfaz «Queue»). .................................................. 85 2.6.1. La clase «PriorityQueue». ............................................ 85 2.6.2. Colas de doble dirección (interfaz «Deque»). .............. 85 2.6.3. La clase «ArrayDeque». ............................................... 86 2.6.4. Actualizaciones en una cola. ........................................ 86 2.6.5. Ordenación en una cola. ............................................... 86

2.7. Conjuntos: interfaz «Set». .................................................. 86 2.7.1. Clase «HashSet». ......................................................... 87 2.7.2. Clase «LinkedHashSet». .............................................. 87 2.7.3. Clase «TreeSet». .......................................................... 87 2.7.4. Actualizaciones en un conjunto. .................................. 87 2.7.5. Ordenación en un conjunto. ......................................... 87 2.7.6. Búsquedas en un conjunto. .......................................... 88

2.8. Mapas (interfaz «Map»). .................................................... 88 2.8.1. Clase «HashMap». ....................................................... 88 2.8.2. La clase «IdentityHashMap». ...................................... 88 2.8.3. La clase «LinkedHashMap»......................................... 88 2.8.4. La clase «WeakHashMap»........................................... 89 2.8.5. Clase «TreeMap». ........................................................ 89 2.8.6. Actualizaciones en un mapa. ........................................ 89 2.8.7. Ordenación de un mapa. .............................................. 89 2.8.8. Búsquedas en un mapa. ................................................ 89

2.9. Clase «Arrays». .................................................................. 90 2.9.1. Ordenación y búsqueda en arrays. ............................... 90 2.9.2. Modificación de arrays. ............................................... 90

2.10. Clase «Collections». ........................................................ 90 3. Clases envoltorio para los tipos primitivos........................... 92

3.1. La clase «Character». ......................................................... 92 3.2. La clase «Number». ........................................................... 92 3.3. La clase «Float». ................................................................ 92 3.4. La clase «Double». ............................................................. 93 3.5. La clase «Integer». ............................................................. 93 3.6. La clase «Long». ................................................................ 94 3.7. La clase «Boolean». ........................................................... 94 3.8. Precisión arbitraria (clases «BigDecimal» y «BigInteger»).95 3.9. Empaquetado y desempaquetado automático. .................... 95

4. Operaciones matemáticas. ..................................................... 95 4.1. La clase «Math». ................................................................ 95 4.2. Valores aleatorios (clase «Random»). ................................ 96

5. Manipulación de secuencias de caracteres. .......................... 96 5.1. La interfaz «CharSequence». ............................................. 96 5.2. La interfaz «Appendable». ................................................. 97 5.3. La interfaz «Readable». ..................................................... 97 5.4. La clase «CharBuffer». ...................................................... 98 5.5. La clase «StringBuffer». .................................................... 98 5.6. La clase «StringBuilder». ................................................... 99 5.7. La clase «StringTokenizer»................................................ 99 5.8. La clase «Scanner». ........................................................... 99

5.8.1. Creación de un escáner. ............................................... 99 5.8.2. Modificación del patrón de delimitación. .................. 100 5.8.3. Reconocimiento de los términos. ............................... 101

6. Formateo y referencia cultural. .......................................... 101 6.1. Referencias culturales (la clase «Locale»). ...................... 101

6.1.1. Creación de referencias culturales. ............................ 101 6.1.2. Métodos de la clase «Locale». ................................... 102 6.1.3. Clases que aplican referencias culturales. .................. 102

6.2. Localizar una aplicación (la clase «ResourceBundle»). ... 102 6.2.1. Implementación de la clase «ResourceBundle». ........ 103

Page 5: Fundamentos de JAVA

6.2.2. La clase «ResourceBundle.Control».......................... 104 6.2.3. Administrador de caché. ............................................ 104 6.2.4. La clase «ListResourceBundle». ............................... 104 6.2.5. La clase «PropertyResourceBundle». ........................ 104

6.3. Clases para manipular fechas. .......................................... 105 6.3.1. Clase «Date». ............................................................ 105 6.3.2. Clases «TimeZone» y «SimpleTimeZone». .............. 105 6.3.3. Clases «Calendar» y «GregorianCalendar». .............. 106 6.3.4. Clases «DateFormat» y «SimpleDateFormat». .......... 106

6.4. Aplicar formatos (la clase «Formatter»). ......................... 107 6.4.1. ¿Cómo aplicar formatos según la referencia cultural?107 6.4.2. Sintaxis de formato. ................................................... 108 6.4.3. Conversiones. ............................................................ 108 6.4.4. Modificadores. ........................................................... 110 6.4.5. Tamaño. ..................................................................... 110 6.4.6. Precisión. ................................................................... 110 6.4.7. Índice de argumento. ................................................. 110

7. Objetos observadores y observables. .................................. 110 7.1. La clase «Observable». .................................................... 110 7.2. La interfaz «Observer». ................................................... 111

8. Gestión de recursos del sistema. ......................................... 112 8.1. La clase «Properties». ...................................................... 112

8.1.1. ¿Qué son las propiedades? ......................................... 112 8.1.2. Propiedades del sistema. ............................................ 112 8.1.3. Archivos de configuración......................................... 112

8.2. La clase «Runtime». ........................................................ 113 8.3. La clase «System». .......................................................... 114

8.3.1. Propiedades y métodos de System. ............................ 114 8.3.2. Entrada/Salida estándar. ............................................ 115 8.3.3. Propiedades del Sistema. ........................................... 115 8.3.4. Finalización. .............................................................. 116 8.3.5. Copia de arrays. ......................................................... 116 8.3.6. Salida del sistema. ..................................................... 116 8.3.7. Seguridad. .................................................................. 116

8.4. El API Preferences. ......................................................... 116 8.4.1. La clase base «Preferences». ..................................... 117 8.4.2. Usar el Registro de Windows con el API Preferences.119

8.5. Recolección de basura. .................................................... 120 8.5.1. El Garbage Collection. .............................................. 120 8.5.2. Referencias débiles. ................................................... 120

9. Reflexión y metadatos.......................................................... 122 9.1. La clase «Class». ............................................................. 122 9.2. La clase «Method». ......................................................... 123 9.3. La clase «Field». .............................................................. 124 9.4. Anotaciones en Java. ....................................................... 124

9.4.1. Definición de las anotaciones. ................................... 124 9.4.2. Anotaciones personalizadas. ...................................... 125

9.5. Cómo cargar recursos de un proyecto JAR. ..................... 125 9.6. La clase «ClassLoader». .................................................. 126

V - ENTRADA Y SALIDA DE DATOS ......................... 128 1. Modelo de flujo de datos en Java........................................ 128

1.1. Clases para lectura y escritura de datos. .......................... 128 1.2. Nombres de las clases de «java.io». ................................ 129 1.3. Clase «File». .................................................................... 130 1.4. Lectura y escritura de archivos. ....................................... 131 1.5. Archivos de texto. ............................................................ 131 1.6. Archivos no de texto. ....................................................... 132 1.7. Archivos de acceso aleatorio. .......................................... 132

2. Compresión zip y jar. .......................................................... 133 2.1. Compresión zip. ............................................................... 133

2.1.1. El paquete «java.util.zip». ......................................... 133 2.1.2. Ejemplo de compresión de archivos a zip.................. 134 2.1.3. Ejemplo de descompresión de un archivo zip. ........... 134

2.2. Compresión jar. ............................................................... 135 2.2.1. El paquete «java.util.jar». .......................................... 135

3. Serialización. ........................................................................ 136 3.1. ¿Qué es serialización y deserialización? .......................... 136 3.2. Interfaz «Serializable». .................................................... 136 3.3. Características de la serialización. ................................... 137 3.4. Interfaz «Externalizable». ................................................ 138 3.5. Serialización a formato XML. ......................................... 138

3.5.1. Uso de XMLEncoder: ................................................ 138 3.5.2. Uso de XMLDecoder: ................................................ 139 3.5.3. ¿Cómo serializar clases que no son JavaBeans? ........ 140

VI - HILOS DE EJECUCIÓN .........................................141 1. Implementación de hilos. ..................................................... 141

1.1. Programas de flujo único. ................................................ 141 1.2. Programas de flujo múltiple. ............................................ 142 1.3. La clase «Thread». ........................................................... 142

1.3.1. Métodos de clase. ....................................................... 142 1.3.2. Métodos de instancia. ................................................ 143

1.4. Creación de un hilo de ejecución. .................................... 144 1.5. Manipulación de un hilo. ................................................. 145

1.5.1. Arranque del hilo. ...................................................... 146 1.5.2. Suspensión del hilo. ................................................... 146 1.5.3. Parada del hilo. .......................................................... 146

2. Bloqueos y sincronización de hilos. ..................................... 147 2.1. Acceso concurrente. ......................................................... 147 2.2. Secciones críticas. ............................................................ 147 2.3. Monitores. ........................................................................ 148

3. Gestión de hilos. ................................................................... 149 3.1. Hilos Demonio (Daemon). ............................................... 149 3.2. Estados en el ciclo de vida de un thread. .......................... 149 3.3. Agrupamiento de hilos. .................................................... 150

3.3.1. El grupo de hilos por defecto. .................................... 150 3.3.2. Creación de un hilo en un grupo de forma explícita. . 150 3.3.3. La clase «ThreadGroup». ........................................... 150

3.4. Planificación y prioridad de hilos..................................... 151 3.4.1. Planificación (Scheduling). ........................................ 151 3.4.2. Prioridad. ................................................................... 151

3.5. Lanzador de tareas. .......................................................... 151 3.5.1. Clase «Timer». ........................................................... 151 3.5.2. Clase «TimerTask». ................................................... 152

VII - INTERFAZ GRÁFICA DE USUARIO .................153 1. Modelo de interfaz gráfica de usuario en Java. ................. 153

1.1. Diferencias entre AWT y Swing. ..................................... 153 1.2. Modelo de delegación de eventos. ................................... 154

1.2.1. Jerarquía de eventos. .................................................. 154 1.2.2. Gestión de los eventos. .............................................. 155 1.2.3. Notificación de eventos.............................................. 156 1.2.4. Eventos de teclado y ratón. ........................................ 157

1.3. Temporizador de eventos. ................................................ 157 2. Librería gráfica AWT .......................................................... 158

2.1. Estructura del AWT. ........................................................ 158 2.1.1. Puntos, rectángulos y dimensiones. ........................... 158 2.1.2. Componentes y contenedores. .................................... 158

2.2. Componentes. .................................................................. 159 2.2.1. Métodos y eventos de la clase «Component». ........... 159 2.2.2. Botones de pulsación (clase «Button»). ..................... 160 2.2.3. Lienzos (clase «Canvas»). ......................................... 161 2.2.4. Botones de comprobación (clase «Chekbox»). .......... 161 2.2.5. Grupo de botones (clase «CheckboxGroup»). ........... 162 2.2.6. Etiquetas (clase «Label»). .......................................... 162 2.2.7. Listas de opciones (clase «List»). .............................. 162 2.2.8. Lista de selección (clase «Choice»). .......................... 164 2.2.9. Barras de desplazamiento (clase «Scrollbar»). ........... 164 2.2.10. Campos de texto (clase «TextField»). ...................... 165 2.2.11. Áreas de texto (clase «TextArea»). .......................... 166

2.3. Contenedores. .................................................................. 166 2.3.1. Métodos y eventos de la clase «Container». .............. 166 2.3.2. Añadir componentes a un contenedor. ....................... 167 2.3.3. Ventanas (clase «Window»). ..................................... 167 2.3.4. Ventanas principales (clase «Frame»)........................ 168 2.3.5. Cuadros de diálogo (clase «Dialog»). ........................ 168 2.3.6. Paneles (clase «Panel»). ............................................. 169 2.3.7. Paneles desplazables (clase «ScrollPane»)................. 169 2.3.8. Cuadro de diálogo de archivos (clase «FileDialog»). 170

2.4. Menús. ............................................................................. 170 2.4.1. Menú principal (clase «Menu»). ................................ 171 2.4.2. Opciones de menú (clase «MenuItem»). .................... 171 2.4.3. Aceleradores de teclado (clase «MenuShortcut»). ..... 171 2.4.4. Barras de menús (clase «MenuBar»). ........................ 171

Page 6: Fundamentos de JAVA

2.4.5. Opciones de menú señalizables (clase

«CheckboxMenuItem»). ...................................................... 173 2.4.6. Menús contextuales (clase «PopupMenu»). .............. 173

2.5. Gestores de distribución (“layouts managers”). ............... 174 2.5.1. Diseño de flujo (clase «FlowLayout»)....................... 175 2.5.2. Diseño de borde (clase «BorderLayout»). ................. 175 2.5.3. Diseño de rejilla (clase «GridLayout»). ..................... 176 2.5.4. Diseño de bolsa de rejilla (clase «GridBagLayout»). 176 2.5.5. Diseño de carta (clase «CardLayout»). ...................... 177 2.5.6. Posicionamiento absoluto. ......................................... 177

2.6. La clase «Toolkit». .......................................................... 178 2.7. Imprimir con AWT. ......................................................... 179

2.7.1. Imprimir usando la clase «Graphics». ....................... 179 2.7.2. Imprimir componentes............................................... 179

2.8. Gráficos. .......................................................................... 180 2.8.1. El sistema de coordenadas. ........................................ 180 2.8.2. Pintando en AWT. ..................................................... 181 2.8.3. Clase «Graphics». ...................................................... 182 2.8.4. Fuentes de letras (clases «Font» y «FontMetrics»). ... 183 2.8.5. Colores (clase «Color»). ............................................ 185 2.8.6. Imágenes (clase «Image»). ........................................ 185

2.9. El portapapeles (la clase «Clipboard»). ........................... 186 2.9.1. La clase «Clipboard». ................................................ 186 2.9.2. La interfaz «ClipboardOwner». ................................. 187 2.9.3. La interfaz «Transferable» y la clase «StringSelection».187 2.9.4. La clase «DataFlavor». .............................................. 187 2.9.5. Ejemplo para copiar clases personalizadas al

portapapeles......................................................................... 189 3. Librería gráfica Swing ........................................................ 190

3.1. Componentes Swing. ....................................................... 190 3.1.1. Etiquetas (clase «JLabel»). ........................................ 190 3.1.2. Botón de pulsación (clases «JButton»). ..................... 190 3.1.3. Botón de conmutación (clases «JToogleButton»). .... 191 3.1.4. Casilla de verificación (clase «JCheckBox»). ........... 191 3.1.5. Botón de radio (clase «JRadioButton»). .................... 191 3.1.6. Grupo de botones (clase «ButtonGroup»). ................ 191 3.1.7. Lista (clase «JList»). .................................................. 191 3.1.8. Caja combinada (clase «JComboBox»). .................... 192 3.1.9. Campo de texto (clase «JTextField»). ....................... 192 3.1.10. Área de texto (clase «JTextArea»)........................... 192 3.1.11. Campo de contraseña (clase «JPasswordField»). .... 193 3.1.12. Panel de texto (clase «JTextPane»). ........................ 193 3.1.13. Campo de texto con formato (clase

«JFormattedText»). ............................................................. 193 3.1.14. Escala (clase «JSlider») y barras de progreso (clase

«JProgressBar»). ................................................................. 194 3.1.15. Escala (clase «JSlider»). .......................................... 194 3.1.16. Menús. ..................................................................... 195 3.1.17. Panel (clase «JPanel»). ............................................ 195 3.1.18. Árboles (clase «JTree»). .......................................... 195 3.1.19. Tablas (clase «JTable»). .......................................... 196 3.1.20. Control de incremento (clase «JSpinner»). .............. 196 3.1.21. Separadores (clase «JSeparator»). ........................... 197 3.1.22. Divisores (clase «JSplitPane»). ............................... 197 3.1.23. Fichas con pestañas (clase «TabbedPane»). ............ 197 3.1.24. Selección de archivos (clase «JFileChooser»). ........ 197 3.1.25. Selección de color (clase «JColorChooser»). .......... 198 3.1.26. Paneles desplazables (clase «JScrollPane»). ............ 198

3.2. Los gestores de distribución de Swing............................. 198 3.2.1. Diseño de caja (clase «BoxLayout»). ........................ 198

3.2.2. Diseño nulo (clase «OverlayLayout»)........................ 199 3.3. Cuadros de diálogo predefinidos. ..................................... 199 3.4. Enlace de propiedades de componentes. .......................... 199

3.4.1. Clases para enlazar. .................................................... 199 3.4.2. Enlace de propiedades del mismo tipo. ...................... 199 3.4.3. Enlace de propiedades de tipos distintos. ................... 200

3.5. Apariencia de las aplicaciones. ........................................ 201 3.6. Pintando en Swing. .......................................................... 201

4. JavaBeans ............................................................................. 201 4.1. ¿Qué es un bean? ............................................................. 201

4.1.1. Introducción. .............................................................. 201 4.1.2. Patrones de diseño de beans. ...................................... 202 4.1.3. Ejemplo de bean......................................................... 202

4.2. Gestión de las propiedades de un bean. ............................ 204 4.2.1. Propiedades de un bean. ............................................. 204 4.2.2. Propiedades simples. .................................................. 204 4.2.3. Propiedades indexadas. .............................................. 205 4.2.4. Propiedades compartidas. .......................................... 205 4.2.5. Clases e interfaces predefinidas para gestionar cambios

en propiedades. .................................................................... 207 4.2.6. Ejemplo de gestión de propiedades compartidas........ 209 4.2.7. Propiedades restringidas. ........................................... 210 4.2.8. Ejemplo de gestión de propiedades restringidas. ....... 211

4.3. Persistencia de un bean. ................................................... 212 4.4. La clase «BeanInfo». ....................................................... 213

VIII - APPLETS ...............................................................214 1. Introducción. ........................................................................ 214

1.1. ¿Cómo crear un applet Java?............................................ 214 1.2. Métodos de la clase «Applet». ......................................... 214 1.3. Paso de parámetros a los applets. ..................................... 215

2. Conceptos avanzados. .......................................................... 216 2.1. Hilos de ejecución y applets. ............................................ 216 2.2. Comunicación entre applets (interfaz «AppletContext»). 216 2.3. Ejemplo de applet animado. ............................................. 216

IX - COMUNICACIONES EN RED...............................219 1. Introducción. ........................................................................ 219

1.1. La clase «InetAddress». ................................................... 219 1.2. Trabajando con URL’s. .................................................... 219

1.2.1. Direcciones de red. .................................................... 219 1.2.2. La clase «URL». ........................................................ 220 1.2.3. La clase «URLEncoder». ........................................... 221 1.2.4. La clase «URLConnection». ...................................... 221

2. Trabajando con Sockets. ..................................................... 222 2.1. Fundamentos sobre sockets. ............................................. 222

2.1.1. Tipos de sokets. ......................................................... 222 2.1.2. Funcionamiento genérico de los sokets. ..................... 222

2.2. Java Sockets. .................................................................... 223 2.2.1. Modelo de comunicaciones con Java. ........................ 223 2.2.2. Apertura de Sockets. .................................................. 223 2.2.3. Creación de Streams. ................................................. 224 2.2.4. Cierre de Sockets. ...................................................... 225 2.2.5. Ejemplo de aplicación Cliente/Servidor mediante

sockets. ................................................................................ 225 3. Trabajando con datagramas. .............................................. 227

3.1. Clientes Datagrama. ......................................................... 227 3.2. La clase «DatagramPacket». ............................................ 228 3.3. La clase «DatagramSocket». ............................................ 228 3.4. Ejemplo de programa de eco con datagramas. ................. 229

Page 7: Fundamentos de JAVA

Fundamentos de Java /7

I - LA TECNOLOGÍA JAVA

Java nació como un lenguaje para la red, para sostener el Hyper Text Markup Language (HTML), que no es un lenguaje de programación propiamente dicho, y para darle la seguridad que el HTML no tiene. Desde que apareció Java en la red se ha empezado a hablar de números de tarjetas de crédito y de informaciones seguras, lo cual ha entusiasmado a las mayores sociedades mundiales que han transformado la vieja Internet, prerrogativa de las universidades y centros de investigación, en el medio actual de comunicación abierto a todos.

1. Introducción a Java.

El lenguaje de programación Java se creó a mediados de los noventa, es el más reciente entre sus semejantes, y por eso se encuentra en fase de evolución, hasta el punto que todos los años sale una nueva versión. Creado como lenguaje exclusivamente para la red se ha convertido en un auténtico lenguaje de programación comparable, desde el punto de vista de la funcionalidad, a Visual Basic .NET, C++ o C#.

1.1. Esquema de compilación y ejecución de un programa java.

Java y la mayor parte de los otros lenguajes pueden compararse sólo desde el punto de vista funcional; y esto es porque son fundamentalmente diferentes, ya que Java compila los archivos fuentes de sus programas en un código llamado «Bytecode» independiente de la plataforma en la que se ha compilado, mientras que lenguajes como C++ compilan los fuentes de los programas en un código que es a su vez código del sistema operativo en el que se ha realizado. Por lo tanto, para poner en marcha un programa Java es necesario contar con un instrumento llamado Máquina Virtual de Java (JVM) que interpreta el bytcode generado por el compilador Java y lo ejecuta en el sistema en el que se ha instalado.

Gracias a la Máquina Virtual, Java es independiente de la plataforma; es decir, el programa compilado Java es ejecutado directamente por la JVM y no por el sistema operativo. Por eso es posible ejecutar el mismo programa Java, compilado una sola vez en un sistema cualquiera, en una plataforma Windows y en una plataforma Unix; sin embargo, para hacer eso se necesita que Windows y Unix instalen una Máquina Virtual que soporte la versión de Java con la que fue compilado el programa. Las dos JVM, instaladas en las dos plataformas distintas, son el mismo programa compilado una vez por Windows y otra vez por Unix, como ocurría con los programas escritos en lenguajes como el C/C++. Los navegadores de internet más populares (como FireFox e Internet Explorer) tienen también instalada un Máquina Virtual para poder ejecutar los programas Java que se encuentran en la red (los applets). Debido a que Java sigue evolucionando, provoca obvios problemas de compatibilidad; siempre ocurre que el navegador más moderno contiene una versión precedente de Java con respecto a la última versión de Sun Microsystem. Además, hay que tener en cuenta que no todos los usuarios de Internet navegan usando la última versión de FireFox o de Explorer. Por eso, cuando queremos crear un applet e insertarlo en un documento HTML, hay que tener en cuenta estos problemas e intentar diseñar un programa que sea compatible con la mayor parte de las JVM implementadas en los distintos navegadores.

HARDWARE (microprocesador y circuitería)

SISTEMA OPERATIVO (Windows, Unix, MacOS, … )

PROGRAMA JECUTABLE MÁQUINA VIRTUAL DE JAVA

PROGRAMA JAVA

código máquina

seudocódigo seudocódigo

bytecodes

Page 8: Fundamentos de JAVA

Fundamentos de Java /8

1.2. Tecnologías Java.

La tecnología Java está organizada en las siguientes áreas: - J2SE (Core/Desktop). La plataforma «Java 2 Edición Estándar» provee un entorno para el núcleo de Java y el desarrollo de aplicaciones de escritorio, y es la base para las tecnologías Java 2, Enterprise Edition (J2EE) y Java Web Services. Incluye un compilador, herramientas, intérpretes y Java API’s que permiten escribir, probar, desarrollar y ejecutar applets y aplicaciones. La plataforma Java 2 provee un entorno completo para el desarrollo de aplicaciones para computadoras de escritorio y servidores. - J2EE (Enterprise/Server). La plataforma «Java 2 Edición Empresarial» define el estándar para desarrollar componentes basados en aplicaciones empresariales multicapa. Se basa en J2SE y provee servicios adicionales, herramientas y API’s para soportar el desarrollo simplificado de las aplicaciones empresariales. - J2ME (Mobile/Wireless). La plataforma «Java 2 Edición Micro» es un conjunto de tecnologías y especificaciones destinadas al consumidor y dispositivos como teléfonos móviles, asistentes digitales personales (PDA’s - personal digital assistants), impresoras y cajas de TV. - Java Card, adapta la plataforma de Java para permitir a tarjetas y otros dispositivos inteligentes, con capacidades limitadas de memoria y de procesamiento, beneficiarse de varias de las ventajas de la tecnología Java. - Java Web Services. Los servicios web están basados en aplicaciones web empresariales abiertas, estándares basados en XML y protocolos de transporte para intercambiar datos con otros clientes. La plataforma J2EE provee los API’s y herramientas necesarias para crear y desarrollar servicios web y clientes interoperables. - XML. El lenguaje de Marcación de Texto Extendido (XML - Extensible Markup Language) es una plataforma cruzada, extensible y basada en un estándar de texto para representar datos. También es una tecnología llave en el desarrollo de servicios web. - JDBC (Java Database Connectivity). Son algunas clases disponibles para conectarnos a bases de datos - JNI (Java Native Interface). Permite desde JAVA poder llamar a código nativo (que esté dentro de una DLL). Puede ser para C, C++ o incluso Visual Basic. - RMI (Invocación de Métodos Remotos). Es análogo a la tecnología RPC de Microsoft. Permite invocar un objeto y llamar a una de sus funciones de forma remota a través de una red. - EJB es un objeto que está dentro de un Servidor de Aplicaciones, por lo que la tecnología JAVA que permitirá llamar a sus métodos es RMI. - JNDI (Java Naming Directory Interface). Servicios de nombrado usado con los EJB’s y otros componentes para localizar los objetos. Actúa como un Servicio de Directorio para saber dónde están los objetos (especie de páginas amarillas).

Otras tecnologías relacionadas son: - Apache Tomcat. Es la implementación de referencia oficial para las especificaciones Servlet 2.2 y JSP 1.1. Puede ser usado como un pequeño servidor para probar páginas JSP y servlets, o puede integrarse en el servidor Web Apache.

public class MiAplicacion {

……

public static void

main(String [] ags) {

……

}

}

public class Ventana {

……

}

…… ……

(Bytecodes) …… ……

…… (Bytecodes)

……

MiAplicacion.java

Ventana.java

MiAplicacion.class

Ventana.class

MiAplicacion.jar

SE COMPILAN

CON javac

SE EJECUTA

CON java

archivos fuentes archivos compilados programa ejecutándose

Page 9: Fundamentos de JAVA

Fundamentos de Java /9

- Glassfish. Es un servidor de aplicaciones desarrollado por Sun Microsystems que implementa las tecnologías definidas en la plataforma Java EE y permite ejecutar aplicaciones que siguen esta especificación. En especial se utiliza para implementar los Enterprise Java Beans (EJB).

1.3. Kits de desarrollo de Java.

Existen distintos programas comerciales que permiten desarrollar código Java. La compañía Sun, creadora de Java, distribuye gratuitamente el Java Development Kit (JDK). Se trata de un conjunto de programas y librerías que permiten desarrollar, compilar y ejecutar programas en Java. Existe también una versión reducida del JDK, denominada JRE (Java Runtime Environment) destinada únicamente a ejecutar código Java. El kit de desarrollo proporciona:

- un compilador: javac - un intérprete: java - un generador de documentación: javadoc - otras herramientas complementarias...

Los IDE’s (Integrated Development Environment) son entornos de desarrollo integrados. En un mismo programa es posible escribir el código Java, compilarlo y ejecutarlo sin tener que cambiar de aplicación. La compañía Sun distribuye gratuitamente un IDE denominado NetBeans.

2. Programas de referencia del JDK.

2.1. Compilador de programas Java: «javac»

Sintaxis y descripción: javac [opciones] archivo.java

El programa javac compila los archivos con código fuente Java (.java) en bytecodes Java (.class). Por defecto, javac genera los archivos .class en el mismo directorio del archivo fuente .java, excepto en el caso de utilizar la opción –d, que permite especificar otro directorio. Cuando un archivo fuente Java hace referencia a una clase que no está definida en alguno de los archivos fuente de la línea de comandos, entonces javac hace uso de la variable de entorno CLASSPATH; por defecto, esta variable de entorno apunta a las clases del sistema. Opciones: Se puede utilizar un argumento que comience por el carácter arroba, @, para indicar que las opciones se encuentran en un fichero, cada argumento en una línea. Estos argumento serán insertados automáticamente en la línea de comandos en la posición en que se haya indicado en el argumento @fichero.

-classpath path Indica a javac dónde tiene que ir a buscar las clases especificadas en el código fuente. Esta opción elimina la ruta por defecto y cualquier otra ruta especificada mediante CLASSPATH. La ruta especificada puede ser una lista de directorios separados por (;) para sistemas Windows y (.) para sistemas Unix. Por ejemplo: javac -classpath .;c:\uso\afq\classes;c:\utiles\java\clases

o en Unix javac [opciones] archivo.java

javac_g [opciones] archivo.java

-d directorio Especifica el lugar dónde se guardan las clases; por defecto se guardan en el mismo directorio que los ficheros fuentes .java. Si no se especifica esta opción, los ficheros fuente deben situarse en directorios que sigan la estructura del paquete a que pertenecen, para que los ficheros .class resultantes de su compilación sean fácilmente localizables. Los parámetros -d y -classpath son independientes. El compilador lee de la ruta donde están las clases y las guarda en el directorio de destino. Es pues muy útil que el directorio destino de las clases se encuentre en el camino de búsqueda que indica la opción classpath. javac -d c:\uso\afq\classes MiPrograma.java

-depend

Hace que el compilador considere la recompilación de ficheros .class referenciados en otros ficheros de clases. Normalmente, sólo recompila los ficheros .class que no encuentra o que están obsoletos, referenciados desde el código fuente.

-deprecation Genera un aviso por cada una de las clases o métodos que se hayan quedado obsoletas. Un método de una clase se dice que ha quedado obsoleto si en la documentación se le ha colocado la opción @deprecated. El compilador generará un aviso de métodos obsoletos aunque no se especifique esta opción.

Page 10: Fundamentos de JAVA

Fundamentos de Java /10

-g Esta opción permite a javac incorporar números de línea e información de variables locales a los archivos de clases, para ser utilizada por las herramientas de depuración. Por defecto, solamente se generan los números de línea, excepto si la optimización (-O) está activada.

-g:nodebug No genera números de línea ni información de variables locales. -nowarn Hace que javac no muestre los mensajes de aviso en la compilación. -O Optimiza el código compilado. Las clases pueden tener un tamaño muy grande. -verbose Hace que el compilador y el enlazador muestren mensajes de los ficheros fuente que están

siendo compilados y de las clases que se están cargando. -JopcionJava Se puede pasar en opcionJava cualquier argumento al intérprete Java. El argumento no debe

contener espacios. Se utiliza para ajustar el entorno de ejecución del compilador o la utilización de memoria.

2.2. Intérprete de bytecodes Java: «java»

Sintaxis y descripción: java [opciones] nombre_de_clase <argumentos>

java [opciones] -jar fichero_jar <argumentos>

javaw [opciones] nombre_de_clase <argumentos>

javaw [opciones] -jar fichero_jar <argumentos>

El comando java es un interfaz simple a base de línea de comandos para acceder a la Máquina Virtual Java. Es el intérprete de Java que ejecuta bytecodes creados por javac. El argumento nombre_de_clase es el nombre de la clase a ser ejecutada y argumentos son los parámetros pasados a la clase. Todos los nombres de ficheros de clases Java deben terminar con la extensión .class, que el compilador javac añade automáticamente, y nombre_de_ clase debe contener un método main(), definido como: class nombre_de_clase {

public static void main( String args[] ) {

. . .

}

}

El intérprete java ejecuta el método main() y finaliza, a no ser que main() cree uno o más hilos de ejecución, en cuyo caso, java no finaliza hasta que haya terminado el último hilo. Se puede utilizar también java para compilar y ejecutar programas con la opción -cs. Como se ha de cargar cada fichero que contenga el bytecode de la clase, su fecha de modificación se compara con la fecha de modificación del fichero que contiene el código fuente de la clase; si ésta última es más reciente, se recompila la clase y se carga el nuevo fichero .class generado. El intérprete java repite el proceso hasta que todas las clases están compiladas y cargadas correctamente. El intérprete java también soporta la ejecución de programas Java completos encapsulados en archivos de tipo JAR. El intérprete puede determinar la legitimación de una clase a través de un mecanismo interno de verificación. Este mecanismo asegura, antes de la ejecución, que los ficheros de clases no violan ninguna de las reglas del lenguaje. El intérprete javaw es una versión especial para Windows que crea una ventana separada que sirve de consola para la salida estándar. Opciones: El intérprete Java tiene un conjunto de opciones estándar que están soportadas por la Máquina Virtual Java actual y que también serán soportadas por futuras versiones de la Máquina Virtual. Además, tiene otra serie de opciones que no son estándar y que aunque estén soportadas ahora pueden no estarlo en el futuro; estas opciones comienzan por -X, y en la documentación original de Sun se puede encontrar una guía completa de ellas.

-Xdebug Permite que el depurador de código Java se conecte a sí mismo a la sesión de java. Cuando se utiliza esta opción, java muestra una contraseña, la cual ha de ser introducida cuando comienza la sesión de depuración.

-classpath path Especifica el camino que java usa para buscar las clases. Sobrescribe el establecido por defecto o la variable de entorno CLASSPATH si ésta ha sido establecida anteriormente.

-help Imprime un mensaje indicando la forma de la llamada.

Page 11: Fundamentos de JAVA

Fundamentos de Java /11

-jar fichero_jar Ejecuta un programa Java encapsulado en un fichero JAR. En lugar de referenciarlo en la línea de comandos, java coge la clase inicial que el fichero jar indica en la cabecera Main-Class. Por ejemplo, si el punto de entrada al programa es com.miEmpresa.miPaquete.MiClase.main(), lo que aparecería en esta entrada sería: Main-Class:

com.miEmpresa.miPaquete.MiClase -Xmx x Establece el tamaño máximo de la memoria del recolector de basura (garbage collector)

a x. El valor por defecto es 16 megabytes de memoria, y x debe ser mayor o igual a 1000 bytes. Por defecto, x se toma en bytes, pero se puede especificar en kilobytes o megabytes poniendo después de x la letra "k" para kilobytes y "m" para megabytes.

-Xms x Establece el tamaño de inicio de la memoria del recolector de basura (garbage collector) a x. El valor por defecto es 1 megabyte, y x debe ser mayor que 1000 bytes. Por defecto, x se toma en bytes, pero se puede especificar en kilobytes o megabytes poniendo después de x la letra "k" para kilobytes y "m" para megabytes.

-Xnoasyncgc Desactiva el recolector asíncrono de basura. Cuando el recolector está activado no actúa a menos que éste sea explícitamente llamado o el programa corra fuera de memoria.

-Xnoclassgc Desactiva el recolector de basura de clases Java. Por defecto, el intérprete Java reclama espacio para clases Java, aunque no se usen, durante la recolección de basura.

-X Imprime ayuda sobre las opciones no estándar. -v (ó –verbose) Hace que java imprima un mensaje en la salida estándar cada vez que se carga el archivo

de una clase. -Xverify Ejecuta el verificador de todo el código. -Xverifyremote Ejecuta el verificador de todo el código que es cargado en el sistema a través de un

cargador de clases. verifyremote es el valor por defecto para el intérprete. -verbosejni Imprime mensajes relacionados con JNI, incluyendo información sobre los métodos

nativos que han sido enlazados y avisos sobre creación de excesivas referencias locales. -verbosegc Hace que el recolector de basura imprima mensajes cada vez que libera memoria. -version Imprime información sobre la versión. -Dpropiedad=valor Redefine el valor de una propiedad. propiedad es el nombre de la propiedad cuyo valor se

quiere cambiar y valor es valor a ser asignado. Por ejemplo, la línea siguiente %java -Dawt.button.color=green ...

establece el valor de la propiedad awt.button.color a "green" (verde). java acepta cualquier número de opciones -D en la línea de comandos.

2.3. Intérprete «jre».

Sintaxis y descripción: jre [opciones] nombre_de_clase <argumentos>

jrew [opciones] nombre_de_clase <argumentos>

Es el intérprete Java Runtime que se puede utilizar para ejecutar aplicaciones Java. El jre es una herramienta similar a java, pero está especialmente diseñada para usuarios finales, que no necesitan todas las opciones de desarrollo que están disponibles en el intérprete java. El comando jrew es idéntico a jre, excepto que con jrew no hay asociada ninguna ventana de consola (se utiliza cuando no se quiere que aparezca una ventana para entrada de órdenes). Opciones:

-classpath path Especifica el camino que jre usa para buscar las clases. Sobrescribe el establecido por defecto o la variable de entorno CLASSPATH si ésta ha sido establecida anteriormente.

-help Imprime un mensaje indicando la forma de la llamada. -mx x Establece el tamaño máximo de la memoria del recolector de basura (garbage collector)

a x. -ms x Establece el tamaño de inicio de la memoria del recolector de basura (garbage collector)

a x. -noasyncgc Desactiva el recolector asíncrono de basura. Cuando el recolector está activado no actúa

a menos que éste sea explícitamente llamado o el programa corra fuera de memoria. -noclassgc Desactiva el recolector de basura de clases Java. Por defecto, el intérprete Java reclama

espacio para clases Java, aunque no se usen, durante la recolección de basura.

Page 12: Fundamentos de JAVA

Fundamentos de Java /12

-nojit Indica que deben ignorar los compiladores en tiempo de ejecución. Se invoca al intérprete Java de defecto.

-v (ó –verbose) Hace que jre imprima un mensaje en la salida estándar cada vez que se carga el archivo de una clase.

-verify Ejecuta el verificador de todo el código. -verifyremote Ejecuta el verificador de todo el código que es cargado en el sistema a través de un

cargador de clases. verifyremote es el valor por defecto para el intérprete. -verbosegc Hace que el recolector de basura imprima mensajes cada vez que libera memoria. -version Imprime información sobre la versión. -Dpropiedad=valor Redefine el valor de una propiedad.

2.4. Depurador «jdb».

Sintaxis y descripción: jdb [opciones]

El depurador para clases jdb ayuda a encontrar y corregir errores en programas Java. Está basado en una línea de comandos de texto y tiene una sintaxis de comandos como la del dbx en UNIX o depuradores gdb. Opciones:

-host <hostname> Indica el nombre de la máquina en la que se está ejecutando el intérprete Java al que nos queremos conectar.

-password <clave> Entra en la sesión activa del intérprete. Esta contraseña es la que imprime el intérprete Java cuando se invoca con la opción -debug.

2.5. Analizador «javah».

Sintaxis y descripción: javah [opciones] nombre_de_clase. . .

Produce ficheros fuente C y ficheros de cabecera C a partir de una clase Java. Estos archivos de C proporcionan la información necesaria para implementar métodos nativos para las clases especificadas en C. El nuevo método de interfaz para código nativo, Java Native Interface (JNI), no necesita información de cabeceras o ficheros stub. javah puede ser invocado con la opción -jni para generar los prototipos de funciones que necesitan los métodos nativos JNI. El resultado se coloca en un fichero .h. Opciones:

-classpath path Indica a javah dónde tiene que ir a buscar las clases especificadas en la línea de comandos.

-d directorio Especifica un directorio en donde javah almacenará los archivos que genera. Por defecto los guarda en el directorio actual.

-jni Hace que javah genere un fichero conteniendo los prototipos de las funciones de los métodos nativos al estilo JNI.

-o fichero_salida Combina todos los archivos .h o .c en un solo archivo, fichero_salida. -stubs Genera ficheros fuente C para la clase o clases, y no genera archivos de cabecera. -td directorio El directorio donde javah almacena los archivos temporales. Por defecto utiliza el

directorio especificado en la variable de entorno %TEMP%. Si esa variable no existe, utiliza el directorio especificado en la variable de entorno %TMP%. Si tampoco existe, javah crea el directorio c:\tmp y almacena allí los archivos temporales.

-trace Añade información de depuración al fichero de stubs. -v Hace que javah imprima mensajes acerca del trabajo que está realizando.

2.6. Desensamblador «javap».

Sintaxis y descripción: javap [opciones] nombre_de_clase. . .

Desensambla el archivo de clases especificado por los nombres de clases en la línea de comandos e imprime una versión medianamente comprensible de esas clases. Opciones:

-b Asegura compatibilidad hacia atrás con javap del JDK 1.1.

Page 13: Fundamentos de JAVA

Fundamentos de Java /13

-c Imprime el código desensamblado. Es decir, todas las instrucciones contenidas en los bytecodes, para cada uno de los métodos que constituyen la clase.

-classpath path Especifica el camino que javap usa para encontrar las clases nombradas en la línea de comandos.

-h Genera código que puede ser incluido en un fichero de cabecera C. -J flag Pasa el flag directamente al sistema de ejecución. -l Imprime números de línea y tablas de variables locales. -package Imprime paquetes y clases y miembros públicos y protegidos. Ésta es la opción de

defecto. -private Muestra todo tipo de clases y sus miembros. -protected Muestra solamente clases y miembros protegidos y públicos -public Muestra solamente clases y miembros públicos. -s Imprime signaturas internas. -verbose Proporciona la salida por pantalla del tamaño de la pila, del número de variables locales

y de los argumentos de los métodos. -version Imprime la cadena que indica la versión de javap.

2.7. Javadoc.

Sintaxis: javadoc [opciones] nombre_de_paquete | clase1.java clase2.java

2.7.1. Descripción. Genera documentación API en formato HTML para el paquete especificado o para los archivos fuentes individuales en Java en la línea de comandos. Javadoc genera un fichero .html por cada fichero .java y paquete que encuentra. También genera la jerarquía de clases (tree.html) y un índice con todos los miembros que ha detectado (AllNames.html). Como argumento a javadoc se le puede indicar una serie de paquetes o ficheros Java con los que se desea generar la documentación. 2.7.2. Doclets. Se puede configurar el contenido y el formato de salida que se va a generar con javadoc a través de doclets. Un doclet es un programa Java escrito utilizando el Java Doclet API, que especifica el contenido y formato de la salida que ha de generar javadoc. Se pueden escribir doclets para generar cualquier tipo de salida de texto, ya sea HTML, SGML, RTF o MIF. JavaSoft solamente proporciona el doclet estándar que genera una salida HTML semejante a la de los JDK anteriores. Sin embargo, a través de los doclets se pueden realizar tareas que nada tengan que ver con la documentación; por ejemplo, un doclet para diagnóstico podría comprobar que todas las clases miembro de las clases sobre las que está operando javadoc tienen los comentarios de documentación. Cuando no se indica ningún doclet específico en la línea de comandos, Javadoc utilizará el doclet estándar que genera una salida HTML semejante a la de las versiones anteriores del JDK, aunque incluso este doclet incorpora algunas opciones más que sus antecesores en el JDK. 2.7.3. Comentar el código fuente. Como javadoc comprueba automáticamente clases, interfaces, métodos y declaraciones de variables, se puede añadir documentación adicional a través de comentarios de documentación en el código fuente. Estos comentarios pueden incluir marcas HTML. Un comentario de documentación está formado por todos los caracteres incluidos entre /** que indica el comienzo del comentario y */ que indica el final. En todas las líneas intermedias los caracteres * a la izquierda se ignoran, y también todos los espacios y tabuladores que precedan a ese carácter *. /**

* Éste es un comentario de documentación

*/

2.7.4. HTML estándar. Se pueden incluir marcas HTML dentro de un comentario de documentación, aunque no deben utilizarse marcas de cabecera como <H1> y <H2>, o líneas horizontales <HR>, porque javadoc crea una estructura completa para el documento y estas marcas interfieren con el formato general de ese documento.

Page 14: Fundamentos de JAVA

Fundamentos de Java /14

2.7.5. Marcas de «javadoc». Javadoc reconoce marcas especiales cuando recorre los comentarios de documentación. Estas marcas permiten autogenerar una documentación completa y bien formateada del API a partir del código fuente. Las marcas comienzan siempre con el signo @. Estas marcas deben situarse al principio de la línea (debe tenerse en cuenta que todo lo que haya hasta el primer carácter * se ignora) y todas las marcas con el mismo nombre deben agruparse juntas dentro del comentario de documentación. 2.7.6. Marcas de documentación de clases e interfaces.

• @see nombre_de_clase Añade un enlace a la clase en la zona "See Also". Por ejemplo:

@see java.lang.String

@see String

@see String#equals

@see java.lang.Object#waint(int)

@see Character#MAX_RADIX

@see <A HREE="spec.html">Especif. Java</A>

El carácter # separa el nombre de una clase del nombre de uno de sus campos, métodos o constructores. Un comentario de documentación puede incluir más de una marca @see.

• @version texto-version Añade una entrada "Version". El texto no tiene que tener formato especial. Un comentario de documentación puede incluir más de una marca @version.

• @author texto-autor Añade una entrada "Author". El texto no tiene que tener formato especial. Un comentario de documentación puede incluir más de una marca @author.

• @since texto Este texto no tiene una estructura especial. Se utiliza para indicar desde qué fecha o desde qué versión se ha introducido el cambio o característica que indica el texto.

• @deprecated texto Añade un comentario indicando que no debería utilizarse la función o método, porque puede dejar de ser soportada por el API. La convención que se sigue es indicar en el texto la función o método por quien se ha sustituido. Por ejemplo:

@deprecated Reemplazado por setBounds(int,int,int,int)

Si el miembro está ya obsoleto y eliminado, el texto que sigue al @deprecated debe ser "No replacement". Ejemplo de comentario de una clase: /**

* Clase que presenta una ventana en la pantalla.

* Por ejemplo:

* <PRE>

* Window ventana = new Window( padre );

* Ventana.show();

* </PRE>

*

* @see awt.BaseWindow

* @see awt.Button

* @version 1.3 15 Ene 97

* @author Agustin Froufe

*/

class Window extends BaseWindow {

. . .

}

2.7.7. Marcas de documentación de campos. La única marca especial que se puede incluir es la marca @see. Ejemplo de comentario de un campo: /**

* Coordenada X de la ventana

* @see window#1

*/

Page 15: Fundamentos de JAVA

Fundamentos de Java /15

int x=1234567;

2.7.8. Marcas de documentación de constructores y métodos. Pueden ser marcas @see y además:

• @param parametro descripcion Añade un parámetro a la sección "Parameters". La descripción puede continuar en la línea siguiente.

• @return descripcion Añade una sección "Return", que debe contener la descripción del valor de retorno.

• @exception nombre_de_la_clase descripcion Añade una entrada "Throws", que contiene el nombre de la excepción que puede ser lanzada por el método. La excepción estará enlazada con su clase en la documentación.

• @see nombre_de_clase Añade un enlace a la clase en la zona "See Also".

• @since texto Indica desde qué fecha o desde qué versión se ha introducido el cambio o característica que indica el texto.

• @deprecated texto Indica que no debería utilizarse la función o método, porque puede dejar de ser soportada por el API.

Ejemplo de comentario de un método: /**

* Devuelve el carácter de la posición indicada entre

* <TT>0</TT> y <TT>length()-1</TT>

* @param indice La posición del carácter a obtener

* @return El carácter situado en la posición

* @exception StringIndexOutOfRangeException

* Se prodcue cuando el indice no está en

* el rango <TT>0</TT> a <TT>length()-1</TT>

*/

public char charAt( int indice ) {

. . .

}

2.7.9. Opciones. Se puede utilizar un argumento que comience por el carácter arroba, @, para indicar que las opciones se encuentran en un fichero, cada argumento en una línea. Estos argumento serán insertados automáticamente en la línea de comandos en la posición en que se haya indicado en el argumento @fichero. javadoc ahora utiliza doclets para determinar su salida. Si no se indica un doclet específico a través de la opción –doclet, javadoc utilizará el doclet estándar. javadoc proporciona un conjunto de opciones que se pueden utilizar con cualquier doclet (opciones de javadoc); y también proporciona opciones adicionales a utilizar con el doclet estándar (opciones con doclet estándar).

-doclet fichero Especifica el fichero que contiene la configuración del formato de salida que se desea generar. Si no se especifica, se generará una salida con formato Html estándar.

-sourcepath path Especifica el camino de búsqueda de los ficheros fuente .java. No afecta a la carga de los ficheros de clases .class.

-classpath path No es conveniente utilizar esta opción, porque no es necesaria habitualmente; es mejor utilizar -sourcepath para indicar donde se encuentran los ficheros .java a documentar.

-encoding nombre Especifica el tipo de codificación del fichero fuente, como EUCJIS\SJIS. Si no se especifica, se usa el codificador por defecto de la plataforma.

-Jflag Pasa flag directamente al sistema operativo. Por ejemplo, si es necesario que limitemos la memoria utilizada por el sistema a 32 megabytes para generar la documentación, podríamos utilizar este flag de la forma siguiente: javadoc -J-Xmx32m -J-Xms32m &ltclasees> …

-package Imprime paquetes y clases y miembros públicos y protegidos. -private Muestra todo tipo de clases y sus miembros. -protected Muestra solamente las clases públicas y protegidas y sus miembros. Es la opción de

defecto.

Page 16: Fundamentos de JAVA

Fundamentos de Java /16

-public Muestra solamente clases y miembros públicos. -verbose Sin esta opción aparecen mensajes de carga de ficheros, generación de documentación

(un mensaje por fichero) y ordenación. Con esta opción se muestran mensajes adicionales especificando el número de milisegundos empleado en generar la documentación de cada fichero Java.

2.7.10. Opciones con doclet estándar.

-author Incluye las marcas @author, que por defecto son omitidos. -d directorio Especifica un directorio en donde javadoc almacena los archivos HTML que genera. (La

"d" significa "destino"). El directorio puede ser absoluto o relativo al directorio de trabajo. Por defecto se utiliza el directorio actual.

-linkall Crea enlaces a la documentación del API para todas las clases que referencien algún documento del API que se está generando. Por ejemplo, si javadoc está corriendo sin esta opción sobre el fichero Graphics.java del paquete java.awt, la línea de comandos sería: javadoc C:\java\awt\Graphics.java

La signatura para el método Graphics.toString() en el html generado por javadoc tendría una apariencia como: <pre>

public String toString()

</pre>

-nodeprecated Excluye los párrafos que contengan el tag @deprecated. -noindex Omite el índice del paquete, que por defecto es generado. -notree Omite la jerarquía de clases/interfaz, que por defecto es generada. -breakindex Divide el índice en 26 ficheros, uno por cada letra. -footer texto Coloca el texto que se indique en cada una de las páginas que se generen,

inmediatamente antes del tag </body>. Si en texto contiene espacios, debe ir encerrado entre comillas simples o dobles.

-version Incluye las marcas @version, que por defecto son omitidas.

2.8. Appletviewer.

Sintaxis y descripción: appletviewer [opciones] urls. . .

Permite ejecutar applets fuera del entorno de un navegador WWW. El appletviewer descarga uno o más documentos HTML especificados por las urls en la línea de comando. Descarga todos los applets referenciados en cada documento y muestra éstos. Cada uno en su propia ventana. Si ninguno de los documentos mostrados tiene una marca <APPLET>, appletviewer no hace nada. Opciones:

-debug Inicia el depurador de Java jdb, permitiendo depurar el applet en el documento. -JopcionJava Se puede pasar en opcionJava cualquier argumento al intérprete Java. El argumento no

debe contener espacios. Se utiliza para ajustar el entorno de ejecución del compilador o la utilización de memoria.

2.9. Compresor «jar».

Sintaxis y descripción: jar [opciones] [manifest] destino fichero [fichero]

Es una aplicación Java que combina múltiples ficheros en un solo fichero JAR. Es una herramienta de propósito general basada en la compresión en formato ZIP. jar fue diseñada fundamentalmente para facilitar el empaquetamiento de applets Java o aplicaciones en un solo archivo. Cuando los componentes de un applet o aplicación (ficheros .class, de imágenes, de sonidos) se combinan en un solo fichero, pueden ser descargados en una sola transacción HTTP por el navegador, en vez de tener que realizar una nueva conexión cada vez que se deba enviar o recoger cada uno los ficheros individuales. Esto reduce el tiempo de descarga del applet, y como además los ficheros están comprimidos, se contribuye a reducir más todavía el tiempo en que se realiza la descarga. Además, se permite la incorporación de marcas adicionales en el fichero para que pueda ser identificado el autor del applet y se pueda autentificar origen del applet. La sintaxis de esta herramienta es muy semejante a la sintaxis del comando tar que se utiliza en sistemas Unix.

Page 17: Fundamentos de JAVA

Fundamentos de Java /17

Los tres tipos de ficheros que se indican en la invocación de la herramienta jar son: - fichero de manifiesto (opcional) - fichero jar destino - ficheros que queremos archivar

Una llamada típica sería: jar cf mificherojar *.class

En este ejemplo, todos los ficheros de clases Java en el directorio en que nos encontremos se agruparán en el fichero mificherojar. Un fichero de manifiesto será generado automáticamente por jar y siempre será la primera entrada en el fichero jar. Por defecto, el fichero se llama META-INF/MANIFEST.INF. El fichero de manifiesto es el lugar donde se almacenará cualquier información adicional referente al archivo. Si hay un fichero de manifiesto ya existente que queremos que sea utilizado por jar para el nuevo archivo a crear, se puede indicar a través de la opción -m: jar cmf miFicheroManifest miFicheroJar *.class

Opciones:

c Crea un archivo nuevo o vacío sobre la salida estándar (stdout). t Presenta la tabla de contenido desde la salida estándar. x fichero Extrae todos los ficheros, o los ficheros que se indiquen, desde la entrada estándar

(stdin). Si fichero se omite, entonces se extraen todos los ficheros; en otro caso se extrae solamente el fichero, o ficheros, especificados.

f El segundo argumento especifica un fichero jar a procesar. En el caso de creación, esto se refiere al nombre del fichero jar que se va a crear (en lugar de la salida estándar). Para tabla y extraer, el segundo argumento identifica el fichero jar que va a ser listado o extraído.

m Incluye información desde el fichero de manifiesto, que debe existir. Por ejemplo: % jar cmf miFicheroManifest miFicheroJar *.class

M No crea un fichero de manifiesto para las entradas. o Almacena solamente, no utiliza compresión alguna. v Presenta mensajes sobre la salida de errores estándar (stderr). Para tabla, por ejemplo,

indicará más información sobre los ficheros, como su tamaño y la fecha de última modificación.

Si cualquiera de los ficheros es un directorio, entonces se procesará el contenido de ese directorio. Ejemplos: Para añadir ficheros en un directorio determinado a un archivo jar cvf fichero.jar *

Si se tienen directorios separados para las imágenes, los ficheros de sonido y las clases Java, se puede componer un solo fichero jar con todo el contenido de esos directorios jar cvf fichero.jar imagenes clases sonido

Page 18: Fundamentos de JAVA

Fundamentos de Java /18

II - PROGRAMACIÓN CON JAVA

Java es un lenguaje de programación orientado a objetos desarrollado por Sun Microsystems a principios de los años 90. La sintaxis de Java se deriva en gran medida de C++; pero a diferencia de éste, que combina la sintaxis para programación genérica, estructurada y orientada a objetos, Java fue construido desde el principio para ser completamente orientado a objetos. Todo en Java es un objeto (salvo algunas excepciones), y todo en Java reside en alguna clase.

1. Estructura de un programa java

Cada clase de Java normalmente se escribirá dentro de un archivo fuente con extensión ".java" siguiendo una estructura específica. Un programa estará formado normalmente por una o varias clases. Cada programa debe contener una clase principal que proporcione el punto de entrada a la ejecución de código. La clase principal debe contener un método especial declarado con la siguiente sintaxis: public static void main ( String args[] )

La primera instrucción incluida en el bloque de código de este método será la primera que se ejecute cuando se lance la aplicación.

1.1. Estructura básica de un programa Java.

La estructura del archivo que contenga la clase principal de un programa de Java es la siguiente:

/**

* Comentarios de documentación

*/

// Declaración del nombre de paquete al que pertenece el programa

package nombreDePaquete;

// Declaración de librerías externas que utilizará el programa

import java.util.*;

// Declaración de la clase que representa al programa

class NombrePrograma {

/*

* Declaración de atributos y métodos del programa

*/

// Declaración del método principal del programa

public static void main ( String args[] ) {

/* ...

instrucciones de código que inician el programa

... */

}

}

El código fuente de los programas se guarda en un archivo de texto con el mismo nombre que el programa y con extensión ".java" y se ubica en una carpeta con el mismo nombre que el paquete. El archivo fuente debe compilarse de forma que genere un archivo de clase con extensión ".class": javac nombrePrograma.java

El comando precedente genera por defecto el archivo "nombrePrograma.class". El archivo de clase se ejecuta en la Máquina Virtual Java con la instrucción: java nombrePrograma.class

La variable de entorno CLASSPATH debe contener las rutas donde se encuentran las clases que se van a utilizar. Esta variable se suele declarar en 2 sitios:

- Como variable de entorno del S.O. (tanto en Windows como Unix) CLASSPATH = %CLASSPATH% ; ruta1 ; ruta2 ....

CLASSPATH = $CLASSPATH : ruta1 : ruta2 ...

- En el propio comando que ejecuta la maquina virtual, con la cláusula –classpath java.exe –classpath “C:\java”

Page 19: Fundamentos de JAVA

Fundamentos de Java /19

1.2. Paquetes.

1.2.1. Introducción. Los paquetes son un conjunto de clases e interfaces relacionadas. En Java los paquetes permiten identificar clases diferentes aunque tengan el mismo nombre; de esta forma una clase se identifica mediante su nombre y la ruta de paquetes en la que se ubica. Además, proporcionan un método de control de acceso. Los paquetes también proporcionan una forma de “ocultar” clases, evitando que otros programas o paquetes accedan a clases que son de uso exclusivo de una aplicación determinada. El paquete al que pertenece una clase se debe declarar dentro del archivo fuente en una línea de código utilizando la palabra package seguida del nombre de paquete. Esta línea debe estar al comienzo del fichero fuente; en concreto, debe ser la primera sentencia ejecutable del código Java, excluyendo, claro está, los comentarios y espacios en blanco. Java también soporta el concepto de jerarquía de paquetes. Esto es parecido a la jerarquía de directorios de la mayoría de los sistemas operativos. Se consigue especificando múltiples nombres en la sentencia package, separados por puntos. Por ejemplo, en las sentencias siguientes la clase Ballena pertenece al paquete mamiferos que cae dentro de la jerarquía del paquete animales. package animales.mamiferos;

class Ballena {

// . . .

}

La analogía con la jerarquía de directorios se ve reforzada por el intérprete Java, ya que éste requiere que los ficheros .class se encuentren físicamente localizados en subdirectorios que coincidan con los nombres del paquete. En el ejemplo anterior, si se encontrase en una máquina Unix, la clase Ballena debería estar situada en la ruta siguiente: animales/mamiferos/Ballena.class

1.2.2. Acceso a otros paquetes («import»). Normalmente, cuando desde una clase de un paquete se utilizan clases de otros paquetes deben referenciarse las clases de esos otros paquetes. Se debe emplear la palabra clave import si se van a colocar múltiples referencias a un mismo paquete, o si el nombre del paquete es muy largo o complicado. import nombre_paquete.nombre_clase;

Esta sentencia, o grupo de ellas, deben aparecer antes de cualquier declaración de clase en el código fuente. Por ejemplo: import animales.mamiferos.Ballena;

Esta instrucción permite referenciar la clase Ballena ubicada en el paquete animales.mamiferos. En la sentencia import también se admite la presencia del caracter *, asterisco. Cuando se emplea se indica que toda la jerarquía de clases localizada a partir del punto en que se encuentre debe ser referenciada, en lugar de indicar solamente una determinada clase: import animales.mamiferos.*;

Esta instrucción permite referenciar todas las clases del paquete animales.mamiferos. 1.2.3. Importaciones estáticas («import static»). Java dispone de clases creadas simplemente para contener miembros (variables y métodos) estáticos. Para usar este tipo de miembros es necesario anteceder la invocación al mismo con el nombre de la clase. Un ejemplo típico es la clase java.util.Math, creada como un contenedor de constantes y funciones matemáticas. En la clase Math se define una constante estática con el valor de la constante π. Para acceder a ella, normalmente se usará la siguiente sintaxis: import java.util.Math;

class Test {

public static main (String [] args ) {

double valorDePi = Math.PI; // se guarda el valor de pi en una variable local.

. . .

}

}

Desde Java 5 es posible simplificar esta sintaxis con las importaciones estáticas, que permiten el acceso a miembros estáticos extendiendo el tipo. En el siguiente ejemplo, todos los campos estáticos, métodos, etc. de la clase Math estarán disponibles para nuestra clase Test usando import static: class Test { import static java.util.Math;

Page 20: Fundamentos de JAVA

Fundamentos de Java /20

public static main (String [] args ) {

double valorDePi = PI; // se guarda el valor de pi en una variable local.

. . .

}

}

También se puede restringir la importación sólo a un miembro estático concreto: import static java.util.Math.PI;

Por regla general las importaciones estáticas se deberían usar: • Con moderación, ya que su empleo excesivo hace los programas inelegibles. • Sólo usarlo cuando se tienda a abusar de la herencia.

1.2.4. Nomenclatura de paquetes. Los paquetes pueden nombrarse de cualquier forma que siga el esquema de nomenclatura de Java. Por convenio, no obstante, los nombres de paquetes comienzan por una letra minúscula para hacer más sencillo el reconocimiento de paquetes y clases, cuando se tiene una referencia explícita a una clase. Esto es porque los nombres de las clases, también por convenio, empiezan con una letra mayúscula. Por ejemplo, cuando se usa el convenio citado, es obvio que tanto animales como mamiferos son paquetes y que Ballena es una clase. Cualquier cosa que siga al nombre de la clase es un miembro de esa clase: animales.mamiferos.Ballena.obtenerNombre();

Cada nombre de paquete ha de ser único, para que el uso de paquetes sea realmente efectivo. Los conflictos de nombres pueden causar problemas a la hora de la ejecución en caso de duplicidad, ya que los ficheros de clases podrían saltar de uno a otro directorio. En caso de proyectos pequeños no es difícil mantener una unicidad de nombres; pero en caso de grandes proyectos, o se sigue una norma desde el comienzo del proyecto, o éste se convertirá en un auténtico caos. Como norma y resumen de todo lo dicho, a la hora de crear un paquete hay que tener presente una serie de convenios:

- La palabra clave package debe ser la primera sentencia que aparezca en el fichero fuente, exceptuando los espacios en blanco y comentarios - Es aconsejable que todas las clases que vayan a ser incluidas en el paquete se encuentren en el mismo directorio. - En un fichero únicamente puede existir, como máximo, una clase con el especificador de acceso public, debiendo coincidir el nombre del fichero fuente con el nombre de la clase.

1.2.5. Ejecución de clases identificadas mediante una ruta de paquetes. Como se ha comentado, la ruta de paquetes de una clase se corresponde con la ruta de subcarpetas donde se encuentra el archivo compilado de la clase. Sin embargo, para el ejecutor java esta ruta de subcarpetas son interpretadas como parte de nombre del archivo. Por ejemplo, supongamos la clase Test está creada con el siguiente código: package java.com.www;

public class Test {

public static void main(String [] args) {

exit(0);

}

}

Y supongamos, que respecto a la carpeta raíz del sistema de archivos la ruta donde se ubica el archivo es la siguiente: /home/java/com/www/Test.class Para ejecutar esta clase deberemos usar el comando siguiente: java /home/java.com.www.Test

Es decir, las subclases que forman la ruta de paquetes de la clase Test no deben ser pasadas al comando java como tales sino como una ruta de paquetes. Si estamos ejecutando la línea de comando dentro de la carpeta /home, el comando sería: java java.com.www.Test

y nunca: java java/com/www/Test

Page 21: Fundamentos de JAVA

Fundamentos de Java /21

1.2.6. Variable de entorno «CLASSPATH». El intérprete Java debe encontrar todas las clases referenciadas cuando se ejecuta una aplicación Java. Por defecto, Java busca en el árbol de instalación del Java esas librerías. En el Tutorial de Java de Sun se indica que “los ficheros .class del paquete java.util están en un directorio llamado util de un directorio java, situado en algún lugar apuntado por CLASSPATH”. CLASSPATH es una variable de entorno que indica al sistema dónde debe buscar los ficheros .class que necesite. Sin embargo, lo que dice el Tutorial de Java de Sun normalmente no es así, lo cual puede ocasionar confusión. Cuando se utiliza el JDK, no existe el directorio que se indica. La no existencia se debe a que Java tiene la capacidad de buscar ficheros comprimidos que utilicen la tecnología Zip. Esto redunda en un gran ahorro de espacio en disco y además mantiene la estructura de directorios en el fichero comprimido. Por tanto, se podría parafrasear lo indicado por Sun escribiendo que “en algún lugar del disco, se encontrará un fichero comprimido (zip) que contiene una gran cantidad de ficheros .class”. De hecho, las clases estándar de Java se encuentran comprimidas en un archivo denominado "src.zip" ubicado en la carpeta de instalación del JDK. CLASSPATH contiene la lista de directorios en los que se debe buscar los árboles jerárquicos de librerías de clases. La sintaxis de esta variable de entorno varía dependiendo del sistema operativo que se esté utilizando; en sistemas Unix, contiene una lista de directorios separados por : (dos puntos), mientras que en sistemas Windows, la lista de directorios está separada por ; (punto y coma). La sentencia siguiente muestra un ejemplo de esta variable en un sistema Unix: CLASSPATH=/home/users/afq/java/classes:/opt/apps/Java

indicando al intérprete Java que busque en los directorios /home/users/afq/java/classes y /opt/apps/Java las librerías de clases. Cuando se ejecuta un programa de Java podemos especificar en el comando java rutas para buscar las librerías referenciadas. Esto se realiza con la opción -classpath. Por ejemplo, para ejecutar el programa Test, que referencia una librería llamada MiLib.jar y que está ubicada dentro de la carpeta /home/com/lib escribiríamos: java -classpath /home/com/lib Test

1.2.7. Paquetes estándar de Java. El lenguaje Java proporciona una serie de paquetes que incluyen ventanas, utilidades, herramientas, un sistema de entrada/salida general, y comunicaciones. Algunos de los paquetes Java más importantes son:

• java.awt. El paquete Abstract Windowing Toolkit (awt) contiene clases para generar ventanas y componentes GUI (Interfaz Gráfico de Usuario) de manipulación de imágenes, impresión, fuentes de caracteres, cursores, etc. • java.io. El paquete de entrada/salida contiene las clases de acceso a ficheros, de filtrado de información, serialización de objetos, etc. • java.applet. Este paquete contiene clases diseñadas para usar con applets. • java.lang. Este paquete incluye las clases del lenguaje Java propiamente dicho: Object, Thread, Exception, System, Integer, Float, Math, String, Package, Process, Runtime, etc. • java.net. Este paquete da soporte a las conexiones del protocolo TCP/IP y, además, incluye las clases Socket, URL y URLConnection. • java.sql. Este paquete incluye todos los interfaces que dan acceso a Bases de Datos a través de JDBC (Java DataBase Connectivity). • java.util. Este paquete es una miscelánea de clases útiles para muchas cosas en programación: estructuras de datos, fechas, horas, internacionalización, etc. Se incluyen, entre otras, Date (fecha y hora), Dictionary (diccionario), List (lista), Map (mapa), Random (números aleatorios) y Stack (pila FIFO). Dentro de este paquete, hay tres sub-paquetes muy interesantes: java.util.jar, que proporciona clases para leer y crear ficheros JAR; java.util.mime, que proporciona clases para manipular tipos MIME, Multipurpose Internet Mail Extension (RFC 2045, RFC 2046) y java.util.zip, que proporciona clases para comprimir, descomprimir, calcular checksums de datos, etc. con los formatos estándar ZIP y GZIP.

2. Sintaxis básica

2.1. Identificadores.

Los identificadores nombran variables, funciones, clases y objetos; cualquier cosa que el programador necesite identificar o usar.

Page 22: Fundamentos de JAVA

Fundamentos de Java /22

En Java un identificador comienza con una letra, un guión de subrayado (_) o un símbolo de dólar ($). Los siguientes caracteres del identificador pueden ser letras o dígitos. Se distinguen las mayúsculas de las minúsculas y no hay una longitud máxima establecida para el identificador. La forma básica de una declaración de variable, por lo general, sería: tipo identificador [ = valor ] [ , identificador [ = valor ] ... ] ;

Donde los corchetes indican partes opcionales. Son identificadores válidos: identificador

nombre_usuario

Nombre_Usuario

_variable_del_sistema

$transaccion

No son identificadores válidos: 2identificador

nombre usuario

Nombre-Usuario

2.2. Palabras clave y reservadas.

Las siguientes son las palabras clave que están definidas en Java y que no se pueden utilizar como identificadores:

abstract continue for new switch

boolean default goto null synchronized

break do if package this

byte double implements private threadsafe

byvalue else import protected throw

case extends instanceof public transient

catch false int return true

char final interface short try

class finally long static void

const float native super while

Además, el lenguaje se reserva unas cuantas palabras más, pero que hasta ahora no tienen un cometido específico. Son:

cast future generic inner

operator outer rest var

2.3. Comentarios y documentación en Java.

En Java hay tres modos de introducir comentarios en los archivos fuente:

// comentarios para una sola línea

/*

comentarios de una o más líneas

*/

/**

* comentario de documentación, de una o más líneas

*/

Los comentarios de documentación se colocan antes de una declaración de variable o función, y sirven como descripción del elemento declarado, permitiendo generar una documentación de las clases que se van construyendo al mismo tiempo que se genera el código de la aplicación. Al utilizar unas palabras clave o tokens la información que les sigue aparecerá de forma diferente al resto, permitiendo la incorporación de información útil, que luego se podrá ver en formato html sobre cualquier navegador. La utilidad javadoc.exe procesará la documentación para miembros public y protected (los comentarios para miembros private y package serán ignorados).

2.4. Variables y tipos de datos.

Todas las variables en el lenguaje Java deben ser de un tipo de dato primitivo (tipos de valor) o de un tipo de clase (tipos de referencia). El tipo de la variable determina los valores que la variable puede contener y las operaciones que se pueden realizar con ella.

Page 23: Fundamentos de JAVA

Fundamentos de Java /23

Los tipos de datos primitivos son aquellos que contienes los valores de datos más habituales y que no se corresponden con objetos de clase. La tabla siguiente resume los tipos primitivos de Java.

Tipo Rango de valores

Enteros byte de -128 a 127 short de -32768 a 32767 int de -21474836448 a 2147483647 long de -9223372036854775808 a 9223372036854775807

Reales float de -3402823E38 a -3402823E38 double números de hasta 15 decimales

Caracter char caracteres Unicode

Booleano boolean sólo admite los literales true y false

Sin tipo void

Por convención, los nombres de variables empiezan por una letra minúscula. Si una variable está compuesta de más de una palabra, como nombreDato, las palabras se ponen juntas y cada palabra después de la primera empieza con una letra mayúscula. Una declaración de variable siempre contiene dos componentes: el tipo de la variable y su nombre identificador. tipoDeVariable nombreDeVariable ;

En la declaración se puede asignar un valor inicial, por ejemplo: int altura = 170 ;

2.5. Literales.

Java utiliza hace uso de los literales para indicar valores en los programas. Los literales pueden ser: - Numéricos. Por ejemplo: 123 ó 234.767. - Booleanos. Sólo se usa true ó false. - Caracter. Constan de un solo caracter entre comillas sencillas; por ejemplo, '#'. - Strings o cadenas de caracteres. Constan de una combinación de caracteres entre comillas dobles; por ejemplo, "Hola, amigo".

Cuando se inserta un literal en un programa el compilador normalmente sabe exactamente de qué tipo se trata. Sin embargo, hay ocasiones en la que el tipo es ambiguo y hay que guiar al compilador proporcionándole información adicional para indicarle exactamente de qué tipo son los caracteres que componen el literal que se va a encontrar. En el ejemplo siguiente se muestran algunos casos en que resulta imprescindible indicar al compilador el tipo de información que se le está proporcionando: char c = 0xffff; // mayor valor de un char en hexadecimal

byte b = 0x7f; // mayor valor de un byte en hexadecimal

short s = 0x7fff; // mayor valor de un short en hexadecimal

int i1 = 0x2f; // hexadecimal en minúsculas

int i2 = 0X2F; // hexadecimal en mayúsculas

int i3 = 0177; // octal (un cero al principio)

long l1 = 100L; // el sufijo L (letra L mayúscula) indica un literal de tipo long

long l2 = 100l; // el sufijo l (letra L minúscula) indica un literal de tipo long

long l3 = 200; // el valor entero 200 es reconocido como un int, y en este caso se amolda a un long

float f1 = 1; // el valor entero 1 es reconocido como un int, y en este caso se amolda a un float

float f2 = 1F; // el sufijo F (letra F mayúscula) indica un literal de tipo float

float f3 = 1f; // el sufijo f (letra F minúscula) indica un literal de tipo long

double d1 = 1D; // el sufijo D (letra D mayúscula) indica un literal de tipo double

double d2 = 1d; // el sufijo d (letra D minúscula) indica un literal de tipo double

double d3 = 47.89; // un número con punto decimal es reconocido como un literal de tip double

double d4 = 1e-45; // un número en base 10 es reconocido como un literal de tipo double

En la escritura de valores literales tenemos que tener en cuenta las siguientes reglas: • Los tipos char y String admiten el uso de caracteres especiales mediante secuencias de escape. Una secuencia de escape comienza con una barra / seguida de una letra. Por ejemplo, el literal '\n' indica el caracter de salto de línea, '\t' indica el caracter de tabulación, '\b' indica el caracter de retroceso, etc. • Un valor hexadecimal (base 16), que funciona con todos los tipos enteros de datos, se indica mediante un 0x o 0X seguido por 0-9 y a-f, bien en mayúsculas o minúsculas.

Page 24: Fundamentos de JAVA

Fundamentos de Java /24

• Si se intenta inicializar una variable con un valor mayor que el que puede almacenar, el compilador generará un error. Por ejemplo, si se exceden los valores para char, byte y short, el compilador automáticamente convertirá el valor a un int e indicará que se necesita un moldeo estrecho para la asignación. • Los números octales (base 8), se indican colocando un cero a la izquierda del número que se desee. • Se puede colocar un carácter al final del literal para indicar su tipo, ya sea una letra mayúscula o minúscula: L se usa para indicar un long, F para un float y una D para un double. • La exponenciación se indica con la letra e, tomando como referente la base 10. Es decir, 1.3e-45f en Java es en realidad 1.3 x 10-45. • Los literales numéricos con separador decimal o con números exponenciales son interpretados como de tipo double, y por lo tanto:

float f0 = 36.45; // error de conversión de tipo

float f1 = 1e-45; // error de conversión de tipo

float f2 = 1e-45f; // conversión implícita correcta en base 10

• Cuando se realizan operaciones matemáticas o a nivel de bits con tipos de datos básicos más pequeños que int (char, byte o short), esos valores son promocionados a int antes de realizar las operaciones y el resultado es de tipo int. Si se quiere seguir teniendo el tipo de dato original, hay que colocar un moldeo, teniendo en cuenta que al pasar de un tipo de dato mayor a uno menor, es decir, al hacer un moldeo estrecho, se puede perder información. • En general, el tipo más grande en una expresión es el que determina el tamaño del resultado de la expresión; si se multiplica un float por un double, el resultado será un double, y si se suma un int y un long, el resultado será un long.

2.6. Operadores.

2.6.1. Introducción. Los operadores de Java son muy parecidos en estilo y funcionamiento a los de C. Tanto C, como C++, como Java, proporcionan un conjunto de operadores para poder realizar acciones sobre uno o dos operandos. Un operador que actúa sobre un solo operando es un operador unario, y un operador que actúa sobre dos operandos es un operador binario. Algunos operadores pueden funcionar como unarios y como binarios, el ejemplo más claro es el operador - (signo menos). Como operador binario, el signo menos hace que el operando de la derecha sea sustraído al operando de la izquierda; como operador unario hace que el signo algebraico del operando que se encuentre a su derecha sea cambiado. En la siguiente tabla aparecen los operadores que se utilizan en Java, por orden de precedencia:

.

++

!

*

+

<<

<

&

&&

? :

=

[]

--

~

/

-

>>

>

^

||

op=

()

instanceof

%

>>>

<= >= == !=

|

(*= /= %= += -= etc.)

Los operadores numéricos se comportan como esperamos: int + int = int

Los operadores relacionales devuelven un valor booleano. Para las cadenas de texto se pueden utilizar los operadores relacionales para comparaciones además de + y += para la concatenación: String nombre = "José " + "Luís ";

String nombre += "Martínez";

Cuando el signo más se utiliza en esta última forma, el operando de la derecha se convierte automáticamente en una cadena de caracteres antes de ser concatenada con el operando que se encuentra a la izquierda del operador +. El compilador asume que el operando de la derecha es capaz de soportar la conversión.

Page 25: Fundamentos de JAVA

Fundamentos de Java /25

2.6.2. Operadores aritméticos. Java soporta varios operadores aritméticos que actúan sobre números enteros y números en coma flotante. Los operadores binarios soportados por Java son:

+ suma los operandos - resta el operando de la derecha al de la izquierda * multiplica los operandos / divide el operando de la izquierda entre el de la derecha % calcula el resto de la división del operando izquierdo entre el derecho

El operador módulo (%) en Java funciona con tipos en coma flotante además de con tipos enteros. Los operadores unarios que soporta Java son:

+ indica un valor positivo - negativo, o cambia el signo algebraico ++ suma 1 al operando, como prefijo o sufijo -- resta 1 al operando, como prefijo o sufijo

En los operadores de incremento (++) y decremento (--), en la versión prefijo, el operando aparece a la derecha del operador, ++x; mientras que en la versión sufijo, el operando aparece a la izquierda del operador, x++. La diferencia entre estas versiones es el momento en el tiempo en que se realiza la operación representada por el operador si éste y su operando aparecen en una expresión larga. Con la versión prefijo, la variable se incrementa (o decrementa) antes de que sea utilizada para evaluar la expresión en que se encuentre, mientras que en la versión sufijo, se utiliza la variable para realizar la evaluación de la expresión y luego se incrementa (o decrementa) en una unidad su valor. El siguiente código muestra el resultado de utilizar estos operadores como prefijo y como sufijo: int x1 = 1; // como resultado, x1=1

int x2 = x1++; // como resultado, x1=2 y x2=1

int x3 = ++x2; // como resultado, x2=2 y x3=2

2.6.3. Operadores relacionales y condicionales. Los operadores relacionales en Java devuelven un tipo booleano, true o false. Los siguientes operadores retornan true si:

> el operando izquierdo es mayor que el derecho >= el operando izquierdo es mayor o igual que el derecho < el operando izquierdo es menor que el derecho <= el operando izquierdo es menor o igual que el derecho == el operando izquierdo es igual que el derecho != el operando izquierdo es distinto del derecho

Los operadores relacionales combinados con los operadores condicionales se utilizan para obtener expresiones más complejas. Los operadores condicionales que soporta Java retornan true si:

&& expresiones izquierda y derecha son true || o la expresión izquierda o al expresión de la derecha son true ! la expresión de la derecha es false

En Java, los operandos de estos operadores deben ser tipos booleanos, o expresiones que devuelvan un tipo booleano. Una característica importante del funcionamiento de los operadores && y ||, es que las expresiones se evalúan de izquierda a derecha y que la evaluación de la expresión finaliza tan pronto como se pueda determinar el valor de la expresión. Por ejemplo, en la expresión siguiente: ( a < b ) || ( c < d )

si la variable a es menor que la variable b, no hay necesidad de evaluar el operando izquierdo del operador || para determinar el valor de la expresión entera. En casos de complicadas, complejas y largas expresiones, el orden en que se realizan estas comprobaciones puede ser fundamental, y cualquier error en la colocación de los operandos puede dar al traste con la evaluación que se desea realizar; y estos errores son harto difíciles de detectar, ya que se debe estudiar concienzudamente el resultado de las expresiones en tiempo de ejecución para poder detectar el problema. Para obligar a que se evalúen todas las expresiones, en vez de && y || se utiliza respectivamente & y |.

Page 26: Fundamentos de JAVA

Fundamentos de Java /26

2.6.4. Operadores a nivel de bits. Java comparte con otros lenguajes un conjunto de operadores que realizan operaciones sobre un solo bit cada vez. Java también soporta el operador >>> y, además, el entorno en el que se realizan algunas de las operaciones no siempre es el mismo en Java que en los otros lenguajes. Los operadores de bits que soporta Java son los que aparecen en la siguiente tabla:

Operador Uso Operación >> Operando >> Despl Desplaza bits del operando hacia la derecha las posiciones

indicadas (con signo) << Operando << Despl Desplaza bits del operando hacia la izquierda las posiciones

indicadas >>> Operando >>> Despl Desplaza bits del operando hacia la derecha las posiciones

indicadas (sin signo) & Operando & Operando Realiza una operación AND lógica entre los dos operandos | Operando | Operando Realiza una operación OR lógica entre los dos operandos ^ Operando ^ Operando Realiza una operación lógica OR Exclusiva entre los dos

operandos ~ ~Operando Complementario del operando (unario)

En Java, el operador de desplazamiento hacia la derecha sin signo rellena los bits que pueden quedar vacíos con ceros. Los bits que son desplazados fuera del entorno se pierden. Cada desplazamiento a la derecha equivale a una división por 2. En el desplazamiento a la izquierda hay que ser precavidos cuando se trata de desplazar enteros positivos pequeños, porque el desplazamiento a la izquierda tiene el efecto de multiplicar por 2 para cada posición de bit que se desplace; y esto es peligroso porque si se desplaza un bit 1 a la posición más alta, el bit 31 o el 63, el valor se convertirá en negativo. 2.6.5. Operadores de asignación. El operador = es un operador binario de asignación de valores. El valor almacenado en la memoria y representado por el operando situado a la derecha del operador es copiado en la memoria indicada por el operando de la izquierda. Java soporta toda la panoplia de operadores de asignación que se componen con otros operadores para realizar la operación que indique ese operador y luego asignar el valor obtenido al operando situado a la izquierda del operador de asignación. De este modo se pueden realizar dos operaciones con un solo operador. += -= *= /= %= &= |= ^= <<= >>= >>>=

Por ejemplo, las dos sentencias que siguen realizan la misma función: x += y;

x = x + y;

2.6.6. Operador ternario «if-then-else». Java, lo mismo que C y C++, soporta el operador ternario: expresion ? sentencia1 : sentencia2

en donde expresion puede ser cualquier expresión de la que se obtenga como resultado un valor booleano; y si es true entonces se evalúa la sentencia1 y en caso contrario se evalúa la sentencia2. La limitación que impone el operador es que sentencia1 y sentencia2 deben devolver el mismo tipo de valor, y éste no puede ser void. Puede resultar útil para evaluar algún valor que seleccione una expresión a utilizar, como en la siguiente sentencia: cociente = denominador == 0 ? 0 : numerador / denominador

Cuando Java evalúa la asignación, primero mira la expresión que está a la izquierda del interrogante. Si denominador es cero, entonces evalúa la expresión que está entre el interrogante y los dos puntos, y se utiliza como valor de la expresión completa. Si denominador no es cero, entonces evalúa la expresión que está después de los dos puntos y se utiliza el resultado como valor de la expresión completa, que se asigna a la variable que está a la izquierda del operador de asignación, cociente.

2.6.7. Operador «instanceof». El operador instanceof permite saber si un objeto pertenece a una determinada clase, retornando valor true o false. Por ejemplo: String s = “hola”;

Page 27: Fundamentos de JAVA

Fundamentos de Java /27

Object objeto = s;

boolean x = objeto instanceof String; // x toma el valor true

Este operador actúa en tiempo de ejecución determinando siempre el tipo real de un objeto referenciado por una variable. El tipo que se evalúa a la derecha de la expresión debe ser compatible con el tipo de la variable que se evalúa a la izquierda. El operador instanceof no evalúa el tipo de una variable sino el del objeto que referencia, por ello no se puede utilizar para evaluar variables de tipo primitivo. 2.6.8. Moldeo (casting) de operadores. Java automáticamente cambia el tipo de un dato a otro cuando es pertinente. Por ejemplo, si se asigna un valor entero a una variable declarada como float, el compilador convierte automáticamente el int a float. El casting, o moldeo, permite hacer esto explícitamente, o forzarlo cuando normalmente no se haría. Para realizar un moldeo se coloca el tipo de dato que se desea (incluyendo todos los modificadores) dentro de paréntesis a la izquierda del valor. Por ejemplo: int i = 100;

long l = (long) i;

long l2 = (long) 200;

Como se puede ver, es posible realizar el moldeo de un valor numérico del mismo modo que el de una variable. No obstante, en el ejemplo el moldeo es superfluo porque el compilador ya promociona los enteros a flotantes cuando es necesario automáticamente, sin necesidad de que se le indique. Aunque hay otras ocasiones en que es imprescindible colocar el moldeo para que el código compile, por ejemplo: float f1 = (float) 200.56; // es necesario una conversión explícita

En el moldeo se aplican las siguientes reglas: • Se aplica un moldeo implícito cuando se asigna un valor numérico a una variable de un tipo numérico de mayor tamaño. (Es el denominado moldeo amplio.) • Debe aplicarse un moldeo explícito cuando se asigna un valor numérico a una variable de un tipo numérico de menor tamaño. En este caso el valor será truncado en sus bits menos significativos. (Es el denominado moldeo estrecho.) • Se permite el moldeo de cualquier tipo primitivo en otro tipo primitivo, excepto en el caso de los booleanos, en que no se permite moldeo alguno. • No se puede moldear un string a una variable de tipo primitivo.

2.7. Estructuras de programación.

El control del flujo es la manera que tiene un lenguaje de programación de provocar que el flujo de la ejecución avance y se ramifique en función de los cambios de estado de los datos. 2.7.1. Sentencias de salto. • La bifurcación if/else tiene la siguiente estructura: if( expresión-booleana ) {

// sentencias cuando se cumple la condición booleana

}

[else {

// sentencias cuando no se cumple la condición booleana

}]

Esta construcción provoca que la ejecución atraviese un conjunto de estados booleanos que determinan que se ejecuten distintos fragmentos de código. La cláusula else es opcional. Cada una de las sentencias puede ser una sentencia compuesta y la expresión-booleana podría ser una variable simple declarada como boolean, o una expresión que utilice operadores relacionales para generar el resultado de una comparación.

• La sentencia switch tiene la siguiente estructura: switch( expresión ) {

case valor1:

sentencias;

[break;]

[case valor2:

sentencias;

[break;]]

[default:

sentencias;]

}

Page 28: Fundamentos de JAVA

Fundamentos de Java /28

La sentencia switch proporciona una forma limpia de enviar la ejecución a partes diferentes del código en base al valor de una única variable o expresión. La expresión puede devolver cualquier tipo básico, y cada uno de los valores especificados en las sentencias case debe ser de un tipo compatible. La sentencia switch funciona de la siguiente manera: el valor de la expresión se compara con cada uno de los valores literales de las sentencias case. Si coincide con alguno, se ejecuta el código que sigue a la sentencia case. Si no coincide con ninguno de ellos, entonces se ejecuta la sentencia default (por defecto), que es opcional. Si no hay sentencia default y no coincide con ninguno de los valores, no hace nada. Al igual que en otros lenguajes, cada constante en sentencia case debe ser única. El compilador de Java inspeccionara cada uno de los valores que pueda tomar la expresión en base a las sentencias case que se proporcionen, y creará una tabla eficiente que utiliza para ramificar el control del flujo al case adecuado dependiendo del valor que tome la expresión. Por lo tanto, si se necesita seleccionar entre un gran grupo de valores, una sentencia switch se ejecutará mucho más rápido que la lógica equivalente codificada utilizando sentencias if-else. La palabra clave break se utiliza para que la ejecución salte tras el final de la sentencia switch. Si no se pone el break, la ejecución continuará en el siguiente case. Una limitación de esta sentencia es que la expresión a evaluar como máximo admite un valor de tipo int. Por tanto no permite evaluar valores enteros largos, numéricos decimales, ni strings.

2.7.2. Sentencias de bucle. • El bucle for tiene la siguiente estructura: for( inicialización; terminación; iteración ) {

sentencias;

}

Un bucle for normalmente involucra a tres acciones en su ejecución: - Inicialización de la variable de control (Cláusula de inicialización) - Comprobación de la variable de control en una expresión condicional (Cláusula de terminación) - Actualización de la variable de control (Cláusula de iteración)

La cláusula de inicio y la cláusula de iteración pueden estar compuestas por varias expresiones separadas mediante el operador coma (,).

for( a=0 , b=0 ; a < 7 ; a++ , b+=2 )

El operador coma garantiza que el operando de su izquierda se ejecutará antes que el operando de su derecha. Las expresiones de la cláusula de inicio se ejecutan una sola vez, cuando arranca el bucle. Cualquier expresión legal se puede emplear en esta cláusula, aunque generalmente se utiliza para inicialización. Las variables se pueden declarar e inicializar al mismo tiempo en esta cláusula:

for( int cnt=0 ; cnt < 2 ; cnt++ )

La segunda cláusula, de testeo, consiste en una única expresión que debe evaluarse a false para que el bucle concluya. El valor de la segunda cláusula es comprobado cuando la sentencia comienza la ejecución y en cada una de las iteraciones posteriores. La tercera cláusula, de iteración, aunque aparece físicamente en la declaración de bucle no se ejecuta hasta que se han ejecutado todas las sentencias que componen el cuerpo del bucle for; por ello se utiliza para actualizar la variable de control. Es importante tener en cuenta que si utilizamos variables incluidas en esta tercera cláusula en las sentencias del cuerpo del bucle, su valor no se actualizará hasta que la ejecución de todas y cada una de las sentencias del cuerpo del bucle se haya completado. En esta cláusula pueden aparecer múltiples expresiones separadas por el operador coma, que serán ejecutadas de izquierda a derecha. La primera y tercera cláusulas del bucle for pueden encontrarse vacías, pero deben estar separadas por punto y coma (;). Incluso la cláusula de testeo puede estar vacía, y en este caso se implementa un bucle infinito.

• El bucle while tiene la siguiente estructura: [inicialización;]

while( terminación-expresión-booleana ) {

sentencias;

[iteración;]

}

Page 29: Fundamentos de JAVA

Fundamentos de Java /29

El bucle while es la sentencia de bucle más básica en Java. Ejecuta repetidamente una vez tras otra unas sentencias mientras una expresión booleana sea verdadera. Las partes de inicialización e iteración, que se presentan entre corchetes, son opcionales. Esta sentencia while se utiliza para crear una condición de entrada. El significado de esta condición de entrada es que la expresión condicional que controla el bucle se comprueba antes de ejecutar cualquiera de las sentencias que se encuentran situadas en el interior del bucle, de tal modo que si esta comprobación es false la primera vez, el conjunto de las sentencias no se ejecutará nunca.

• El bucle do/while tiene la siguiente estructura: [inicialización;]

do {

sentencias;

[iteración;]

} while( terminación-expresión-booleana );

A veces se puede desear el ejecutar el cuerpo de un bucle while al menos una vez, incluso si la expresión booleana de terminación tiene el valor false la primera vez. Es decir, si se desea evaluar la expresión de terminación al final del bucle en vez de al principio como en el bucle while. Esta construcción do-while hace eso exactamente.

2.7.3. Control de errores y excepciones. Cuando durante la ejecución de un programa se produce un error grave (como una división por cero o una mala asignación de memoria), puede provocar un bloqueo o la terminación abrupta del mismo. Para prevenirlo se debe proteger el código sospechoso mediante una estructura try/catch/finally:

try {

// instrucciones que pueden provocar una excepción

} catch ( Excepcion1 e ) {

// instrucciones si hay una excepción del tipo Excepcion1

} catch ( Exception e ) {

// instrucciones si hay una excepción de cualquier tipo menos la de los tipos de excepciones anteriores

} finally {

// instrucciones que se ejecutan siempre (con o sin excepción)

}

Java implementa excepciones para facilitar la construcción de código robusto. Cuando ocurre un error en un programa el código que encuentra el error lanza una excepción, que se puede capturar y recuperarse de ella. Java proporciona muchas clases de excepciones predefinidas. Esta estructura está sujeta a las siguientes reglas:

• Los bloques catch y finally son opcionales, pero un bloque try debe ir siempre acompañado al menos de un bloque catch o de un bloque finally. • Sólo puede haber como máximo un bloque finally, acompañando siempre a un bloque try. • Pueden existir varios bloques catch, siempre y cuando se indiquen distintos tipos de excepciones. • Si no se producen excepciones en el bloque try se ejecuta el código del bloque finally (si existe) y se continúa ejecutando el código posterior a la estructura. • Si se produce una excepción en el bloque try, será ejecutado sólo el código del primer bloque catch que coincida con el tipo de excepción producido, se ejecuta el código del bloque finally (si existe) y se continúa ejecutando el código posterior a la estructura. • Normalmente el tipo Exception se corresponderá con el último bloque catch, puesto que este tipo es la base de todas las excepciones. • Las instrucciones del bloque finally se ejecutarán siempre, incluso aunque se ejecute un return en el bloque try. Sin embargo existe una excepción a este regla, y es que se invoque la instrucción System.exit(), que provoca la finalización abrupta de la máquina virtual de Java.

2.7.4. Control general del flujo. • Sentencia break. break [etiqueta];

La sentencia break puede utilizarse en una sentencia switch o en un bucle. Cuando se encuentra en una sentencia switch, break hace que el control del flujo del programa pase a la siguiente sentencia que se encuentre fuera del entorno del switch. Si se encuentra en un bucle, hace que el flujo de ejecución del programa deje el ámbito del bucle y pase a la siguiente sentencia que venga a continuación del bucle.

Page 30: Fundamentos de JAVA

Fundamentos de Java /30

Java incorpora la posibilidad de etiquetar la sentencia break, de forma que el control pasa a sentencias que no se encuentran inmediatamente después de la sentencia switch o del bucle, es decir, saltará a la sentencia en donde se encuentre situada la etiqueta. La sintaxis de una sentencia etiquetada es la siguiente:

etiqueta: sentencia;

• Sentencia continue. continue [etiqueta];

La sentencia continue no se puede utilizar en una sentencia switch, sino solamente en bucles. Cuando se encuentra esta sentencia en el discurrir normal de un programa Java, la iteración en que se encuentre el bucle finaliza y se inicia la siguiente. Java permite el uso de etiquetas en la sentencia continue, de forma que el funcionamiento normal se ve alterado y el salto en la ejecución del flujo del programa se realizará a la sentencia en la que se encuentra colocada la etiqueta. Por ejemplo, al encontrarse con bucles anidados, se pueden utilizar etiquetas para poder salir de ellos:

uno: for( ) {

dos: for( ) {

continue; // seguiría en el bucle interno

continue uno; // seguiría en el bucle principal

break uno; // se saldría del bucle principal

}

}

• Sentencia return. return expresión;

La sentencia return se utiliza para terminar un método o función y opcionalmente devolver un valor al método de llamada. En el código de una función siempre hay que ser consecuentes con la declaración que se haya hecho de ella. Por ejemplo, si se declara una función para que devuelva un entero, es imprescindible colocar un return final para salir de esa función, independientemente de que haya otros en medio del código que también provoquen la salida de la función. En caso de no hacerlo se generará un aviso (Warning), y el código Java no se puede compilar con avisos.

int func() {

if( a == 0 ) {

return 1;

}

return 0; // es imprescindible este return porque este método debe retornar siempre un entero

}

Si el valor a retornar es void se puede omitir el valor de retorno, con lo que la sintaxis se queda en un sencillo return, y se usaría simplemente para finalizar el método en que se encuentra, y devolver el control al método o función de llamada.

3. Almacenamiento de datos en memoria

3.1. Arrays.

Java permite juntar y manejar múltiples valores u objetos través de un objeto array, que representa una lista de elementos que pueden ser accedidos a través de un índice entero.

índices: 0 1 2 3 . . .

contenido: Primer elemento

Segundo elemento

Tercer elemento

Cuarto elemento

3.1.1. Arrays de una dimensión. En general, cuando se crea un array, se utiliza el operador new, más el tipo de dato de los elementos del array, más el número de elementos deseados encerrado entre corchetes ('[' y ']'). Ejemplo de creación de un array de 10 número enteros. int [] listavalores = new int [10]; // o bien

int listavalores [] = new int [10];

El operador new asigna la memoria necesaria para guardar como máximo 10 números enteros. Cada valor puede ser accedido por orden de índice contando desde cero. Por ejemplo, para asignar el primer valor a 156 escribiríamos:

Page 31: Fundamentos de JAVA

Fundamentos de Java /31

listavalores [0] = 156;

Java controla el uso de los índices de un array. Por tanto, si intentásemos acceder a un elemento de listavalores con el índice 12 se generaría una excepción en tiempo de ejecución. Es posible obtener el número de elementos máximo de un array mediante la propiedad length. Por ejemplo: int n = listavalores.length;

Se pueden inicializar los valores de un array en su declaración. En este caso no es necesario usar el operador new, sino que se ponen los valores entre llaves separados por comas: Ejemplos de arrays inicializados con y sin el operador new: int [] enteros1 = {23, 57, 34, 12};

int [] enteros2 = new int [] {23, 57, 34, 12};

String [] palabras1 = {"asa", "auto", "mesa"};

String [] palabras2 = new String [] {"asa", "auto", "mesa"};

3.1.2. Arrays multidimensionales. Una array multidimensional o matriz permite crear listas de elementos que a su vez son listas. Para crear un array bidimensional de 3 filas por 2 columnas con números enteros se escribiría: int [] [] matriz = new int[3][2]; // o bien

int matriz [] [] = new int[3][2];

La instrucción anterior reserva memoria para los 6 elementos del array. Pero se podría sólo asignar memoria para las filas escribiendo: int [] [] matriz = new int[3][];

Los elementos matriz[0], matriz[1] y matriz[2] serían arrays de enteros que todavía no tiene asignada memoria. Para asignársela escribiríamos: matriz [0] = new int[2];

matriz [1] = new int[3];

matriz [2] = new int[4];

Como de puede ver, podemos establecer distintas longitudes en cada fila.

3.2. Cadenas de caracteres.

La clase más básica para manipular textos en Java en mediante objetos de la clase String, cuyas instancias representan una cadena de caracteres constante. 3.2.1. La clase «String». Un objeto java.lang.String representa una cadena alfanumérica de un valor constante, y que por tanto no puede ser cambiado después de haber sido creado. Una string se crea de dos maneras:

- Asignando una cadena literal de caracteres entre comillas dobles. String s1 = "Hola";

- Utilizando un constructor con el operador new. String s2 = new String ("Hola");

Existen varios constructores para crear nuevas cadenas: new String();

Crea un string vacío. new String( String value );

Crea un string con el mismo contenido que otro string. new String( char value[] );

Crea un string a partir de un array de caracteres. new String( char value[], int indiceInicial, int cuenta );

Crea un string con parte del contenido de un array de caracteres. new String( byte bytes[] );

new String( byte bytes[], String enc );

Crea un string a partir del contenido de un array de bytes. new String( byte bytes[], int offset, int length );

new String( byte bytes[], int offset, int length, String enc );

Crean un string con parte del contenido de un array de bytes. new String( StringBuffer buffer );

Crea un string a partir del contenido del argumento. Donde el parámetro enc de algunos constructores indica el tipo de cifrado de los bytes. Por defecto los strings cifran los bytes en formato Unicode.

Page 32: Fundamentos de JAVA

Fundamentos de Java /32

Los métodos que incluye esta clase son: • char charAt(int i)

Retorna el caracter de la posición i, teniendo en cuenta que el primer caracter tiene la posición cero. • int compareTo(String s)

Devuelve un valor negativo si la cadena es lexicalmente menor que s, devuelve cero si son iguales y un valor positivo si es mayor.

• String concat(String str) Retorna un string producto de concatenar el actual con el argumento str.

• boolean equals(Object obj) Comparación de strings.

• boolean equalsIgnoreCase(String str2) Lo mismo que equals() pero no tiene en cuenta mayúsculas o minúsculas.

• getChars(int i1, inti2, char[] c, int pos) Copia los caracteres entre las posiciones i1 y i2 al array c a partir del índice pos.

• int indexOf(int ch) • int indexOf(int ch, int fromIndex) • int lastIndexOf(int ch) • int lastIndexOf(int ch, int fromIndex) • int indexOf(String str) • int indexOf(String str, int fromIndex) • int lastIndexOf(String str) • int lastIndexOf(String str, int fromIndex)

Devuelven el primer/último índice de un caracter/cadena empezando la búsqueda a partir de un determinado desplazamiento.

• long length() Retorna el número de caracteres del string.

• boolean regionMatches(int of, String s, int oof,int len) • boolean regionMatches(boolean ignoreCase, int of, String s, int oof,int len)

Comprueban si una región de esta cadena es igual a una región de otra cadena. • String replace(char c1, char c2)

Retorna el string remplazando el caracter c1 por c2. • boolean startsWith(String prefix)

• boolean startsWith(String prefix, int of) • boolean endsWith(String suffix)

Indica si el string comienza o termina con un cierto prefijo o sufijo comenzando en un determinado desplazamiento.

• String substring(int i, int len) Retorna el substring a partir de la posición i y de len caracteres.

• Sting toLowerCase() Retorna el string convertido a minúsculas.

• String toUpperCase() Retorna el string convertido a mayúsculas.

• String trim() Retorna el string eliminando espacios blancos al principio y final.

• String valueOf( x ) Retorna la representación como string del argumento x.

Java permite concatenar cadenas fácilmente utilizando el operador +. El siguiente fragmento de código concatena tres cadenas para producir un nuevo string. "La entrada tiene " + contador + " caracteres."

Hay que tener en cuenta que el operador +, cuando se aplica en expresiones donde al menos unos de los operando es un string, provoca que todos los operandos sean convertidos automáticamente a su representación string. Por ejemplo: String s = "El número " + 2 + " sumado con " + 4 + " da como resultado " + (2+4)

Da como resultado: s = "El número 2 sumado con 4 da como resultado 6"

Page 33: Fundamentos de JAVA

Fundamentos de Java /33

3.2.2. Funciones de conversión de strings. La clase String posee numerosas funciones para transformar valores de otros tipos de datos a su representación como cadena. Todas estas funciones tienen el nombre de valueOf, estando el método sobrecargado para todos los tipos de datos básicos. A continuación se muestra un ejemplo de su utilización: float f = 3.141592;

String PI = String.valueOf( f ); // PI = "3.141592"

String letraA = String.valueOf( 'A' ); // letraA = "A"

String sTrue = String.valueOf( 4 > 2); // sTrue = "true"

Es posible convertir un string a otra tipo de dato si su contenido es válido. Podemos usar las clases envolventes de los tipos primitivos, las cuales poseen método parse para realizar estas conversiones. Por ejemplo, el método parseInt() perteneciente a la clase Integer puede recibir 2 argumentos y su función es convertir el argumento de tipo String que recibe como primer argumento a un valor de tipo entero con signo y a la base especificada por el segundo argumento. Ya que también convierte a una base especifica, el primer argumento String debe contener dígitos acordes a esa base. int i1 = Integer.parseInt("23", 10); // asigna el valor 23

int i2 = parseInt("-FF", 16); // asigna el valor -255

De forma análoga a Integer.parseInt(), existen los métodos Long.parseLong(), Float.parseFloat(), Double.parseDouble(), oct. 3.2.3. Diferencia entre variables string y variables de tipo primitivo. El modo en cómo se asigna memoria para variables de tipo string y las de tipo primitivo difiere. Cuando se hace una asignación de datos de tipo primitivo se copian los contenidos:

int x = 7;

int y = 12; x

y

x = y; x

y

Cuando se asignan datos de tipo string se copian las referencias y no los contenidos:

String s1 = "Hola";

String s2; s1 s2 = null

s2 = "Adios"; s1

s2

s1 = s2 ;

s1

s2

Si concatenamos consecutivamente un string, se van creando nuevos bloques de memoria:

String s = ""

for (int i = 0; i < 4; i++)

s = s + "A"

s

Todos aquellos bloques de memoria que quedan sin referenciar son liberados automáticamente por un subproceso de la Máquina Virtual denominado recolector de basura.

AAA

AA

A

Adios

Hola

Adios

Hola

Hola

12

12

12

7

Page 34: Fundamentos de JAVA

Fundamentos de Java /34

III - ORIENTACIÓN A OBJETOS

La programación orientada a objetos o POO es un paradigma de programación que usa objetos y sus interacciones para diseñar aplicaciones y programas de ordenador. Está basada en varias técnicas, incluyendo herencia, modularidad, polimorfismo y encapsulamiento. Los objetos son entidades que combinan un estado, un comportamiento y una identidad:

• El estado está compuesto de datos, guardados en uno o más atributos a los que se habrán asignado unos valores concretos. • El comportamiento está definido por los procedimientos o métodos con los que puede operar dicho objeto; es decir, qué operaciones se pueden realizar con él. • La identidad es una propiedad de un objeto que lo diferencia del resto.

La programación orientada a objetos expresa un programa como un conjunto de estos objetos, que colaboran entre ellos para realizar tareas. Esto permite hacer los programas y módulos más fáciles de escribir, mantener, y reutilizar.

1. Fundamentos de la orientación a objetos en Java

1.1. Clases y objetos.

Java es un lenguaje orientado a objetos que usa clases y objetos, los cuales encapsulan básicamente datos (como campos o atributos) y acciones (como métodos). Cuando creamos aplicaciones se distingue entre dos tipos de clases:

- Clases de entidad: Aquellas que encapsulan los datos usados en la aplicación. Por ejemplo, una clase que encapsule los datos de un cliente.

- Clases de sesión: Aquellas que contienen los procesos y acciones que debemos realizar en la aplicación. Por ejemplo, una clase que contenga los métodos necesarios para procesar altas, bajas y modificaciones de clientes.

Las clases actúan como tipos de datos complejos y por tanto podemos declarar variables de su tipo. Estas variables se utilizarán para referenciar objetos de la clase especificada. Debemos definir las clases antes de crear objetos o instancias del tipo de la clase. 1.1.1. ¿Qué es una clase? Una clase es como un plano o plantilla que puede ser usada para crear objetos. La clase define las características de un tipo de objeto, tales como los datos que el objeto puede contener y las operaciones que puede realizar. 1.1.2. ¿Qué es un objeto? Un objeto es una instancia de una clase. Si una clase es como un plano, entonces un objeto es lo que se crea a partir del plano. La clase es la definición de un elemento, y el objeto es el elemento actual. Muchas veces el término instancia es usado como sinónimo de objeto. 1.1.3. Ejemplo de clases y objetos. Los planos para una casa son como una clase, y la casa que se edifica a partir de los planos es un objeto. En orientación a objetos podemos definir la clase Casa que especifica todas las casas que tienen un área, color y tamaño indeterminados. Podemos crear un objeto de la clase Casa que tenga un área, color y tamaño determinado.

1.2. Campos, propiedades, métodos y eventos.

Las clases contienen miembros que representan los datos y comportamiento de las clases y objetos de ese tipo de clase. Estos miembros pueden ser campos, propiedades y métodos. Las clases también pueden contener eventos, especialmente las clases que representan controles gráficos como botones y cuadros de texto.

Campos Miembros de la clase que representan los datos que definen un objeto o clase.

Propiedades Miembros de la clase que proporcionan acceso a los campos de la clase.

Métodos Miembros de la clase que definen acciones que el objeto o clase puede realizar

Eventos Acciones generadas desde un objeto que pueden ser gestionadas desde código.

Page 35: Fundamentos de JAVA

Fundamentos de Java /35

1.2.1. ¿Qué es un campo? Un campo o atributo es un miembro de la clase que representa un trozo de los datos que la clase necesita rellenar en su diseño. Normalmente se definen los campos como variables miembros privados de la clase. Esto impide que los campos puedan ser accedidos directamente desde fuera de la clase. 1.2.2. ¿Qué es una propiedad? Una propiedad es un miembro de la clase que proporciona un mecanismo flexible para leer, escribir o computar el valor de un campo. Podemos usar propiedades como miembros públicos en vez de los campos. Para crear una propiedad se utilizan unos métodos especiales llamados accesores. Cuando definimos una propiedad en una clase, podemos proporcionar accesores para obtener el valor (get), asignar el valor (set), o ambos. La siguiente tabla describe los tres tipos de propiedades que podemos definir.

Tipo de propiedad Accesores Ejemplo en una clase «CuentaBancaria»

Lectura-Escritura Accesor get y set Una propiedad que lee y asigna el titular de una cuenta

Solo-lectura Accesor get Una propiedad que obtiene el saldo actual

Solo-escritura Accesor set Una propiedad que asigna el máximo reintegro permitido

1.2.3. ¿Qué es un método? Un método es un miembro de la clase que contiene un bloque de código y representa una acción que el objeto o la clase puede realizar. Los métodos pueden acceder a los campos de la clase para completar sus tareas designadas. 1.2.4. ¿Qué es un evento? Un evento es un miembro de la clase que indica qué cosas le han ocurrido a un objeto o clase. Los eventos proporcionan un mecanismo de notificación que permite responder al evento y realizar las operaciones apropiadas. 1.2.5. Ejemplo de campos, propiedades, métodos y eventos. Una clase que representa un calendario puede tener los siguientes miembros:

• Tres campos enteros: uno para el día, otro para el mes y otro para el año. • Propiedades para obtener y asignar estos campos, y propiedades adicionales para computar valores como el día del año, y cuándo el año es bisiesto. • Métodos para incrementar la fecha por un día, por un mes, y por un año. • Eventos para avisar de que algún aspecto de la fecha ha cambiado.

1.3. ¿Qué es encapsulación?

Los lenguajes no orientados a objetos como C y COBOL consisten de datos, tanto en una base de datos como en memoria, e instrucciones para manipularlos. Estos lenguajes no fuerzan ni ordenan la relación entre los datos y el código que debe manipularlos. Si algún aspecto de los datos cambia (por ejemplo, si el campo año cambia de dos dígitos a cuatro), entonces seguramente deberemos cambiar todo el código que usa esos datos. Los lenguajes orientados a objetos soportan el concepto de encapsulación, de forma que podemos controlar la visibilidad y accesibilidad de los datos y sus detalles de implementación en nuestras aplicaciones. 1.3.1. ¿En qué consiste encapsular los datos? Encapsulación es la habilidad de un objeto o clase para ocultar sus datos internos y detalles de implementación, haciendo que solo partes concretas de los objetos o clases sean accesibles programáticamente. La encapsulación es un principio esencial de la orientación a objetos. Por ejemplo, cuando definimos una clase deberíamos definir siempre los campos como privados. Esto previene accesos externos de código directamente. El único camino, entonces, para que código externo interactúe con un objeto o clase es a través de una interfaz pública bien definida, la cual normalmente consiste de propiedades y métodos de la clase. 1.3.2. Beneficios de la encapsulación. Se usa la encapsulación para ocultar la información. Cuando ocultamos información, como el estado interno de un objeto, el código externo se centra sólo en las características útiles de cara al exterior del objeto. La encapsulación ayuda a proteger los datos en un objeto; el código externo puedo interactuar con los datos de un objeto sólo a través de miembros definidos como accesibles. Por ejemplo, el mecanismo interno de un teléfono está oculto para los usuarios. Los cables, interruptores, y otras partes internas están encapsuladas dentro de su carcasa y no son accesibles al usuario.

Page 36: Fundamentos de JAVA

Fundamentos de Java /36

Podemos usar la encapsulación para cambiar fácilmente los detalles de implementación de nuestras aplicaciones sin que los usuarios de nuestros objetos o clase experimenten ningún cambio en el modo en que interactúan con el objeto o clase.

1.4. ¿Qué es sobrecarga?

Cuando añadimos métodos o campos a una clase debemos elegir un nombre significativo para ellos de forma que lo describa sin ambigüedades. En muchos casos puede ser necesario definir varias implementaciones alternativas para un método dentro de una clase, con diferentes parámetros en cada versión. Java permite asignar el mismo nombre a cada uno de estos métodos, siempre y cuando cada versión del método tenga un número diferente de parámetros, o diferentes tipos. 1.4.1. ¿Por qué se sobrecarga? Sobrecarga es la posibilidad de definir varios métodos con el mismo nombre en la misma clase, teniendo cada método una firma o signatura diferente (la firma del método incluye el nombre del método, el valor de retorno y los parámetros). Los métodos sobrecargados normalmente contienen un código similar, ya que se suelen utilizar para pasar valores al objeto o clase en diversos formatos, o bien para pasar datos parciales al objeto y asignar valores por defecto al resto de datos. En este tipo de escenarios es recomendado tratar de centralizar el código de proceso en uno de los métodos sobrecargados y desde los otros invocar a éste. Sobrecargar permite crear clases más fáciles de comprender al usuario, porque los métodos que realizan la misma tarea tienen el mismo nombre. 1.4.2. Ejemplo de sobrecarga. Las librerías de clases de Java usan sobrecarga. Por ejemplo, la clase String tiene varias versiones sobrecargadas del método valueOf, permitiendo convertir cada tipo de dato primitivo a un objeto String. Algunos ejemplos de invocación del método valueOf son los siguientes: String entero = String.valueOf(124)

String decimal = String.valueOf(12.34)

String logico = String.valueOf(true)

1.5. Definición de una clase.

1.5.1. Cómo definir una nueva clase. En Java las clases encapsulan principalmente campos (variables miembro) y métodos (procedimientos y funciones). La sintaxis general de declaración de una clase es: modificadores class nombreClase extends nombreSuperClase implements Interfaz_1, ..., Interfaz_n {

// declaración de variables y métodos

}

Los «modificadores» son palabras claves que se especifican antes de la palabra class y se separan con un espacio. Se utilizan para especificar el tipo de accesibilidad de la clase y el comportamiento de la misma. Una clase sólo puede heredar (extends) de una superclase, pero puede implementar (implements) varias interfaces (se ampliarán estos conceptos en capítulos posteriores). El siguiente ejemplo de código crea una clase llamada CuentaBancaria que será utilizada para procesar ingresos y reintegros sobre un acumulado. package banco;

public class CuentaBancaria {

private double acumulado = 0; // campo interno con el acumulado actual

public double getAcumulado() { // propiedad accesora para obtener el acumulado actual

return acumulado;

}

public void ingreso (double valor) { // método para registrar un ingreso

acumulado += valor;

}

public void reintegro (double valor) { // método para registrar un reintegro

acumulado -= valor;

}

}

El código generado tiene las siguientes características: • Se crea la clase CuentaBancaria dentro de un paquete denominado banco.

Page 37: Fundamentos de JAVA

Fundamentos de Java /37

• Accesibilidad de la clase: la clase generada tiene el modificador de acceso público (puede ser utilizada desde cualquier otra clase). • Accesibilidad de los miembros: el campo acumulado es privado (no es accesible desde el exterior de la clase), los métodos getAcumulado(), ingreso() y reintegro() son públicos (son accesibles desde el exterior de la clase).

1.5.2. Niveles de acceso. Cuando diseñamos una clase es importante considerar la accesibilidad de la clase y sus miembros. Por ejemplo, debemos decidir si la clase será visible para toda la aplicación o se restringirá su acceso al paquete en el cual está definida. Igualmente, debemos decidir un nivel de acceso para cada uno de sus miembros. Java proporciona tres palabras, conocidas como modificadores de acceso, las cuales especifican 4 niveles de acceso para las clases y sus miembros. Si no especificamos un modificador de acceso en la definición de la clase o de un miembro, el modificador por defecto es de paquete (package). La siguiente tabla muestra estos cuatro niveles. Las clases sólo pueden especificar accesibilidad public o accesibilidad por defecto (cuando no se indica ninguna de las otras).

Modificador de acceso Nivel de acceso

public Accesible por cualquier código de la aplicación

private Accesible sólo por el código que contiene la clase

Accesible por todo el código del paquete actual

protected Accesible por el código contenido en la clase y sus derivadas

1.5.3. Modificadores de tipos de clases. Además de los modificadores de acceso, en una clase se pueden especificar los siguientes:

Modificador Significado

abstract Establece que la clase no puede se instanciada (no se pueden crear objetos de la misma, pero sí se pueden declarar variables). Si la clase tiene un método abstracto, la clase también debe ser abstracta.

final Establece que la clase no puede ser usada como superclase de otras clases.

synchronizable Establece que todos los métodos de la clase estén sincronizados, es decir, que no se puede acceder al mismo tiempo a ellos desde distintos hilos de ejecución.

1.5.4. Cómo añadir campos y métodos a las clases. Podemos definir muchos campos y métodos en una clase, dependiendo de los objetivos y funcionalidades buscadas por la clase. Definiendo campos. Cada campo tiene un nombre, un tipo, y un modificador de acceso. Podemos poner la definición de un campo en cualquier sitio (fuera de un método) de la clase. Algunos programadores prefieren situarlos al principio de la clase y otros al final. La sintaxis de declaración de un campo es: modificadores Tipo nombre_campo [ = valor_inicial ] ;

Podemos aplicar los siguientes modificadores a un campo:

Modificador Significado

public Modificador de acceso para permitir que sea accesible desde cualquier otra clase.

private Modificador de acceso para que sólo sea accesible por la propia clase.

protected Modificador de acceso para que sólo sea accesible por la propia clase y sus subclases.

Si no se especifica public, private o protected, se aplica el modificador de acceso por defecto para que sólo sea accesible por la propia clase y todas aquellas que pertenezcan al mismo paquete.

static Indica que será una variable estática o de clase. Estas variables son compartidas por todas las instancias de la clase, y por tanto tienen un mismo valor para todos los objetos de la clase.

final Indica que, una vez asignada, la variable no puede cambiar su valor inicial. Es decir, el valor asignado permanece invariable. La primera asignación de una variable final puede realizarse en su declaración o en el constructor de la clase. Si la variable final es de un tipo de objeto, estonces su valor es una referencia que no puede ser cambiada, pero el objeto referenciado sí puede ser modificado (si lo permite el tipo de objeto).

Page 38: Fundamentos de JAVA

Fundamentos de Java /38

transient Indica que la variable no será serializada en un proceso de serialización de un objeto de la clase. (Véase el capítulo dedicado a la Serialización.)

volatile Impide que una variable ser optimizado por el compilador cuando sea usada desde distinto hilos de ejecución sincronizados.

Definiendo métodos Un método es un procedimiento o una función en una clase. Cada método tiene un nombre, una lista de parámetros, un tipo de retorno si es una función o void si es un procedimiento, y modificadores de acceso. modificadores tipo_retorno nombre_método ( lista_parámetro ) {

bloque de código

}

Un método tiene acceso completo y no restringido a todos los demás miembros de su clase. Esto es un aspecto muy importante de la programación orientada a objetos; los métodos encapsulan operaciones sobre los campos de su clase. Podemos aplicar los siguientes modificadores a un método:

Modificador Significado

public Modificador de acceso para permitir que sea accesible desde cualquier otra clase.

private Modificador de acceso para que sólo sea accesible por la propia clase.

protected Modificador de acceso para que sólo sea accesible por la propia clase y sus subclases.

Si no se especifica public, private o protected, se aplica el modificador de acceso por defecto para que sólo sea accesible por la propia clase y todas aquellas que pertenezcan al mismo paquete.

static Indica que será un método estático o de clase. Pueden ser invocados sin necesidad de crear un objeto de la clase.

final Evita que un método pueda ser sobrescrito por una subclase

native Para indicar métodos escritos en otro lenguaje.

synchronized Para sincronizar métodos usados por hilos de ejecución distintos.

abstract Indica un método sin bloque de código.

Cómo añadir propiedades a una clase. Uno de los principios más importantes de la orientación a objetos es que las clases deben encapsular su estado interno e implementación. Uno de los caminos que podemos seguir para aplicar este principio es definir todos los campos como privados. Si requerimos acceder a los valores de los campos de una clase definiremos propiedades mediante métodos accesores de tipo get y tipo set, como sigue:

• El accesor get debe retorna un valor del tipo de dato especificado. • El accesor set recibe un valor explícito como parámetro indicando el nuevo valor de la propiedad.

Por ejemplo, si una clase declara la variable nombre como sigue: private String nombre;

Sus métodos accesores deben definirse como sigue: public String getNombre() {

return nombre;

}

public void setNombre(String nombre) {

this.nombre = nombre;

}

Si queremos definir una propiedad de solo-lectura solamente debemos crear el método get correspondiente, si queremos que sea una propiedad de solo-escritura, solamente debemos crear el método set correspondiente. Si la propiedad es de tipo booleano el nombre del método accesor get debe comenzar por is. Por ejemplo, si una clase declara la variable activo como sigue: private boolean activo;

Sus métodos accesores deben definirse como sigue: public boolean isActivo() {

return activo;

}

public void setActivo(boolean activo) {

this.activo = activo;

}

Page 39: Fundamentos de JAVA

Fundamentos de Java /39

1.5.5. Cómo añadir constructores a las clases. Cuando creamos un objeto en el código, debemos de asegurarnos de que también los estamos inicializando apropiadamente. El modo más fácil de garantizar esto es definir a uno o varios constructores en la clase. Cuando creamos un objeto, el intérprete invoca automáticamente a un constructor para inicializar el objeto. Definición de constructores. Un constructor es un método que el intérprete invoca implícitamente y automáticamente cuando creamos un objeto. Las siguientes reglas y directrices se aplican cuando definimos un constructor.

• Son procedimientos que no retornan valor (ni siquiera void) con el mismo nombre que la clase. • Nunca retorna valor, pero sí pueden definir parámetros. Podemos crear tantos constructores como queramos, siempre y cuando se diferencien en su lista de parámetros. • Si no creamos explícitamente un constructor, el compilador genera uno por defecto sin argumentos. • Normalmente son declarados con accesibilidad pública. Podemos crear constructores privados para que sean llamados desde otro constructor de la misma clase, y protegidos para que sean invocados por los constructores de las subclases derivadas. • Normalmente se utilizan par inicializar algún o todos los campos del objeto. También pueden realizar operaciones adicionales como escribir información a un archivo de registro.

La sintaxis general para los constructores es como sigue: modificadores NombreClase ( lista_de_parámetros ) {

// Código de inicialización

}

Ejemplo. El siguiente ejemplo define tres constructores para una clase CuentaBancaria como sigue:

• El primer constructor recibe dos parámetros: uno para inicializa el nombre del titular, y otro para inicializar el saldo. • El segundo constructor es un constructor sin argumentos que asigna el nombre del titular y el saldo a valores por defecto. • El tercer constructor recibe un parámetro string, que se usa para inicializar el nombre del titular, e inicializa el saldo a cero.

public class CuentaBancaria {

private String nombre;

private double saldo;

public CuentaBancaria (String nombre, double saldo) {

this.nombre = nombre

this.saldo = saldo

}

public CuentaBancaria () {

this("[Desconocido]", 0) // Se llama al primer constructor

}

public CuentaBancaria (String nombre) {

this ( nombre , 0) // Se llama al primer constructor

}

. . .

}

Debemos resaltar que el segundo y tercer constructor invocan al primero para asignar los valores. Ésta es una buena práctica muy recomendable de programación. 1.5.6. Cómo compartir miembros. Para hacer que un campo, propiedad o método de una clase sea compartido por todas las instancias de la clase, de forma que tenga el mismo valor para todas las instancias debemos usar el modificador static. Los campos compartidos se crean una sola vez (no cada vez que se instancia un objeto) y deben inicializarse en un método especial denominado inicializador. Cuándo se utiliza «static». Al compartir un miembro de una clase, éste está disponible para cada instancia; en cambio, si es no compartido, cada instancia mantiene su propia copia. Por ejemplo, esto es útil si el valor de una variable se aplica a toda la aplicación. Si se declara esta variable static, todas las instancias tendrán acceso a la misma ubicación de almacenamiento y si una instancia cambia el valor de la variable, todas las instancias tendrán acceso al valor actualizado.

Page 40: Fundamentos de JAVA

Fundamentos de Java /40

El uso compartido no modifica el nivel de acceso de un miembro. Por ejemplo, un miembro de clase puede ser compartido y privado (accesible sólo desde dentro la clase), o no compartido y público. Reglas de uso.

• Acceso. Se tiene acceso a un elemento compartido calificándolo con su nombre de clase, y no con el nombre de variable de una instancia específica de su clase o estructura. Ni siquiera se tiene que crear una instancia de una clase o estructura para tener acceso a sus miembros compartidos. • Restricciones del código. El código de una propiedad o método compartido no puede utilizar otros miembros no compartidos de la clase, pero sí otros miembros compartidos de la clase.

Comportamiento. • Almacenamiento. Una variable compartida se almacena en la memoria sólo una vez, independientemente del número de instancias que se hayan creado de su clase. De igual manera, una propiedad o procedimiento compartido contiene sólo un conjunto de variables locales. • Acceso mediante una variable de instancia. Es posible tener acceso a un elemento compartido calificándolo con el nombre de una variable que contiene una instancia específica de su clase o estructura. Aunque esto suele funcionar del modo previsto, el compilador genera un mensaje de advertencia aconsejando el acceso mediante el nombre de la clase en lugar de la variable. • Acceso mediante una expresión de instancia. Si se tiene acceso a un elemento compartido mediante una expresión que devuelve una instancia de su clase o estructura, el compilador obtiene acceso mediante el nombre de clase en lugar de evaluar la expresión. Esto genera unos resultados inesperados si se había previsto que la expresión realizara otras acciones además de devolver la instancia.

Ejemplo. El siguiente código crea un campo, interesBase, y un método compartido dentro de la clase CuentaBancaria. public class CuentaBancaria {

private static double interesBase;

. . .

public static double interesActual ( ) {

return interesBase + 0.2;

}

. . .

}

1.5.7. Inicializadores. Un inicializador static es un método sin nombre ni argumentos que se invoca automáticamente la primera vez que se usa la clase. Consiste en un bloque de código que permite inicializar variables estáticas, invocar métodos de librerías nativas, etc. class MiClase {

//Se declara un atributo de clase

static int contador;

// Éste es el método inicializador

static {

contador = 0;

}

}

También existen inicializadores de objeto que no llevan la palabra static, que se usan para crear clases anónimas (véase el capítulo de clases internas anónimas). 1.5.8. Finalizadores. Java dispone del recolector de basura (garbage collector), un proceso que destruye aquellos objetos que ya no se usan. El método finalize, si se usa, será invocado por el recolector de basura cuando destruya a un objeto. ...........

// Cierra el canal cuando este objeto es reciclado

protected void finalize() {

close();

}

...........

Además del método System.gc(), que permite invocar al recolector de basura en cualquier instante, también puede ser posible forzar la ejecución de los finalizadores usando:

Page 41: Fundamentos de JAVA

Fundamentos de Java /41

java.lang.Runtime.runFinalizersOnExit(boolean)

java.lang.System.runFinalizersOnExit(boolean)

Pero esto no garantiza que se liberen todos los recursos y, por lo tanto, que se produzca la finalización del objeto.

1.6. Declaración de métodos.

1.6.1. Sintaxis de un método. Los métodos son funciones miembro de una clase. Java no permite funciones que no pertenezcan a una clase. Su sintaxis general es la siguiente: modificadores tipo_retorno nombre ( argumentos ) throws excepciones

El término modificadores puede ser una lista de los siguientes valores: public, private, protected, static, abstract y final. El término tipo_retorno indica el tipo del valor que retornará el método. Puede ser un tipo de dato primitivo o un tipo de clase existente. El término nombre es el nombre del método. El término argumentos es una lista opcional de parámetros que se pueden pasar al método. Su sintaxis es:

( tipo1 param1, tipo2 param2, ... ) La cláusula throws excepciones indica las excepciones que puede relanzar el método. 1.6.2. Valor de retorno de un método. En Java es imprescindible que a la hora de la declaración de un método se indique el tipo de dato que ha de devolver. Si no devuelve ningún valor, se indicará el tipo void como tipo de retorno. Todos los tipos primitivos en Java se devuelven por valor (se devuelve una copia del valor) y todos los objetos se devuelven por referencia (se devuelve una referencia al objeto en memoria). Para devolver un valor se utiliza la palabra clave return, seguida de una expresión que será evaluada para saber el valor de retorno. Esta expresión puede ser compleja o puede ser simplemente el nombre de un objeto, una variable de tipo primitivo o una constante. Si un programa Java devuelve una referencia a un objeto y esa referencia no es asignada a ninguna variable, o utilizada en una expresión, el objeto se marca inmediatamente para que el reciclador de memoria en su siguiente ejecución devuelve la memoria ocupada por el objeto al sistema, asumiendo que la dirección no se encuentra ya almacenada en ninguna otra variable. El valor de retorno debe coincidir con el tipo de retorno que se ha indicado en la declaración del método; aunque en Java, el tipo actual de retorno puede ser una subclase del tipo que se ha indicado en la declaración del método. Esto es posible porque todas las clases heredan desde un objeto raíz común a todos ellos: Object. También se puede utilizar un interfaz como tipo de retorno, en cuyo caso, el objeto retornado debe implementar dicho interfaz. 1.6.3. Nombre del método. El nombre del método puede ser cualquier identificador legal en Java. Java soporta el concepto de sobrecarga de métodos, es decir, permite que dos métodos compartan el mismo nombre pero con diferente lista de argumentos, de forma que el compilador pueda diferenciar claramente cuando se invoca a uno o a otro, en función de los parámetros que se utilicen en la llamada al método. 1.6.4. Métodos de instancia. Cuando se incluye un método en una definición de una clase Java sin utilizar el modificador static estamos generando un método de instancia. Aunque cada objeto de la clase no contiene su propia copia de un método de instancia (no existen múltiples copias del método en memoria), el resultado final es como si fuese así, como si cada objeto dispusiese de su propia copia del método. Cuando se invoca un método de instancia a través de un objeto determinado, si en el código de este método se referencian variables de instancia de la clase, éstas harán referencia a las variables de instancia específicas del objeto específico que se está invocando. La llamada a los métodos de instancia en Java se realiza utilizando el nombre del objeto, el operador punto y el nombre del método. miObjeto.miMetodoDeInstancia();

1.6.5. Métodos estáticos. Cuando un método se incluye en la definición de una clase Java utilizando el modificador static se obtiene un método estático o método de clase.

Page 42: Fundamentos de JAVA

Fundamentos de Java /42

Lo más significativo de los métodos de clase es que pueden ser invocados sin necesidad de que haya que instanciar ningún objeto de la clase. En Java se puede invocar un método de clase utilizando el nombre de la clase, el operador punto y el nombre del método. MiClase.miMetodoDeClase();

En Java, los métodos de clase operan solamente con variables de clase; no tienen acceso a variables de instancia de la clase, a no ser que se cree un nuevo objeto y se acceda a las variables de instancia a través de ese objeto. Si se observa el siguiente trozo de código de ejemplo: class Documento extends Pagina {

static int version = 10; // variable compartida

int numeroDeCapitulos; // variable de instancia

static void insertaUnCapitulo() {

numeroDeCapitulos++; // ESTO DA ERROR DE COMPILACIÓN

}

static void modificaVersion( int i ) {

version++; // esto sí funciona

}

}

La modificación de la variable numeroDeCapitulos no funciona porque se está violando una de las reglas de acceso al intentar acceder desde un método estático a una variable no estática. 1.6.6. Paso de parámetros. En Java, todos los métodos deben estar declarados y definidos dentro de una clase, y hay que indicar el tipo y nombre de los argumentos o parámetros que acepta. Los argumentos son como variables locales declaradas en el cuerpo del método que están inicializadas al valor que se pasa como parámetro en la invocación del método. En Java, todos los argumentos de tipos primitivos se pasan por valor, mientras que los objetos se pasan por referencia. Cuando se pasa un objeto por referencia, se está pasando la dirección de memoria en la que se encuentra almacenado el objeto. Si se modifica una variable que haya sido pasada por valor, no se modificará la variable original que se haya utilizado para invocar al método, mientras que si se modifica el contenido de una variable pasada por referencia, la variable original del método de llamada se verá afectada de los cambios que se produzcan en el método al que se le ha pasado como argumento. 1.6.7. Argumentos variables. Una nueva característica desde Java 5 es poder pasar un array de parámetros variables a un método. Anteriormente, si queríamos pasar una lista de argumentos a un método podíamos hacerlo creando un parámetro de tipo array. Por ejemplo: int suma ( int [] params) {

int s = 0;

for (int i : params )

s += i;

return s;

}

El método anterior declara un método suma que calcula y retorna la suma de una lista de valores enteros pasados como un array de valores int. La nueva característica de Java permite declarar el método anterior con la siguiente sintaxis: int suma ( int ... params) {

int s = 0;

for (int i : params )

s += i;

return s;

}

Al declarar el parámetro con tres puntos (...) se indica que el método admite de cero a varios argumentos de tipo int separados por comas. Por ejemplo, serían válidas las siguientes invocaciones del método suma: suma(); // retorna el valor 0

suma(5); // retorna el valor 5

suma(3, 7); // retorna el valor 10

Page 43: Fundamentos de JAVA

Fundamentos de Java /43

suma(3, 7, 2); // retorna el valor 12

Las normas a aplicar con argumentos variables son: • Todos los parámetros opcionales deben ser del mismo tipo, el declarado en el método. • No se pueden declarar a la vez varias listas de argumentos variables. Tampoco se pueden declarar más parámetros después. Las siguientes declaraciones no son válidas:

void unMetodo ( int ... x , int ... c ) { }

void unMetodo ( int ... x , int c ) { }

Pero sí se pueden declarar parámetros antes. La siguiente declaración es válida: void unMetodo ( double d , int ... x ) { }

• El argumento variable es pasado al código del método como un array con el mismo nombre que el parámetro y con tantos elementos como argumentos pasados en la invocación del método. Los argumentos son asignados al array en el mismo orden en que se pasan. • Si al invocar el método no se pasan argumentos, el array pasado al código será un array con cero elementos (y por tanto distinto de null).

A la hora de aplicar sobrecarga en el método debemos tener en cuenta que los dos siguientes métodos son distintos: void unMetodo( String ... s ) { System.out.print( "Primer método"; }

void unMetodo( String s1, String s2 ) { System.out.print( "Segundo método"; }

La invocación de este método producirá los siguientes resultados: unMetodo( ) // Se escribe: Primer método

unMetodo( "A") // Se escribe: Primer método

unMetodo( "A", "B") // Se escribe: Segundo método

unMetodo( "A", "B", "C") // Se escribe: Primer método

Como vemos, al pasar dos argumentos, el compilador invoca la sobrecarga que declara explícitamente los dos parámetros.

1.7. Tipos enumerados (enums).

A partir de Java 5 se permite que una variable tome valores dentro de un conjunto de valores predefinidos; en otras palabras, valores dentro de una lista enumerada. 1.7.1. Declaración de enumeraciones. Los tipos enumerados sirven para restringir la selección de valores a algunos previamente definidos. Por ejemplo, si tenemos una aplicación para la venta de café en vasos de diferentes tamaños pero no queremos que los tamaños sean diferentes a CHICO, MEDIANO y GRANDE, podemos crear un tipo enumerado para delimitar dicha selección: enum TamañoDeCafe { CHICO , MEDIANO , GRANDE };

Posteriormente, al elegir un tamaño de café podemos hacerlo de la siguiente manera: TamañoDeCafe tdc = TamañoDeCafe.MEDIANO;

No es necesario que las constantes dentro de las enumeraciones estén en mayúsculas, pero en las convenciones de código de Sun se pide hacerlo de esta manera. Se debe tomar en cuenta que un tipo enumerado puede ser declarado dentro o fuera de una clase, pero NO dentro de un método. Por ejemplo: enum Instrumentos{ GUITARRA, TROMPETA, BATERIA, BAJO };

public class Enumerados {

public static void main (String [] args) {

Instrumentos in = Instrumentos.BATERIA;

System.out.println(in);

EnumDentroClase edc = new EnumDentroClase();

edc.tamaño = EnumDentroClase.TamañoDeCafe.CHICO;

System.out.println(edc.tamano);

}

}

class EnumDentroClase{

enum TamañoDeCafe {GRANDE,MEDIANO,CHICO};

TamañoDeCafe tamaño;

}

Al ejecutar el código anterior, obtenemos: BATERIA

CHICO

Page 44: Fundamentos de JAVA

Fundamentos de Java /44

Los tipos enumerados no son enteros o cadenas, cada uno es simplemente una instancia del tipo enumerado del que es declarado. Se puede pensar como una especie (no exactamente) de array de variables estáticas finales. Además de constantes dentro de un tipo enumerado, existen algunas otras cosas que podemos declarar. 1.7.2. Declarar constructores, métodos y variables dentro de un tipo enumerado. Debido a que los tipos enumerados son como una clase de tipo especial en Java, hay muchas cosas que se pueden realizar dentro de un enum; además de declarar constantes, un tipo enumerado puede contener constructores, métodos y variables. Tomando el ejemplo del tamaño de café, podemos pensar en escenario donde además de saber el tamaño del café necesitemos la cantidad en onzas de cada tamaño, esto podemos hacerlo de la siguiente manera: enum TamañoCafe{

CHICO(5), MEDIANO(8), GRANDE(10);

private int onzas;

TamañoCafe(int onzas) {

this.onzas = onzas;

}

public int getOnzas() {

return this.onzas;

}

}

public class Cafe {

TamañoCafe tc;

public static void main (String[] args){

Cafe c1 = new Cafe();

Cafe c2 = new Cafe();

c1.tc = TamañoCafe.GRANDE;

c2.tc = TamañoCafe.CHICO;

System.out.println("Tamaño de café 1 (c1): "+c1.tc);

System.out.println("Tamaño de café 2 (c2): "+c2.tc);

System.out.println("Onzas 1 (c1): "+c1.tc.getOnzas());

System.out.println("Onzas 2 (c2): "+c2.tc.getOnzas());

}

}

Al ejecutar el código anterior obtendremos lo siguiente: Tamaño de café 1 (c1): GRANDE

Tamaño de café 2 (c2): CHICO

Onzas 1 (c1): 10

Onzas 2 (c2): 5

Algunos puntos importantes sobre los constructores de los tipos enumerados son: • No se puede invocar al constructor directamente, éste se invoca una vez que se crea el tipo enumerado y es definido por los argumentos utilizados para crearlo. • Se puede definir más de un argumento en un constructor de un tipo enumerado; asimismo, se puede definir más de un constructor para un tipo enumerado siempre y cuando éste tenga argumentos diferentes (sobrecargar el constructor). • El o los valores pasados al constructor deben establecer en la declaración de la enumeración, y no pueden ser establecidos al usar los valores de la enumeración.

1.7.3. Comparación entre tipos enumerados. Los valores de los tipos enumerados se pueden comparar entre ellos con los operados de igualdad y desigualdad; sin embargo, aunque los valores estén ordenados por el orden en que se declaran no puede aplicárseles los demás operadores de comparación. Por ejemplo: enum Dia { Lunes, Martes, Miercoles };

boolean b = Dia.Lunes == Dia.Martes; // b = false

boolean b = Dia.Lunes != Dia.Martes; // b = true

boolean b = Dia.Lunes <= Dia.Martes; // ERROR DE COMPILACIÓN

Como cualquier otra clase, los tipos enumerados también heredan de Object y por tanto disponen del método equasl() que permite comparar dos valores: boolean b = Dia.Lunes.equals(Dia.Martes); // b = false

Page 45: Fundamentos de JAVA

Fundamentos de Java /45

Como los tipos enumerados implementan la interfaz java.lang.Comparable disponen del método compareTo() para poder comparar un valor con otro pasado por parámetro. Por ejemplo: int i = Dia.Lunes.compareTo(Dia.Martes) // i = -1, para indicar que Lunes es menor que Martes

int i = Dia.Lunes.compareTo(Dia.Lunes) // i = 0, para indicar que Lunes es igual a Lunes

int i = Dia.Miercoles.compareTo(Dia.Martes) // i = 1, para indicar que Miercoles es mayor que Martes

1.8. Creación y destrucción de objetos.

Como hemos visto, un objeto es una instancia de una clase. La creación de un objeto se realiza en tres pasos (aunque se pueden combinar):

- Declaración, en la que se proporciona un nombre al objeto. - Instanciación, en la que se asigna memoria al objeto (normalmente mediante el operador new). - Inicialización, opcionalmente se pueden proporcionar valores iniciales a las variables de instancia del objeto

1.8.1. Declaración de objetos. Los objetos se declaran igual que las variables de los tipos primitivos. Por ejemplo, la instrucción: Objecto miObjeto;

Declara una variable de la clase Object que inicialmente no referencia ningún objeto. Por tanto, la comparación miObjto==null retornaría valor true. Para instanciar el objeto debemos utilizar el operador new, que asigna memoria para un objeto de la clase indicada y llama a uno de sus constructores. Por ejemplo, la instrucción: miObjeto = new Object();

Instancia un objeto de la clase Object y asigna su referencia en la variable miObjeto. Ahora, la comparación miObjto==null retornaría valor false. El operador new reserva memoria para guardar todas las variables no estáticas (heredadas o propias) del objeto y retorna una referencia a la posición de memoria. Proceso de creación de un objeto (al invocar new):

- Al crear el primer objeto de la clase se carga la clase en memoria y se ejecutan los inicializadores static. - Se reserva la memoria necesaria para el objeto. - Se asigna el valor por defecto de los atributos. - Se ejecutan los inicializadores del objeto. - Se ejecuta el código del constructor usado.

Varias variables pueden referenciar un mismo objeto. Por ejemplo, si escribimos: Object otroObjeto = miObjeto;

Conseguimos que tanto miObjeto como otroObjeto referencien el mismo objeto. 1.8.2. Utilización de objetos. Una vez que se tiene declarado un objeto con sus variables y sus métodos, podemos acceder a ellos para que el uso para el que se ha creado ese objeto entre en funcionamiento. Para acceder a variables o métodos en Java se especifica el nombre del objeto y el nombre de la variable miembro, o método, separados por un punto (.). En la siguiente sentencia, se muestran las formas de acceso en Java: System.out.println( "miObjeto referencia a " + miObjeto.toString() );

1.8.3. Destrucción de objetos (el recolector de basura). En Java, el programador no necesita preocuparse más de devolver la memoria utilizada por los objetos al sistema operativo. Eso se realizará automáticamente por parte de un proceso de la máquina virtual de Java denominado recolector de basura (o bien “garbage collector”). El único propósito para el que se ha creado este reciclador de memoria es para devolver al sistema operativo la memoria ocupada por objetos que ya no son utilizados. Un objeto es blanco del recolector de basura para su reciclado cuando ya no hay referencias a ese objeto. Sin embargo, el que un objeto sea elegido para su reciclado, no significa que eso se haga inmediatamente. El recolector de basura comprueba todos los objetos cuando pierden la referencia inicial con la que fueron creados, y aquellos que no tienen ninguna otra referencia son marcados y liberada la memoria que estaban consumiendo. El recolector de basura es un proceso (thread, o hilo de ejecución) de baja prioridad que puede ejecutarse sincrónicamente o asincrónicamente, dependiendo de la situación en que se encuentre el sistema Java. Se ejecutará sincrónicamente cuando el sistema detecta poca memoria o en respuesta a un programa Java. El

Page 46: Fundamentos de JAVA

Fundamentos de Java /46

programador puede activar el recolector de basura en cualquier momento llamando al método System.gc(). Se ejecutará asincrónicamente cuando el sistema se encuentre sin hacer nada en aquellos sistemas operativos en los que para lanzar un proceso haya que interrumpir la ejecución de otro, como es el caso de Windows. No obstante la llamada al sistema para que se active el recolector de basura no garantiza que la memoria que consumía sea liberada. A continuación se muestra un ejemplo sencillo de funcionamiento del recolector de basura: String s; // no se ha asignado memoria todavía

s = new String( "abc" ); // memoria asignada al objeto "abc"

s = "def"; // se ha asignado nueva memoria al objeto "def"

// el objeto "abc" es marcado por el garbage collector y se liberará su memoria

Antes de que el reciclador de memoria reclame la memoria ocupada por un objeto, se invoca el método finalize(). Una forma sencilla para provocar la destrucción de un objeto es asignar todas las variables que lo referencian a valor null: String miObjeto = "", otroObjeto = miObjeto;

miObjeto = null; // El recolector marca el objeto como candidato para su posible destrucción

otroObjeto = null; // El objeto es marcado para su destrucción

1.9. Factores en el diseño: cohesión y acoplamiento.

El acoplamiento y la cohesión juegan un rol central en el diseño de software. Yourdon y Constantine, en su obra clásica "Diseño Estructurado", identifican que el objetivo del diseño es minimizar los costos. El costo del software está determinado por el costo de mantenimiento, y el costo del mantenimiento está determinado por el costo de los cambios que surgen en el sistema. Un diseño de software efectivo minimiza la probabilidad de que se propaguen los cambios. Los cambios que involucran a un único elemento son menos costosos y más predecibles que los cambios a un elemento que requieren cambiar dos más, y luego tres... El costo esperado del cambio se puede reducir prestando especial atención a dos factores: el acoplamiento entre los elementos y la cohesión dentro de los elementos. 1.9.1. Cohesión. La cohesión es la medida del grado de identificación de un módulo de software con una función concreta. Distinguiremos entre varios tipos de cohesiones que afectan al diseño de las clases:

• Tipos de cohesión aceptables (fuerte) - COHESIÓN FUNCIONAL: un módulo realiza una única acción. - COHESIÓN SECUENCIAL: un módulo contiene acciones que han de realizarse en un orden particular sobre unos datos concretos. - COHESIÓN DE COMUNICACIÓN: un módulo contiene un conjunto de operaciones que se realizan sobre los mismos datos. - COHESIÓN TEMPORAL: las operaciones se incluyen en un módulo porque han de realizarse al mismo tiempo; p.ej. inicialización de una instancia.

• Tipos de cohesión inaceptables (débil) . COHESIÓN PROCEDURAL: un módulo contiene operaciones que se realizan en un orden concreto aunque sean independientes. - COHESIÓN LÓGICA: cuando un módulo contiene operaciones cuya ejecución depende de un parámetro; el flujo de control del módulo es lo único que une a las operaciones que lo forman. - COHESIÓN COINCIDENTAL: cuando las operaciones de un módulo no guardan ninguna relación observable entre ellas.

Hay que procurar evitar situaciones de cohesión procedural, lógica o coincidental. 1.9.2. Acoplamiento. El acoplamiento es la medida de la interacción de los módulos que constituyen un programa. Los niveles de acoplamiento (de mejor a peor) son:

- ACOPLAMIENTO DE DATOS (acoplamiento normal): Todo lo que comparten dos módulos se especifica en la lista de parámetros del módulo invocado. - ACOPLAMIENTO DE CONTROL: Cuando un módulo pasa datos que le indican a otro qué hacer (el primer módulo tiene que conocer detalles internos del segundo). - ACOPLAMIENTO EXTERNO: Cuando dos módulos utilizan los mismos datos globales o dispositivos de E/S (p.ej. ficheros).

Page 47: Fundamentos de JAVA

Fundamentos de Java /47

Si los datos son de sólo lectura, el acoplamiento se puede considerar aceptable. No obstante, en general, este tipo de acoplamiento no es deseable porque la conexión existente entre los módulos no es visible (de forma explícita). - ACOPLAMIENTO PATOLÓGICO: Cuando un módulo utiliza el código de otro o altera sus datos locales (“acoplamiento de contenido”). Los lenguajes estructurados incluyen reglas para el ámbito de las variables que impiden este tipo de acoplamiento. Los lenguajes orientados a objetos incluyen modificadores de visibilidad para evitar este tipo de acoplamiento.

El objetivo final es reducir al máximo el acoplamiento entre módulos y aumentar la cohesión interna de los módulos.

2. UML, el Lenguaje Unificado de Modelado

El lenguaje UML es un estándar OMG diseñado para visualizar, especificar, construir y documentar software orientado a objetos. Un modelo es una simplificación de la realidad. El modelado es esencial en la construcción de software para:

- Comunicar la estructura de un sistema complejo - Especificar el comportamiento deseado del sistema - Comprender mejor lo que estamos construyendo - Descubrir oportunidades de simplificación y reutilización

Un modelo proporciona “los planos” de un sistema y puede ser más o menos detallado, en función de los elementos que sean relevantes en cada momento. El modelo ha de capturar “lo esencial”. Todo sistema puede describirse desde distintos puntos de vista:

- Modelos estructurales (organización del sistema) - Modelos de comportamiento (dinámica del sistema)

UML estandariza 9 tipos de diagramas para representar gráficamente un sistema desde distintos puntos de vista.

2.1. Ventajas e inconvenientes de UML.

La ventaja principal de UML es que unifica distintas notaciones previas.

Los inconvenientes de UML son: - Falta de integración con otras técnicas (P.ej. diseño de interfaces de usuario) - UML es excesivamente complejo (y no está del todo libre de ambigüedades)

2.2. Diagramas UML.

2.2.1. Diagramas de clases. Los diagramas de clases muestran un conjunto de clases y sus relaciones.

Page 48: Fundamentos de JAVA

Fundamentos de Java /48

Los diagramas de clases proporcionan una perspectiva estática del sistema (representan su diseño estructural). UML aplica la siguiente notación para los diversos miembros de una clase:

• Atributos. [visibilidad] nombre [multiplicidad] [: tipo [= valor_por_defecto]]

• Operaciones [visibilidad] nombre ([[in|out] parámetro : tipo [, …]])[:tipo_devuelto]

Se debe interpretar esta notación de la siguiente manera: - Los corchetes indican partes opcionales - Visibilidad puede ser: privada (-), protegida (#) o pública (+) - Multiplicidad se expresa entre corchetes (p.ej. [2], [0..2], [*], [3..*]) - Los parámetros pueden ser de entrada (in) o de salida (out)

2.2.2. Diagramas de objetos. Los diagramas de objetos muestran un conjunto de objetos y sus relaciones (una situación concreta en un momento determinado). Los diagramas de objetos representan instantáneas de instancias de los elementos que aparecen en los diagramas de clases. Por tanto, un diagrama de objetos expresa la parte estática de una interacción. Para ver los aspectos dinámicos de la interacción se utilizan los diagramas de interacción.

Page 49: Fundamentos de JAVA

Fundamentos de Java /49

2.2.3. Diagramas de interacción. Los diagramas de interacción muestran una interacción concreta: un conjunto de objetos y sus relaciones, junto con los mensajes que se envían entre ellos. Distinguiremos entre diagramas de secuencias y diagramas de comunicación/colaboración.

• Diagramas de secuencia. Resaltan la ordenación temporal de los mensajes que se intercambian.

Los diagramas de secuencia muestran la secuencia de mensajes entre objetos durante un escenario concreto (paso de mensajes).

- En la parte superior aparecen los objetos que intervienen. - La dimensión temporal se indica verticalmente (el tiempo transcurre hacia abajo). - Las líneas verticales indican el período de vida de cada objeto. - El paso de mensajes se indica con flechas horizontales u oblicuas (cuando existe demora entre el envío y la atención del mensaje). - La realización de una acción se indica con rectángulos sobre las líneas de actividad del objeto que realiza la acción.

• Diagramas de comunicación/colaboración. Resaltan la organización estructural de los objetos que intercambian mensajes.

Page 50: Fundamentos de JAVA

Fundamentos de Java /50

La distribución de los objetos en el diagrama permite observar adecuadamente la interacción de un objeto con respecto de los demás.

- La perspectiva estática del sistema viene dada por las relaciones existentes entre los objetos (igual que en un diagrama de objetos). - La vista dinámica de la interacción viene indicada por el envío de mensajes a través de los enlaces existentes entre los objetos. (Los mensajes se numeran para ilustrar el orden en que se emiten.)

Los diagramas de secuencia y de comunicación son isomorfos, es decir: - Un diagrama de secuencia se puede transformar mecánicamente en un diagrama de comunicación. - Un diagrama de comunicación se puede transformar automáticamente en un diagrama de secuencia.

2.2.4. Otros diagramas UML. Se utilizan otro tipo de diagramas para representar aspectos dinámicos del sistema.

• Diagramas de casos de uso. Representan a los actores y casos de uso del sistema. Los diagramas de casos de uso se suelen utilizar en el modelado del sistema desde el punto de vista de sus usuarios para representar las acciones que realiza cada tipo de usuario.

• Diagramas de estados. Representan los estados y transiciones entre estados. Los diagramas de estados son especialmente importantes para describir el comportamiento de un sistema reactivo (cuyo comportamiento está dirigido por eventos).

• Diagramas de actividades.

Page 51: Fundamentos de JAVA

Fundamentos de Java /51

Representan el flujo de control en el sistema. Los diagramas de actividades muestran el orden en el que se van realizando tareas dentro de un sistema (el flujo de control de las actividades).

2.2.5. Diagramas UML para representar aspectos físicos del sistema. • Diagramas de componentes. Representan a los componentes de la aplicación y las dependencias entre ellos. Establecen la organización lógica de la implementación de un sistema.

• Diagramas de despliegue. Representan los nodos de procesamiento y sus componentes. Establecen la configuración del sistema en tiempo de ejecución.

Page 52: Fundamentos de JAVA

Fundamentos de Java /52

2.3. Relaciones entre clases: Diagramas de clases UML.

Las relaciones existentes entre las distintas clases nos indican cómo se comunican los objetos de esas clases entre sí. Los objetos se comunican entre sí invocando sus respectivos métodos y propiedades: los mensajes entre objetos se transmiten a través de estas invocaciones. Existen distintos tipos de relaciones:

- Asociación (conexión entre clases) - Dependencia (relación de uso) - Generalización/especialización (relaciones de herencia)

2.3.1. Asociación. Una asociación es una relación estructural que describe una conexión entre objetos. Gráficamente se muestra como una línea continua que une las clases relacionadas entre sí.

Navegación de las asociaciones. Aunque las asociaciones suelen ser bidireccionales (se pueden recorrer en ambos sentidos), en ocasiones es deseable hacerlas unidireccionales (restringir su navegación en un único sentido). Gráficamente, cuando la asociación es unidireccional, la línea termina en una punta de flecha que indica el sentido de la asociación:

En el caso de asociaciones unidireccionales se produce la siguiente equivalencia:

Equivale a

class Cuenta {

private Dinero balance;

public void ingresar (Dinero cantidad) {

balance += cantidad;

}

public void retirar (Dinero cantidad) {

Page 53: Fundamentos de JAVA

Fundamentos de Java /53

balance -= cantidad;

}

public Dinero getSaldo () {

return balance;

}

}

En este ejemplo se ha supuesto que Dinero es un tipo de dato con el que se pueden hacer operaciones aritméticas y hemos añadido un método adicional que nos permite comprobar el saldo de una cuenta. En el caso de asociaciones bidireccionales se produce la siguiente equivalencia:

Equivale a

public class Cuenta {

. . .

private Cliente titular;

. . .

}

public class Cliente {

. . .

private Cuenta cuenta[];

. . .

}

Con la salvedad de que en este caso, el enlace bidireccional hemos de mantenerlo nosotros. Un cliente puede tener varias cuentas, por lo que en la clase cliente hemos de mantener un conjunto de cuentas (un array en este caso). Multiplicidad de las asociaciones. La multiplicidad de una asociación determina cuántos objetos de cada tipo intervienen en la relación; es decir, el número de instancias de una clase que se relacionan con una instancia de la otra clase.

- Cada asociación tiene dos multiplicidades (una para cada extremo de la relación). - Para especificar la multiplicidad de una asociación hay que indicar la multiplicidad mínima y la multiplicidad máxima (mínima..máxima).

Multiplicidad Significado

1 Uno y sólo uno

0..1 Cero o uno

N..M Desde N hasta M

* Cero o varios

0..* Cero o varios

1..* Uno o varios (al menos uno)

- Cuando la multiplicidad mínima es 0, la relación es opcional. - Una multiplicidad mínima mayor o igual que 1 establece una relación obligatoria.

Todo departamento tiene un director. Un profesor puede dirigir un departamento.

Todo profesor pertenece a un departamento. A un departamento pueden pertenecer varios profesores.

Page 54: Fundamentos de JAVA

Fundamentos de Java /54

Relación opcional Un cliente puede o no ser titular de una cuenta

Relación obligatoria Una cuenta ha de tener un titular como mínimo

Relaciones involutivas. Se produce una relación involutiva cuando la misma clase aparece en los dos extremos de la asociación.

Agregación y composición. Un caso particular de una asociación es cuando se da una relación entre un todos y sus partes. En este caso hablaremos de una agregación de las partes al todo, o de una composición del todo con las partes. Gráficamente se muestran como asociaciones con un rombo en uno de los extremos. En la agregación las partes pueden formar parte de distintos agregados.

En la composición se produce una agregación disjunta y estricta: las partes sólo existen asociadas al compuesto (y por tanto sólo se accede a ellas a través del compuesto).

2.3.2. Dependencia. Una dependencia es una relación (más débil que una asociación) entre un cliente y el proveedor de un servicio usado por el cliente.

- Cliente es el objeto que solicita un servicio. - Servidor es el objeto que provee el servicio solicitado.

Gráficamente, la dependencia se muestra como una línea discontinua con una punta de flecha que apunta del cliente al proveedor. Tenemos como ejemplo de dependencia la resolución de una ecuación de segundo grado:

Page 55: Fundamentos de JAVA

Fundamentos de Java /55

Para resolver una ecuación de segundo grado hemos de recurrir a la función sqrt de la clase Math para calcular una raíz cuadrada. 2.3.3. Herencia (generalización y especialización). La herencia es la relación entre una superclase y sus subclases. Objetos de distintas clases pueden tener atributos similares y exhibir comportamientos parecidos (p.ej. animales, mamíferos…).

public class Empleado {

...

}

public class Profesor extends Empleado {

...

}

public class PAS extends Empleado {

...

}

Como la noción de clase está próxima a la de conjunto, podemos hablar de herencia en términos de operaciones de conjuntos. Así, la generalización y especialización expresan relaciones de inclusión entre conjuntos. Si tomamos el siguiente ejemplo de herencia:

Instancias: coches vehículos - Todo coche es un vehículo. - Algunos vehículos son coches.

Propiedades: propiedades(coches) propiedades(vehículos) - Un coche tiene todas las propiedades de un vehículo. - Algunas propiedades del coche no las tienen todos los vehículos.

3. Herencia y jerarquía de clases.

La herencia es una técnica muy importante en la programación orientada a objetos que permite definir nuevas clases basadas en una clase existente. En este apartado se explica cómo usar la herencia para mejorar la calidad y consistencia de una aplicación orientada a objetos y se muestra la sintaxis para implementar herencia.

3.1. ¿Qué es herencia?

La herencia es un concepto muy importante en la orientación a objetos. Podemos usar herencia para definir una clase base común que determine las similitudes entre varias clases relacionadas. La clase base contendrá los campos comunes, propiedades y métodos de todas las clases. Podemos entonces definir clase derivadas

Page 56: Fundamentos de JAVA

Fundamentos de Java /56

que hereden los miembros de la clase base y que pueden añadir miembros específicos para cada clase derivada. Si la herencia la expresamos en un diagrama de clases, se forma una jerarquía de clases cuya cabeza es conocida como la superclase de la jerarquía. Las clases intermedias y finales de la jerarquía también son conocidas como subclases o clases hijas. 3.1.1. Ejemplo de herencia. Un banco puede ofrecer a sus clientes varios tipos diferentes de cuentas bancarias, como cuentas guardadas y cuentas validadas. Estos tipos diferentes de cuentas comparten características comunes; por ejemplo, cada tipo de cuenta tiene un número de cuenta, un balance actual, y un nombre de titular. Así mismo, cada tipo de cuenta permite a los clientes depositar y retirar dinero de la cuenta y determinar los fondos de la cuenta.

En una aplicación orientada a objetos, podemos definir una clase base llamada CuentaBancaria para representar las características comunes para aplicar a cada tipo de cuente. Podemos entonces definir clases derivadas llamadas CuentaAhorro y CuentaVivienda que hereden de CuentaBancaria y proporcionen campos adicionales, propiedades y métodos apropiados para el tipo de la cuenta. 3.1.2. Beneficios de la herencia. La siguiente lista detalla alguno de los beneficios de la herencia:

• Desarrollo productivo. La herencia ayuda a desarrollar aplicaciones más rápidamente, porque podemos reutilizar el código implementado en la clase base. La herencia reduce el código de la aplicación, lo cual simplifica las pruebas de software y la fase de mantenimiento en el ciclo de vida del software. • Modelado del mundo real. Podemos usar la herencia para crear jerarquías de clases que se correspondan con las relaciones entre entidades del mundo real. • Polimorfismo. Gracias a este concepto una aplicación cliente puede usar instancias de clases derivadas indistintamente, sin tener que determinar exactamente el tipo de instancia que realmente es. La aplicación cliente puede crear una instancia de una clase derivada y referenciarla como una variable de la clase base. Si la aplicación cliente invoca un miembro sobrescrito en la variable de la clase base, el polimorfismo automáticamente se encarga de llamar a la versión correcta del miembro invocado; la aplicación cliente no necesita verificar cada instancia para determinar el tipo de clase preciso.

3.2. Cómo definir clases base y clases derivadas.

Java proporciona un conjunto de palabras clave relacionadas con la herencia, que podemos usar para definir clases base y clases derivadas. 3.2.1. Definición de una clase base. Cuando definimos una clase que será heredada por otras clases, debemos decidir cómo los miembros de la clase base serán accesibles por las clases derivadas. La siguiente lista describe los niveles de acceso posibles:

Modificador Acceso de clase derivada Acceso de clase no derivada

private No No

protected Sí No

(package) Sí, si está en el mismo paquete Sí, si está en el mismo paquete

public Sí Sí

El modificador (package) hace referencia a la falta de cualquiera de los otros modificadores. 3.2.2. Definición de una clase derivada. Las siguientes reglas y directivas se aplican cuando definimos clases derivadas:

• Especificación de la clase base. La clase derivada debe especificar la clase de la cual hereda. Si no se especifica una clase base, se hereda por defecto de la clase System.Object. Una clase derivada sólo puede heredar de una única clase base. Las sintaxis general para derivar una clase es:

modificadores class NombreDeClaseDerivada extends NombreDeClaseBase {

// Miembros de la clase derivada

}

Page 57: Fundamentos de JAVA

Fundamentos de Java /57

• Definición de miembros adicionales en la clase derivada. La clase derivada hereda automáticamente todos los miembros definidos en la clase base, excepto los constructores. La clase derivada puede también definir campos adicionales, propiedades y métodos que aumenten las capacidades heredadas. • Acceso a miembros de la clase base. La clase derivada puede acceder a todos los miembros no privados definidos en la clase base. Si la clase derivada necesita acceder a un campo privado de la clase base, una buena solución es definirlo como protected.

3.2.3. Ejemplo de herencia. El siguiente ejemplo define una clase base llamada CuentaBancaria, la cual define los miembros comunes para todos los tipos de cuentas bancarias. La clase CuentaBancaria contiene campos privados (nombre, y balance) que no son accesibles por las clases derivadas. También contiene una propiedad protegida (balance) que es accesible por las clases derivadas. También se define una clase derivada CuentaAhorro que hereda de la clase base. public class CuentaBancaria {

private String nombre;

private double balance;

protected double getBalance() {

return balance;

}

// Otros miembros.

}

class CuentaAhorro extends CuentaBancaria {

// Miembros de la clase derivada.

}

3.2.4. Clases no heredables. Si queremos que una clase no puede ser usada para crear subclases debemos usar el modificador final. final class Punto extends FiguraBase {

public void dibuja() {

System.out.println("Punto (" + posicionX + "," + posicionY + ")");

}

}

class SubPunto extends Punto { // DA ERROR DE COMPILACIÓN

. . .

3.3. Sobreescritura o reescritura de miembros.

Cuando definimos una clase base, podemos especificar datos y funcionalidades comunes que serán aplicadas a todas las clases derivadas. Sin embargo, algunas operaciones en la clase base pueden requerir diferentes implementaciones en algunas o todas las clases derivadas. Para conseguir esta flexibilidad, por defecto, los campos, métodos y propiedades en la clase base son reescribibles. Cuando definimos un miembro reescribible en la clase base proporcionamos una implementación por defecto en la clase base. Cada clase derivada puede heredar la implementación por defecto o rescribir el miembro y proporcionar una implementación alternativa si es necesario. 3.3.1. Definición de miembros en clases derivadas. La siguiente lista describe las opciones disponibles para definir miembros en clases derivadas:

En clase base En clase derivada

Herencia Las clases derivadas implícitamente heredan los miembros definidos en la clase base. Si una clase derivada no necesita proporcionar una implementación alternativa a un miembro reescribible no necesitamos redefinir el miembro en la clase derivada.

Sobreescritura Si una clase derivada necesita proporcionar una implementación alternativa de un miembro reescribible, podemos redefinirlo en la clase derivada. Cuando se rescribe un método o una propiedad se aplica el polimorfismo. Cuando se rescribe un campo, el campo en la clase derivada oculta el campo de la clase base.

final Un método de la clase base definido con la palabra final no puede ser reescrito en ninguna clase derivada.

La palabra final aplicada a un campo lo convierte en una constante (en el sentido que sólo puede ser asignada una vez) y eso no afecta al hecho de que pueda rescribirse.

abstract Un método de la clase base definido con la palabra abstract debe ser reescrito en todas las

Page 58: Fundamentos de JAVA

Fundamentos de Java /58

clases derivadas que no sean abstractas.

La palabra abstract no es aplicable sobre campos.

3.3.2. Ejemplo de miembros reescritos. En el siguiente ejemplo, la case derivada Clase2 reescribe el método muestra() de la clase Clase1, y vuelve a declarar una variable. class Clase1 {

public int unaVar = 1;

public void muestra() {

System.out.println("Ésta es la clase " + unaVar);

}

}

class Clase2 extends Clase1 {

public int unaVar = 2;

public void muestra() {

System.out.println("Ésta es la clase " + unaVar);

}

public static void main (String [] args) {

Clase1 primero = new Clase1();

Clase2 segundo = new Clase2();

primero.muestra (); // Se muestra "Ésta es la clase 1".

segundo.muestra (); // Se muestra "Ésta es la clase 2".

}

}

Si aplicamos el polimorfismo con las clases anteriores obtendremos los siguientes resultados: Clase1 unaClase = new Clase2();

unaClase.muestra(); // Se muestra "Ésta es la clase 2".

System.out.print( unaClase.unaVar ); // Se muestra "1".

Se puede comprobar que al invocar un campo sobrescrito no se aplica el polimorfismo, sino que el compilador invoca la variable declarada en la clase del mismo tipo que la variable que invoca el campo. 3.3.3. Definición de miembros en la clase base como reescribibles. Por defecto, los métodos de una clase se definen como reescribibles. Si queremos evitar que un método pueda ser reescrito debemos usar el modificador final. Si queremos forzar que un método sea reescrito por las clases derivadas debemos usar el modificador abstract. 3.3.4. Reescritura en la clase derivada. Para rescribir un método en una clase derivada debemos declararlo con una firma compatible con la que tiene en la clase base. El que la firma sea compatible implica lo siguiente:

• Debe tener el mismo nombre de método. • Debe tener la misma lista de parámetros en cuanto a cantidad y tipos de los parámetros (el nombre de los parámetros no tiene porqué coincidir). Por ejemplo:

class A {

public void metodo1(int a1, String a2) { }

public void metodo2(int a1, String a2) { }

}

class B extends A {

public void metodo1(int b1, String b2) { }

public void metodo2(String a1, int a2) { }

}

En este caso metodo1 es un método reescrito, mientras metodo2 no se reescribe, sinó que se sobrecarga. • Debe tener el mismo tipo de retorno o una subclase del tipo. Por ejemplo:

class A {

public int metodo1() { return 1; }

public Exception metodo2() { return new Exception(); }

}

class B extends A {

public double metodo1() { return 1; }

public RuntimeException metodo2() { return new RuntimeException(); }

}

Page 59: Fundamentos de JAVA

Fundamentos de Java /59

En este caso metodo1 provoca un error de compilación al tener tipos de retorno no compatibles, mientras que metodo2 se reescribe por tener un tipo de retorno que es subclase del tipo Exception. • Si el método base relanza excepciones con la clásula throws, el método reescrito puede no relanzar excepciónes, relanzar las mismas excecpciones o relanzar otras excepciones. En todo caso, el compilador garantiza que el método reescrito relance las excepciones relanzadas por el método base. Por ejemplo:

class A {

public void metodo1() throws RuntimeException { }

public void metodo2() throws Exception { }

}

class B extends A {

public void metodo1() throws IndexOutOfBoundsException { }

public void metodo2() { }

}

El método metodo1 de la clase A sólo relanza RuntimeException, mientras que en la clase B relanza RuntimeException e IndexOutBoundsException. El método metodo2 relanza Exception en ambas clases, • El modificador de acceso debe ser el mismo u otro menos restrictivo. Por ejemplo, si en la clase base un método está definido con private, en una clase derivada puede ser reescrito como public, pero no al contrario.

Cuando rescribimos un miembro, algunas veces necesitamos invocar el miembro de la clase base para realizar los procesos por defecto. Para ello debemos invocarlo usando el objeto predefinido super. Por ejemplo: class A {

public void metodo1() {

System.out.println("Clase A");

}

}

class B extends A {

public void metodo1() {

super.metodo1(); // invoca el método de la clase A

System.out.println("Clase B");

}

}

Si se invoca el siguiente código: B b = new B();

b.metodo1();

El resultado será el siguiente: Clase A

Clase B

3.3.5. Diferencias entre sobrecarga y reescritura de un método. Hay que tener cuidado con la diferencia entre sobrecargar y sobrescribir un método, y entenderla correctamente. Para sobrecargar un método hay que duplicar el nombre del método pero utilizando una lista de argumentos diferente al original. Cuando el compilador se encuentra en una clase derivada un método con un nombre ya utilizado en una clase base realiza las siguiente consideraciones:

• Si coincide la lista de parámetros, el compilador considera que se intenta reescribir el método. Si el tipo de retorno no es compatible genera un error de compilación. • Si no coincide la lista de parámetros, el compilador considera que se está sobrecargando el método. Cualquier otro elemento de la firma es indiferente.

3.3.6. Referencias «this» y «super». Al acceder a variables de instancia de una clase, la constante predefinida this hace referencia a los miembros de la propia clase en el objeto actual. Para que un objeto referencie miembros de su superclase debe usar la constante predefinida super. 3.3.7. Ejemplo de reescritura de métodos. El siguiente ejemplo muestra cómo definir métodos reescribibles:

• La clase base, CuentaBancaria, define los métodos reescribibles deposita y retira. • La clase derivada, CuentaAhorro, rescribe el método retira.

class CuentaBancaria {

Page 60: Fundamentos de JAVA

Fundamentos de Java /60

private String nombre;

private double balance;

protected double getBalance() {

return balance;

}

public void deposita(double monto) {

balance += monto;

}

public void retira(double monto) {

balance -= monto;

}

}

class CuentaAhorro extends CuentaBancaria

public void retira(double monto) {

if (monto <= getBalance()) {

super.retira(monto);

}

}

}

3.4. Clases y métodos abstractos.

Una de las características más útiles de cualquier lenguaje orientado a objetos es la posibilidad de declarar clases que definen solamente cómo se utiliza, sin tener que implementar métodos. Son las clases abstractas. Mediante una clase abstracta se intenta fijar un conjunto mínimo de métodos (el comportamiento) y de atributos, que permitan modelar un cierto concepto, que será refinado y especializado mediante el mecanismo de la herencia. Como consecuencia, la implementación de la mayoría de los métodos de una clase abstracta podría no tener significado. Para resolver esto, Java proporciona los métodos abstractos. Estos métodos se encuentran incompletos, sólo cuentan con la declaración y no poseen cuerpo de definición. Esto es muy útil cuando la implementación es específica para cada usuario, pero todos los usuarios tienen que utilizar los mismos métodos. 3.4.1. Cómo crear clases abstractas. Cuando queramos que una clase no pueda instanciar objetos, para que sólo puedan hacerlo sus subclases debemos usar el modificador abstract. public abstract class FiguraBase

protected int posicionX, posicionY;

public void Dibuja() {

}

}

. . .

FiguraBase figura = new FiguraBase() // DA ERROR DE COMPILACIÓN

3.4.2. Cómo crear métodos abstractos. Cuando queramos crear un método sin código, para que sea reescrito en las clases derivadas y que actúe el polimorfismo, también debemos usar el modificador abstract en el método. public abstract class FiguraBase

protected int posicionX, posicionY;

public abstract void Dibuja() ;

}

Un método declarado como abstracto no lleva bloque de código. Un método abstracto no puede ser privado, puesto que entonces no podría ser accesible para ser reescrito. Además, si una clase posee un método abstracto propio obligatoriamente la clase también tiene que ser declarada como abstracta; si una subclase hereda un método abstracto debe declararse como abstracta o bien debe reescribir el método e implementarle algún bloque de código.

3.5. Control de acceso.

Cualquier miembro individual de una clase en puede ser designado como: private, public o protected. Un miembro private solamente puede ser accedido por otros miembros de la propia clase; no puede ser accedido por miembros de una clase heredada. Es la designación más restrictiva de todas.

Page 61: Fundamentos de JAVA

Fundamentos de Java /61

Un miembro designado como public puede ser accedido desde cualquier código dentro del ámbito de un objeto instanciado a partir de la clase. Es la designación menos restrictiva. La designación de protected entra en juego solamente cuando se ve involucrada la herencia. Un miembro designado como protected aparece como public para los miembros de clases derivadas de la clase y aparece como private para todas las demás. Además, Java incluye la noción de package (paquete) si no se ha indicado ninguno de los otros tres modificadores. Un miembro package es accesible por miembros de otras clases del mismo paquete. El control de acceso se aplica siempre a nivel de clase, no a nivel de objeto. Es decir, los métodos de instancia de un objeto de una clase determinada tienen acceso directo a los miembros privados de cualquier otro objeto de la misma clase. Por lo tanto, cuando se crea una nueva clase en Java, se puede especificar el nivel de acceso que se quiere para las variables de instancia y los métodos definidos en la clase: private, protected, public y package. La tabla siguiente muestra el nivel de acceso que está permitido a cada uno de los especificadores:

clase subclase paquete todos private X

protected X X

public X X X X

(package) X X

La columna “clase” indica que todos los métodos de una clase tienen acceso a todos los otros miembros de la misma clase, independientemente del nivel de acceso especificado. La columna “subclase” se aplica a todas las clases heredadas de la clase, independientemente del paquete en que residan. Los miembros de una subclase tienen acceso a todos los miembros de la superclase que se hayan designado como public. En general, si la subclase no se encuentra en el mismo paquete que la superclase, no tiene acceso a los miembros protegidos de la superclase. Los miembros de una subclase no tienen acceso a los miembros de la superclase catalogados como private o package, excepto a los miembros de una subclase del mismo paquete, que tienen acceso a los miembros de la superclase designados como package. La columna “paquete” indica que las clases del mismo paquete tienen acceso a los miembros de una clase, independientemente de su árbol de herencia. La tabla indica que todos los miembros protected, public y package de una clase pueden ser accedidos por otra clase que se encuentre en el mismo paquete. La columna “todos” indica que los privilegios de acceso para métodos que no están en la misma clase, ni en una subclase, ni en el mismo paquete, se encuentran restringidos a los miembros públicos de la clase.

3.6. La clase «Object».

Cuando creamos una clase que no extiende a ninguna otra el compilador de Java hace que extienda por defecto a la clase Object. Por tanto, la clase Object es la clase raíz de todo el árbol de la jerarquía de clases Java, y proporciona los siguientes métodos:

• public boolean equals( Object obj ) Se puede utilizar para comparar dos objetos. Se utiliza para comparar dos variables de objeto por contenido, al contrario que el operador == que compara por la referencia de las variables. El código base compara dos objetos por igualdad de referencias. Este método puede ser invocado por clases que gestionen una colección de objetos para establecer la igualdad entre los mismo. Un ejemplo típico de reescritura de este método es una clase puede ser el siguiente:

class Cliente {

public String nombre;

public boolean equals(Object obj) {

if (obj == null || this.getClass() != obj.getClass() // si el argumento es nulo o no es de la misma clase

return false;

if (this.nombre && ((Cliente) obj).nombre!=null) // si este nombre es nulo y el otro no

return false;

return this.nombre.equals((Cliente) obj).nombre) ; // si no, se compara por los nombres

}

La declaración de esta clase Cliente estable la igualdad de dos instancias en su coincidencia de los nombres. • public int hashCode( )

Page 62: Fundamentos de JAVA

Fundamentos de Java /62

Se utiliza para obtener un código de hash asociado a una instancia. Este método es útil para poder almacenar los objetos en tablas de hash, como en el caso de las colecciones del tipo java.util.Hashtable. Para implementar el código de este método hay que tener en cuenta las siguientes restricciones: - Dos objetos considerados iguales mediante el método equals() deben retornar el mismo código de hash. - Dos objetos no iguales deben retornar códigos de hash diferentes. - En ejecuciones distintas un mismo objeto puede retornar códigos de hash distintos. Continuando con el ejemplo de la clase Cliente, la reescritura de este método puede ser como sigue:

class Cliente {

. . .

public int hashCode() {

return 43 * 7 + (this.nombre != null ? this.nombre.hashCode() : 0);

}

}

Si se analiza el código vemos que el algoritmo se basa en el código de hash del campo nombre. Esto es así para que dos clientes con el mismo nombre retornen el mismo código de hash. (La clase String aplica un algoritmo de cálculo del código de hash basado en los códigos de sus caracteres.)

• public final native Class getClass() Se utiliza para obtener la clase de un objeto. Instancias de la clase Class representan las clases e interfaces que está ejecutando la aplicación Java. No hay un constructor para la clase Class, sus objetos son construidos automáticamente por la Máquina Virtual Java (JVM) cuando las clases son cargadas, o por llamadas al método defineClass() del cargador de clases. Habiendo determinado la clase del objeto, el método Class.newInstance() puede invocarse para instanciar objetos de un tipo utilizando su constructor por defecto. El resultado es el mismo que utilizando el operador new con un constructor de la clase.

• public String toString() Se usa para convertir todos los objetos conocidos por el compilador a algún tipo de representación de cadena, que dependerá del objeto. El código base retorna el nombre de la clase, una @ y un código de hash asociado al objeto.

• protected Object clone() throws CloneNotSupportedException Se usa para crear y retorna una copia del objeto. El significa de “copia” puede depender de la clase del objeto. Por lo general evaluarán a cierto las siguientes expresiones:

x.clone() != x

x.clone().getClass() == x.getClass()

x.clone().equals(x)

Al ser un método protegido, normalmente las clases que quieran clonarse deberán reescribirlo y gestionar la excepción que lanza. Además la clase deberá implementar la interfaz Cloneable, porque sino se lanzará una excepción del tipo CloneNotSupportedException. Un ejemplo de rescritura de clone() es el siguiente:

class Cliente implements Cloneable {

private String nombre;

. . .

public Cliente clone() {

try {

return (Cliente) super.clone();

} catch (CloneNotSupportedException ex) {

return null;

}

}

}

El método clone() realiza una clonación superficial; es decir, no clona recursivamente los campos internos, sólo asigna una copia de los valores. En el ejemplo previo, el campo nombre de una instancia Cliente no será clonado; el nombre de la instancia original y de la instancia clonada referenciará el mismo string.

3.7. Polimorfismo.

Cuando manejamos jerarquías de clases los objetos pueden tratarse de una forma general o individualizada al mismo tiempo.

Page 63: Fundamentos de JAVA

Fundamentos de Java /63

Por ejemplo, en una jerarquía Cliente-Persona-Empresa, donde Persona y Empresa son dos subclases de Cliente, un objeto de la clase Empresa puede ser tratado también como un objeto Cliente. abstract class Cliente {

protected String nombre;

public Cliente(String nombre) {

this.nombre=nombre;

}

. . .

public abstract String identificador ();

}

class Persona extends Cliente {

private String nif;

public Persona(String nombre, String nif) {

super(nombre);

this.nif = nif;

}

. . .

public String identificador () {

return “El NIF de “ + nombre + “ es “ + nif;

}

}

class Empresa extends Cliente {

private String cif;

public Persona(String nombre, String cif) {

super(nombre);

this.cif = cif;

}

. . .

public String identificador () {

return “El CIF de “ + nombre + “ es “ + cif;;

}

}

...........

// Creamos un Vector para almacenar clientes

Vector<Cliente> lisCli = new Vector<Cliente>();

//El primer cliente será de tipo Persona. Al asignarlo se produce un moldeo implícito a Cliente

lisCli.add( new Persona(“Juan”, “23232323P”) );

//El segundo cliente será de tipo Empresa. También se moldea implícitamente a Cliente

lisCli.add( new Empresa(“Rocas S.A.”, “F232323232323”) );

// Imprimo los identificadores de los clientes

for (Cliente cliente : lisCli) {

System.out.println( cliente.identificador() );

}

...........

En este ejemplo, los dos objetos creados son tratados bajo dos puntos de vista. El primer objeto del vector es creado como Persona pero después será manipulado como Cliente. Cuando se invoca el método identificador() a través de la variable cliente, el compilador tiene en cuenta el tipo de la variable (en este caso Cliente) para determinar si se puede invocar el método. Sin embargo, durante la ejecución se tiene en cuenta el tipo real del objeto referenciado por la variable cliente (que puede ser Persona o Empresa) y se invoca el método reescrito correspondiente al objeto.

4. Interfaces.

En la programación orientada a objetos, surge la necesidad de definir funcionalidades y atributos comunes para clases relacionadas, teniendo que delegar la implementación de las funcionalidades y los atributos en cada clase. Un buen ejemplo de esto surge en una aplicación de dibujo de formas, como rectángulos y triángulos. Las clases usadas para definir estas formas necesitan tener funcionalidades comunes como un método dibujar y una propiedad area. Para conseguir esto, podemos usar una interfaz como Dibujable para definir la

Page 64: Fundamentos de JAVA

Fundamentos de Java /64

funcionalidad de dibujo y sus atributos, como area, ancho, alto, y color. Sin embargo, podemos dejar la implementación de la interfaz a las clases Rectangulo y Triangulo, puesto que el área se calcula de una forma distinta en cada caso. A fin de cuentas, con una interfaz podemos definir el comportamiento (aquellos aspectos abstractos) al que deben sujetarse las clases que la implementan. En el ejemplo anterior, tanto un Rectangulo como un Triangulo serán dos clases de objetos que pueden ser dibujados y que se caracterizarán por tener un área, una anchura, una altura y un color. La interfaz como tal nunca contendrá instrucciones de código, excepto la declaración de variables comunes, propiedades y métodos. Un beneficio muy importante que las interfaces proporcionan es la multiherencia. Una clase sólo puede heredar como máximo de otra clase, pero sí puede implementar varias interfaces. Por ejemplo, necesitamos que la clase Rectangulo implemente la interfaz Dibujable, pero también nos puede interesar que implemente una interfaz Imprimible.

4.1. ¿Qué son las interfaces?

Una interfaz parece una clase, pero carece de implementación de código. Una interfaz contiene sólo definiciones de constantes compartidas, métodos y propiedades. La razón por la que una interfaz proporciona sólo definiciones es porque deben ser implementadas por clases, las cuales proporcionan el código para cada miembro definido en la interfaz.

4.1.1. Sintaxis. La sintaxis básica para definir una interfaz es: interface NombreInterfaz {

. . .

}

La interfaz representa un contrato. Una clase que la implementa debe implementar todos los aspectos de la interfaz exactamente como es definida. 4.1.2. Interfaces vs. herencia. Hay situaciones en las cuales se puede usar tanto una interfaz como la herencia de clases. Deberíamos usar herencia cuando queremos proporcionar una implementación base que podamos rescribir en clases derivadas. Una interfaz deja toda la implementación en manos del programador y simplemente se asegura que el contrato es satisfecho. Así, las interfaces son usadas cuando no tenemos la opción de usar herencia, porque un objeto ya hereda de una clase base específica. 4.1.3. Interfaces vs. abstracción. Los métodos abstractos son útiles cuando se quiere que cada implementación de la clase parezca y funcione igual, pero necesita que se cree una nueva clase para utilizar los métodos abstractos. Los interfaces proporcionan un mecanismo para abstraer los métodos a un nivel superior, lo que permite simular la herencia múltiple de otros lenguajes. Un interfaz sublima el concepto de clase abstracta hasta su grado más alto. Un interfaz podrá verse simplemente como una forma, es como un molde, solamente permite declarar nombres de métodos, listas de argumentos, tipos de retorno y adicionalmente miembros datos (los cuales podrán ser únicamente tipos básicos y serán tomados como constantes en tiempo de compilación, es decir, static y final). Un interfaz contiene una colección de métodos que se implementan en otro lugar. Los métodos de una clase son public, static y final. La principal diferencia entre interface y abstract es que un interfaz proporciona un mecanismo de encapsulación de los protocolos de los métodos sin forzar al usuario a utilizar la herencia. 4.1.4. Beneficios de las interfaces. Las interfaces son lo más útil para aplicar la arquitectura “plug-and-play”, donde los componentes pueden ser intercambiados cuando sea necesario. Podemos implementar la misma interfaz en estos componentes y usarlos de manera intercambiable sin ningún esfuerzo suplementario de programación. La interfaz fuerza a cada componente a proporcionar unos miembros específicos públicos que podemos usar según las exigencias.

Page 65: Fundamentos de JAVA

Fundamentos de Java /65

La siguiente lista describe algunos beneficios de las interfaces: • Desacoplamiento. Podemos usar las interfaces para desacoplar las aplicaciones de la implementación específica de clases. En nuestra aplicación, podemos referirnos a un tipo de interfaz en vez de una clase. De este modo, nuestra aplicación puede trabajar con cualquier clase que implemente la interfaz. • Gestión de la dependencia. Podemos usar las interfaces para reducir las dependencias de nuestras aplicaciones. Las interfaces normalmente son más estables que las clases que las implementan. Al referenciar interfaces en vez de clases, las aplicaciones son más estables porque las interfaces cambian menos que la implementación de las clases. Por ejemplo, imaginemos que tenemos una librería que es llamada por varias aplicaciones cliente. Podemos querer actualizar la librería, pero debemos asegurarnos de que sus cambios no generen incompatibilidades con las aplicaciones cliente. Para evitar esto, creamos una interfaz que describa la implementación existente dentro de la librería, y nos aseguramos de que la nueva librería implemente la nueva interfaz. • Implementación de varias interfaces. Una clase puede implementar varias interfaces simultáneamente. La clase debe implementar todos los miembros de cada una de las interfaces que implementa. (Recordemos que una clase no puede heredar de varias superclases.)

4.2. Cómo crear una interfaz

Una interfaz especifica un grupo de miembros relacionados que pueden ser implementados por clases. 4.2.1. Definición de una interfaz. El siguiente código muestra cómo definir la interfaz Dibujable. interface Dibujable {

int area() ;

}

Esta interfaz define un método llamado area que retorna un valor entero. Podemos observar que este método no tiene un bloque de código que deba ejecutar. Por convención, los nombres de interfaces se derivan de verbos, mientras que los nombres de clases se derivan de sustantivos. No es que sea necesario, pero si conveniente. Java proporciona muchas interfaces para varios propósitos. Algunas de las más comunes son Comparable, Comparator, Serializable, y Externalizable. 4.2.2. Miembros de una interfaz. Las interfaces permiten declarar dos tipos de miembros: variables y métodos. Por definición, los miembros de una interfaz son siempre públicos. Aunque no se explicite, todas las variables definidas en una interfaz son invariables (final) y compartidas (static). Al ser definidas como constantes, debe asignárseles un valor en su declaración. interface MiInterfaz {

int x = 6; // CORRECTO

String s ; // INCORRECTO

}

Los métodos se declaran sólo con su firma y un punto y coma (;). No deben ir seguidos de un bloque de código entre llaves. Por definición, un método de una interfaz no puede ser abstracto ni final. 4.2.3. Implementación de las interfaces. El siguiente código muestra cómo la interfaz Dibujable es implementada por la clase Rectangulo. La clase Rectangulo proporciona el código para el método area definido en la interfaz. Es importante que la implementación del método tenga la misma firma, tipo de retorno y nombre del método. Cualquier diferencia respecto a lo definido en la interfaz generará un error de compilación. class Rectangulo implements Dibujable

private int ancho;

private int alto;

public Rectangulo( int ancho, int alto) {

this. ancho = ancho

this.alto = alto

}

public int area() {

return ancho * alto;

}

}

Page 66: Fundamentos de JAVA

Fundamentos de Java /66

4.2.4. Implementación de varias interfaces. El siguiente código muestra la implementación por parte de una clase de varias interfaces. La clase Rectangulo implementa las interfaces Dibujable y Comparable. Como resultado, la clase Rectangulo proporciona una implementación para el método area() de Dibujable y el método compareTo() de Comparable. interface Dibujable {

int area();

}

class Rectangulo implements Dibujable, Comparable {

private int ancho;

private int alto;

public Rectangulo (int ancho, int alto) {

this.ancho = ancho;

this.alto = alto;

}

public int area() {

return ancho * alto;

}

public int compareTo (Object o) {

int miArea = this.area();

int suArea = ((Rectangulo) o).area();

if (miArea<suArea)

return -1;

else if (miArea==suArea)

return 0;

else

return 1;

}

}

4.2.5. Jerarquías de interfaces. Podemos aplicar herencia a las interfaces de la misma forma que se aplica a las clases mediante la palabra clave extends. Sin embargo, se establecen algunas diferencias:

• Las jerarquías de interfaces son independientes de las jerarquías de clases. • Una interfaz puede extender a varias interfaces. • Las interfaces no extienden a otra por defecto, como pasa con Object para las clases. • Una interfaz no puede extender a una clase. • Un interfaz hereda todas las constantes y métodos de sus super-interfaces, excepto si el interfaz oculta una constante con otra del mismo nombre, o si re declara un método con una nueva declaración de ese método. • Una clase que implementa una interfaz debe implementar los miembros definidos en ésta, y los miembros definidos en las super-interfaces que ésta extiende.

En el siguiente ejemplo se muestra la definición completa de un interfaz, declaración y cuerpo. public interface MiInterfaz extends InterfazA, InterfazB {

public final double PI = 3.14159;

public final int entero = 125;

void put( int dato );

int get();

}

El cuerpo del interfaz contiene las declaraciones de los métodos, que terminan en un punto y coma y no contienen código alguno en su cuerpo. Todos los métodos declarados en un interfaz son implícitamente, public y abstract, y no se permite el uso de transient, volatile, private, protected o syncronized en la declaración de miembros en un interfaz. En el cuerpo del interfaz se pueden definir constantes, que serán implícitamente public, static y final. 4.2.6. Consideraciones. Como resumen de las interfaces podemos resaltar que:

- No se pueden crear directamente objetos del tipo interfaz. (No poseen constructores.) - Cualquier método declarado en una interfaz es por definición público. - Cualquier variable declarada en una interfaz es por definición pública, final y estática.

Page 67: Fundamentos de JAVA

Fundamentos de Java /67

- La jerarquía de una interfaz es independiente de la jerarquía de clases que implementen dicha interfaz. - En cuanto al polimorfismo, una interfaz se comporta como superclase de las clases que la implementan.

4.3. Clases e interfaces internas.

Una clase interna es una clase definida dentro de otra clase, llamada clase contenedora. Esto no implica que la clase interna herede de la clase contenedora. Se pueden dar cuatro tipos de clases internas: estáticas, miembro, locales y anónimas. 4.3.1. Clases e interfaces internas static. Se crean dentro de otra clase al máximo nivel. class ClaseContendora {

. . .

static class ClaseInterna {

. . .

}

static interface InterfazInterna {

. . .

}

. . .

}

También es posible definir clases e interfaces internas static dentro de una interfaz contenedora: interface InterfazContenedora {

class ClaseInterna {

...

}

interface InterfazInterna {

. . .

}

}

Por definición las clases e interfaces internas de una interfaz son estáticas. Por regla, todas las interfaces internas (tanto en clases como en interfaces contenedoras) son implícitamente estáticas. En cierta forma, las clases internas static se comportan como clases normales en un paquete; para utilizar su nombre desde fuera de la clase contenedora hay que precederlo por el nombre de la clase contenedora y el operador punto (.). // Para instanciar una clase interna estática

ClaseContenedora.ClaseInterna cc = new ClaseContenedora.ClaseInterna();

InterfazContenedora.ClaseInterna cc = new InterfazContenedora.ClaseInterna();

Pueden utilizarse los modificadores final, public, private o protected para controlar el acceso a ellas. Las clases internas static pueden ver y utilizar directamente los miembros estáticos de la clase contenedora, pero no así los miembros no estáticos. 4.3.2. Clases internas miembro. Se crean dentro de otra clase al máximo nivel sin la palabra static. class ClaseContendora {

// . . .

class ClaseInterna {

// . . .

}

}

Se llaman clases internas miembro o simplemente clases internas, no existiendo interfaces internas de este tipo. Las clases internas no pueden tener variables miembro estáticas. Pueden ser public, private o protected. Incorporan una nueva sintaxis para las palabras this, new y super:

- En la clase interna, this referencia al objeto de la propia clase interna. Para acceder al objeto de la clase contenedora se utiliza ClaseContenedora.this (donde ClaseContenedora es el nombre de la clase contenedora). - Para crear un nuevo objeto de la clase interna se puede utilizar new, precedido por la referencia al objeto de la clase contenedora. Por ejemplo:

ClaseContendora objCont = new ClaseContenedora();

ClaseInterna objInt = objCont.new ClaseInterna();

Page 68: Fundamentos de JAVA

Fundamentos de Java /68

- En subclases que hereden de una clase interna se referencia la clase base con ref.super(). Cada objeto de la clase interna existe siempre dentro de un y sólo un objeto de la clase contenedora. Según esto, podemos establecer las siguientes relaciones entre la clase interna y la contenedora:

- Los métodos de la clase interna ven directamente las variables miembro del objeto contenedor. - Los métodos de la clase contenedora no ven directamente las variables miembro de la clase interna: necesitan cualificarlos con una referencia a los correspondientes objetos. - Otras clases NO pueden utilizar directamente los objetos de la clase interna, sin cualificarlos con el objeto o nombre de la clase contenedora. Por ejemplo, debemos usar este código en otra clase:

ClaseContenedora cc = new ClaseContenedora();

ClaseContenedora.ClaseInterna ci = cc.new ClaseInterna();

Respecto a los permisos de acceso: - Las clases internas, además de public, pueden ser también private y protected. - Los métodos de la clase interna acceden directamente a todos los miembros de la clase contenedora (incluso los privados). - También la clase contenedora puede acceder, si dispone de una referencia, a todas la variables miembro de sus clases internas (tanto públicas como privadas). - Una clase interna puede también acceder a los miembros (incluso privados) de otras clases internas definidas en la misma clase contenedora.

4.3.3. Clases internas locales. Se crean dentro de un bloque de código de la clase contenedora (normalmente en un método). Por ello sólo pueden ser usadas dentro del bloque de código. class ClaseContendora {

. . .

void metodoContenedor ( ) {

class ClaseInterna {

. . .

}

}

}

Las principales características de las clases locales son: - Como las variables locales, estas clases sólo son visibles y utilizables en el bloque de código en el que están definidas. - Las clases locales tiene acceso a todas las variables miembro y métodos de la clase contenedora. - Las clases locales pueden utilizar las variables y parámetros de métodos visibles en ese bloque de código, pero sólo si son final (en realidad la clase local trabaja con sus copias de las variables locales y por eso exige que sean final y no puedan cambiar). - Un objeto de la clase interna local sólo puede existir en relación con un objeto de la clase contenedora. - La palabra this se puede utilizar de la misma forma que en las clases internas miembro, pero no las palabra new y super.

Restricciones en el uso de las clases interna locales: - No pueden tener el mismo nombre que ninguna de sus clases contenedoras. - No pueden definir variables, métodos y clases estáticos. - No pueden ser declaradas public, protected, private o package, pues su visibilidad es siempre como la de las variables locales.

4.3.4. Clases anónimas. Son similares a las clases internas locales pero no tienen nombre. En las clases locales primero se define la clase y luego se crean uno más objetos. En las clases anónimas se unen estos dos pasos: como la clase no tiene nombre sólo se puede instanciar una vez, ya que además no permite definir constructores. Se usan con el operador new para crear un único objeto de ellas. Normalmente se utilizan para crear subclases que gestionan eventos de la interfaz gráfica de usuario. Ejemplo de clase anónima (en negritas): . . .

unObjet.addActionListener ( new ActionListener() {

public void actionPerformed(ActionEvent e) {

. . .

}

Page 69: Fundamentos de JAVA

Fundamentos de Java /69

} );

. . .

Formas de definir una clase anónima: - Extendiendo una clase. Requieren el operador new seguido del nombre de la clase de la que hereda y la definición de la clase anónima entre llaves. En nombre de la súper clase puede ir seguido de argumentos para invocar uno de los constructores heredados.

java.util.Date d = new java.util.Date(); // Asigna la fecha y hora actuales en la variable 'd'

// Creo una clase anónima que extiende 'Date' para reescribir el método 'toString'

// y retornar la fecha en formato 'año-mes-día'.

Date obj = new Date( d.getTime() ) {

public String toString() {

return getYear() + "-" + getMonth() + "-" + getDay();

}

};

- Implementando una interfaz. Requieren el operador new seguido del nombre de la interfaz que implementa (sin implements) y la definición de la clase anónima entre llaves. En este caso la clase anónima deriva de Object por defecto. El nombre de la interfaz va seguido de paréntesis vacíos, pues el constructor de Object no tiene argumentos.

java.util.Comparator ordenInverso = new java.util.Comparator() {

public int compare(Object o1, Object o2) {

return (– compare(o1, o2));

}

}

5. Gestión de errores y excepciones en Java

5.1. Introducción.

Un error o excepción es un evento que ocurre durante la ejecución de un programa y detiene el flujo normal de la secuencia de instrucciones de ese programa. Cuando se produzca una condición excepcional en nuestro programa, deberíamos generar o lanzar una excepción. Las excepciones y errores son objetos que derivan de la clase java.lang.Throwable.

La clase Throwable tiene dos subclases: Error y Exception. • Un Error indica que se ha producido un fallo no recuperable o no esperado. • Una Exception indica una condición anormal esperable que puede ser subsanada para evitar la terminación de la ejecución del programa.

Para capturar y procesar errores y excepciones se utiliza la estructura try/catch/finally: try {

// código que puede generar un error o excepción

} catch ( TipoException1 e ) {

// código para tratar la excepción de tipo TipoException1

} catch ( Exception e ) {

// código para tratar cualquier tipo de excepción

} catch ( Error e ) {

// código para tratar cualquier tipo de error

} finally {

// código que se ejecuta siempre (con o sin excepción)

}

Throwable

Exception Error

ArithmeticException RuntimeException . . .

Page 70: Fundamentos de JAVA

Fundamentos de Java /70

Cuando se lanza un error o excepción dentro del bloque try, el flujo del programa es derivado al primer bloque catch que defina un tipo de error o excepción compatible con el que se ha generado. El bloque finally garantiza que se ejecutará un bloque de código incondicionalmente.

5.2. Excepciones (clase «Exception»).

La clase java.lang.Exception y sus subclases indican condiciones de error que son previsibles, y que por tanto una aplicación razonable debería gestionar. Hay varias subclases de la clase Exception ya predefinidas, y cada una de ellas, a su vez, tiene numerosas subclases. 5.2.1. Generar excepciones en Java. Las excepciones pueden originarse de dos modos:

• El programa hace algo ilegal (caso normal). Como ejemplo, el siguiente método lanza una excepción si el segundo argumento pasado en un cero.

int divide (int x, int y) {

return x/y; // si y==0 se genera una excepción de división por cero (ArithmeticException)

}

• El programa explícitamente genera una excepción ejecutando la sentencia throw. Como ejemplo, el siguiente método lanza una excepción si su argumento es nulo.

void miMetodo (String s) {

if (s==null)

throw new NullPointerException(“No se admite un argumento nulo”);

}

Todas las excepciones pueden llevan un mensaje asociado a ellas al que se puede acceder utilizando el método getMessage(), que presentará un mensaje describiendo el error o la excepción que se ha producido. 5.2.2. Gestión de excepciones. Las excepciones pueden gestionarse de dos formas:

• Capturándolas mediante un bloque try/catch. Como ejemplo, el siguiente método retorna la longitud de un string, y si el string es nulo retorna el valor -1.

public int longitud(String s) {

try {

return s.length(); // si s==null se lanza una excepción de uso de objeto nulo

} catch (NullPointerException ex) {

return -1;

}

}

• Relanzándola a través del método donde se produce la excepción. Para que un método pueda relanzar excepciones explícitamente hay que indicarlo expresamente con la palabra throws seguido de los tipos de excepciones que lanza. Como ejemplo, se reescribe el método longitud() para relanzar la excepción.

public int longitud(String s) throws Exception {

if (s==null) // si s==null se relanza la excepción

throw new Exception("No se admite un argumento nulo");

return s.length();

}

Las excepciones de la clase RuntimeException o cualquiera de sus subclases son las únicas a las que el compilador no obliga ser gestionadas mediante un bloque try/catch o mediante throws. Todos los demás tipos de excepciones generan un error de compilación si se lanzan con throw y no son gestionadas en el código. 5.2.3. Excepciones predefinidas. Las siguientes son las clases de excepciones predefinidas más frecuentes que se pueden encontrar. Todas ellas derivan directamente de la clase Exception y se encuentran en el paquete java.lang.

• ArithmeticException. Las excepciones aritméticas son típicamente el resultado de división por 0. • NullPointerException. Se produce cuando se intenta acceder a una variable o método antes de ser definido. • IncompatibleClassChangeException. El intento de cambiar una clase afectada por referencias en otros objetos, específicamente cuando esos objetos todavía no han sido recompilados. • ClassCastException. El intento de convertir un objeto a otra clase que no es válida. • NegativeArraySizeException. Puede ocurrir si hay un error aritmético al cambiar el tamaño de un array.

Page 71: Fundamentos de JAVA

Fundamentos de Java /71

• OutOfMemoryException. ¡No debería producirse nunca! El intento de crear un objeto con el operador new ha fallado por falta de memoria. • NoClassDefFoundException. Se referenció una clase que el sistema es incapaz de encontrar. • ArrayIndexOutOfBoundsException. Es la excepción que más frecuentemente se produce. Se genera al intentar acceder a un elemento de un array más allá de los límites definidos inicialmente para ese array. • UnsatisfiedLinkException. Se hizo el intento de acceder a un método nativo que no existe. • InternalException. Este error se reserva para eventos que no deberían ocurrir. Por definición, el usuario nunca debería ver este error y esta excepción no debería lanzarse. • RuntimeException. Es la superclase de las excepciones que pueden ser lanzadas durante las operaciones normales de la máquina virtual de Java. Un método no requiere relanzar explícitamente excepciones de esta clase o de cualquiera de sus subclases.

5.2.4. Crear excepciones personalizadas. El programador puede lanzar sus propias excepciones extendiendo la clase Exception o alguna de sus subclases. Por ejemplo, considérese un programa cliente/servidor. El código cliente se intenta conectar al servidor, y durante 5 segundos se espera a que conteste el servidor. Si el servidor no responde, el servidor lanzaría la excepción de time-out (tiempo agotado): // Defino un nuevo tipo de excepción con un mensaje personalizado

class ServerTimeOutException extends Exception {

public ServerTimeOutException() {

super("Se ha agotado el tiempo de intento de conexión");

}

}

// Método que relanza la excepción anterior o cualquier otra que se produzca

public void conecta (String server) throws Exception {

int exito = open (server,80); // me conecto al servidor

if( exito == -1 ) // si no hubo conexión lanzo mi excepción

throw new ServerTimeOutException();

}

5.3. Errores (clase «Error»).

La clase java.lang.Error es una subclase de Throwable que indica un serio problema que una aplicación razonable no debería capturar. La mayoría de los errores son condiciones anormales. Por ejemplo, el error ThreadDeath es lanzado cuando un hilo de ejecución es abortado abruptamente por el sistema operativo, y la mayoría de aplicaciones no deberían intentar capturarla. 5.3.1. Gestión de errores. Aunque un error puede ser lanzado y gestionado de la misma forma que se hace con una excepción, el compilador no requiere relanzar los errores mediante la cláusula throws ni capturarlos mediante un bloque try/catch.

5.4. Aseveraciones (Assertions).

5.4.1. Introducción. Se denomina aseveración (assert) a una instrucción que contiene una expresión booleana que el programador sabe que en un momento dado de la ejecución del programa se debe evaluar a verdadero. Verificando esta expresión booleana el sistema comprueba que el programa se ejecuta dentro de los límites que el programador le marca y además reduce la posibilidad de errores. 5.4.2. Uso de «assert». Para declarar una aseveración en una clase Java se usa la palabra clave assert, que tiene las siguientes sintaxis: assert expresion1 ;

assert expresión1 : expresión2 ;

En cualquiera de los dos casos, expresión1 tiene que ser una expresión booleana o se producirá un error de compilación. Cuando se evalúa una aseveración que sólo tenga expresión1 se comprueba la veracidad de la expresión y si es verdadera se continúa la ejecución del programa, pero si es falsa se lanza una excepción de tipo AssertionError. Si la aseveración contiene además una expresión2 (que no puede ser un tipo primitivo, y debe evaluarse como no null) y expresión1 es falsa, se evalúa expresion2 y se le pasa como parámetro al constructor del AssertionError, para ser desplegado como información adicional al momento de ejecución.

Page 72: Fundamentos de JAVA

Fundamentos de Java /72

5.4.3. Compilación. Para que el compilador del JDK (javac) anterior a la versión 1.4 entienda la nueva instrucción assert, debe usarse el parámetro «-source 1.4», por ejemplo: javac -source 1.4 pragrama.java

Esto le indica al compilador que el código que recibe utiliza características del lenguaje aparecidas en la versión 1.4 del JDK. 5.4.4. Activación/Desactivación de las aseveraciones. Las aseveraciones están pensadas para la comprobación de invariantes (condiciones que se cumplen siempre en un determinado trozo de código), por lo que tienen más interés en la etapa de desarrollo y depuración. Por esto se puede desactivar y activar la comprobación de las aseveraciones. Por defecto la comprobación está desactivada y se proporcionan dos opciones para el intérprete del JDK (java).

• java -enableassertions (ó -ea), para activar la comprobación. • java -disableassertions (ó -da), para desactivar la comprobación.

Si estos modificadores se escriben tal cual, se activará o desactivará la comprobación de aseveraciones para la clase que se pretende ejecutar. Pero si lo que se quiere es activar/desactivar la comprobación de las aseveraciones en un determinado paquete o en una determinada clase: java -enableassertions:saludos.Hola... HolaMundo

(Activa aseveraciones en el paquete saludos, por los puntos ... ) java -enableassertions:saludos.Hola HolaMundo

(Activa aseveraciones de la clase saludos.Hola, porque no lleva puntos) Y lo mismo para disable: java -disablessertions:saludos.Hola... HolaMundo

java -disableassertions:saludos.Hola HolaMundo

También se puede activar para unos y desactivar para otros: java -ea:camion.Rueda... -da:camion.Freno camion.Conducir

En resumen, la sintaxis es la siguiente: java [-enableassertions | -ea] [:<package name>"..." | :<class name>]

java [-disableassertions | -da] [:<package name>"..." | :<class name>]

5.4.5. Ejemplos de uso. Hasta ahora, cualquiera que programase en Java y supiese que alguna condición debía ser cierta en alguna parte del código (el invariante) lo indicaba con un comentario:

if (a==1) {

...

} else if (a==2) {

...

} else { //nunca debe ser a==3

...

}

Aquí es donde se puede aplicar la nueva instrucción assert, (y en general para cualquier invariante): if (a==1){

...

} else if (a==2) {

...

} else {

assert (a==3);

...

}

De esta manera conseguimos proteger el else. Si se entra por el else y la aseveración no se cumple, se genera una excepción de tipo AssertionError. Otro candidato para las aseveraciones es una sentencia switch que no tenga cláusula default. En este caso el aserto comprobará que nunca se entra por el default, de esta manera:

switch (suerte) {

case Moneda.CARA:

...

return;

case Moneda.CRUZ:

...

Page 73: Fundamentos de JAVA

Fundamentos de Java /73

return;

case default:

assert false;

}

Es decir, deberíamos usar assert false en cualquier lugar del programa donde se supone que no se debe entrar nunca. En cuanto al mal uso de las aseveraciones, se da el caso de que el siguiente código producirá un error de compilación en la última línea:

while (true) {

assert false;

}

assert false; // error de compilación

5.4.6. Conclusiones. Como conclusiones, las aseveraciones deben usarse:

- Como una característica de depuración dinámica. - Para realizar verificaciones de integridad que prueben condiciones que deberían ser siempre true y que indiquen algunos errores de programación si no se cumplen.

Las aseveraciones no deberían ser usadas: - Para operaciones normales de tiempo de ejecución. - En la validación de los parámetros de un método. - Para forzar precondiciones sobre métodos públicos.

Como regla de oro, el código debería trabajar correctamente siempre si las aseveraciones no están activadas.

Page 74: Fundamentos de JAVA

Fundamentos de Java /74

IV - CLASES ÚTILES DE JAVA

El kit de desarrollo de Java proporciona muchas clases predefinidas que realizan tareas habituales en un programa, o bien que facilitan la interacción con el sistema subyacente.

1. Colecciones clásicas.

1.1. Introducción.

El paquete java.util proporciona clases para almacenar objetos en forma de listas o colecciones: Vector, BitSet (o vector de bits), Stack y Hashtable. Estas colecciones se redimensionan automáticamente y permiten especificar el tipo de elementos que contendrán. Estas colecciones no permiten elementos de tipos primitivos; de esta manera, si se desea crear una colección de números enteros, éstos deberán almacenarse usando la clase Integer y no el tipo int. La sintaxis para crear una instancia de estas colecciones es la siguiente: tipo_de_coleccion miColeccion = new tipo_de_coleccion <tipo_de_los_elementos> ()

donde entre < y > se indica el tipo de los elementos, permitiendo así crear colecciones genéricas. Si no se tipa una colección se asume por defecto que los elementos serán de tipo Object. Son métodos comunes a todas estas colecciones:

• elements() para obtener una Enumeration, que permite recorrer la colección. • size() que retorna el número de elementos • toString() que obtiene una representación en forma de string.

1.2. Tipos genéricos.

Una de las mejoras más significantes que tiene el lenguaje Java a partir de la versión 5 es la introducción de los tipos genéricos. 1.2.1. Tipado de clases aplicando genéricos. Los tipos genéricos permiten especificar el tipo de elemento de una colección en tiempo de compilación. Para ello solo tenemos que escribir inmediatamente después de la clase entre los signos < y > el tipo que van a tener los elementos de la estructura, así quedaría algo como: List<String> agenda = new ArrayList<String>();

agenda.add("Pedro");

El código anterior declara una lista de tipo ArrayList indicando que los elementos deben ser de tipo String. Al tipar una colección, automáticamente el compilador adapta los métodos de la colección al tipo indicado. De esta manera, el método get(), tal como se muestra en la siguiente instrucción String persona = agenda.get(0);

retorna el primer elemento de la colección como una objeto de tipo String, y por tanto no hace falta realizar un moldeo o casting. Además, las listas tipadas admiten el polimorfismo en sus elementos; es decir, un ArrayList<Persona> admite un elemento de tipo Amigo, si Amigo extiende a Persona. 1.2.2. Moldeo y polimorfismo aplicando genéricos. Hay que tener cuidado con el moldeo entre colecciones tipadas. Un ArrayList<Amigo> no admite moldearse a un ArrayList<Persona>, aunque Amigo extienda de Persona. class Persona {}

class Amigo extends Persona() { }

List<Persona> personas;

List<Amigo> amigos;

amigos = personas; // ERROR DE COMPILACIÓN

Podemos ampliar este concepto usando el símbolo ? y extends o super para crear polimorfismo. Por ejemplo, un ArrayList<? extends Persona> admite cualquier subclase de Persona, incluida Amigo, y ArrayList<? super Amigo> admite cualquier superclase de Amigo, incluida Persona. List<? extends Persona> varios;

varios = personas; // CORRECTO

varios = amigos; // CORRECTO

personas = varios; // INCORRECTO

amigos = varios; // INCORRECTO

También podemos usar ?, extends y super sobre interfaces en vez de sobre clases.

Page 75: Fundamentos de JAVA

Fundamentos de Java /75

1.2.3. Conversión de cualquier clase o interfaz a genérica. Utilizando esta sintaxis se puede convertir cualquier clase o interfaz en genérica. Por ejemplo: public class Alquiler<T> {

private List<T> inventario;

public Alquiler(List<T> inventario) {

this.inventario = inventario;

}

public T alquiler() {

return inventario.get(0);

}

public void devolver(T o) {

inventario.add(o);

}

}

Con esto le decimos a la clase que es una clase genérica, que dependiendo del tipo <T> que reciba como argumento en el constructor debe adaptar sus métodos y atributos. Por tanto, para construir una clase que gestione el alquiler de bicicletas tan solo deberíamos llamar al constructor con el tipo Bicicleta como parámetro: List<Bicicleta> inventario = new ArrayList<Bicicleta>();

. . .

Alquiler<Bicicleta> alquiler = new Alquiler<Bicicleta>(inventario);

Un ejemplo de interfaz genérica puede ser la siguiente: public interface Emparejable<T, M> {

void inserta(T uno, M otro);

}

El código precedente nos permitiría crear clases capaces de emparejar dos objetos de diversos tipos. Por ejemplo, una implementación de esta interfaz podría ser la siguiente: public class Emparejador<T, M> implements Emparejable<T, M> {

private HashMap<T, M> mapa = new HashMap<T, M>();

public void inserta(T uno, M otro) {

mapa.put(uno, otro);

}

}

. . .

Emparejador<String, Integer> parejas = new Emparejador<String, Integer>();

parejas.inserta("uno", 1);

. . .

También podemos ampliar el tipado a clases que extiendan a otra clase o implementen una interfaz. Por ejemplo: class Comparador<E extends Number>{

public boolean compara(E a, E b) {

return a.equals(b);

}

}

Permite crear una clase para comparar sólo objetos que deriven de la clase Number. Hay que fijarse que en este contexto no se puede aplicar el signo ?; es decir, es incorrecta la sintaxis: class Comparador<? extends Number>

1.2.4. Aplicar genéricos sobre clases genéricas. Podemos ampliar el concepto de genéricos aplicándolo sobre tipos que extiendan a clases o interfaces genéricas. Por ejemplo, podemos declarar la siguiente clase: class Comparador<E extends Comparable<E>>{

private E referencia;

public Comparador(E referencia) {

this.referencia=referencia;

}

public boolean compara(E valor) {

return referencia.equals(valor);

Page 76: Fundamentos de JAVA

Fundamentos de Java /76

}

}

En este tipo de definiciones, el tipo genérico E de la clase debe coincidir con el genérico de la interfaz o clase genérica. Es decir, no sería válida la siguiente sintaxis: class Comparador<E extends Comparable<T>>

1.3. Interfaz «Enumeration».

La interfaz java.util.Enumeration declara dos métodos para recorrer una colección: • hasMoreElements(), que indica si hay más elementos o se ha llegado al final. • nextElement(), que devuelve el elemento actual y avanza al siguiente elemento.

Se puede obtener una enumeración de una colección invocando el método elements() de la colección. Un ejemplo para recorrer un vector de strings mediante una Enumeration, e imprimir su contenido por consola, es el siguiente: Vector<String> miColeccion = new Vector()

. . .

// Se añaden elementos al vector mediante al método add()

. . .

Enumeration <String> enum = miColeccion.elements();

while ( enum.hasMoreElements() ) {

System.out.println( enum.nextElement() );

}

. . .

1.4. Vectores.

Un vector es una colección secuencial de objetos, que puede crecer o reducirse, cuyos elementos pueden ser accedidos mediante un índice. 1.4.1. La clase «Vector». En Java los ventores se implementa mediante la clase java.util.Vector, la cual deriva de Object e implementa las interfaces Cloneable y Serializable. Cada vector intenta optimizar la gestión de su contenido manteniendo una capacidad y un incremento de capacidad. La capacidad es siempre mayor o igual que el número de elementos del vector, y se incrementa automáticamente si se añaden más elementos que los que permite la capacidad actual. Métodos de la clase Vector:

• Vector() • Vector(int capacidad) • Vector(int capacidad, int incremento)

Constructores. Donde capacidad indica la capacidad (número de elementos) inicial, e incremento es el incremento de capacidad que se produce automáticamente cuando el vector se llena.

• void addElement(Objet e) Añade un nuevo objeto al final del vector.

• boolean removeElement(Object e) Remueve el objeto e de la lista.

• void removeAllElements() Vacía el vector.

• Object clone() Retorna una copia del vector.

• void copyInto(Object a []) Copia el vector en el array a.

• void trimToSize() Ajusta la capacidad del vector a los elementos actuales.

• void setSize(int newSize) Establece una nueva capacidad.

• int capacity() Retorna la capacidad actual del vector.

• boolean isEmpty() Indica si el vector está vacío.

Page 77: Fundamentos de JAVA

Fundamentos de Java /77

• Enumeration elements() Retorna una enumeración con los elementos.

• boolean contains(Object e) Indica si contiene el elemento e.

• int indexOf(Object e, int index) Retorna el índice del elemento e a partir de la posición dada ó -1 si no existe.

• void insertElementAt(Object e, int index) Inserta e por delante de la posición dada.

• int lastIndexOf(Object e, int index) Retorna el índice del último elemento e a partir de la posición dada ó -1 si no existe.

• Object elementAt(int index) Retorna el elemento de índice dado.

• Object firstElement() Retorna el primer elemento.

• Object lastElement() Retorna el último elemento.

• void setElementAt(Object e, int index) Cambia el elemento de índice dado.

• void removeElementAt(int index) Remueve el elemento de índice dado.

• int size() Retorna el número de elementos actual.

1.4.2. Actualizaciones en un vector. Un vector representa una lista indexada cuyos elementos se ordenan según el orden en que se añaden, teniendo en cuenta que el primer elemento se asocia con el índice cero. Para insertar un nuevo elemento en un vector se utilizan los métodos add() y addElement(). Vector<String> vector = new Vector();

vector.add("Primer elemento");

vector.addElement("Segundo elemento");

Los nuevos elementos se añaden al final del vector, pero también podemos añadirlos en una posición determinada usando los métodos add() y insertElementAt(). vector.add(2, "Tercer elemento");

vector. insertElementAt("Antes del último elemento", 2);

Se puede quitar un elemento de un vector indicando su índice con el método removeElementAt(), o bien pasándolo como argumento del método removeElement(). String s = “un elemento”;

vector.addElement(s);

vector.removeElement(s);

vector.removeElementAt(0);

Para cambiar un elemento por otro se usa el método setElementAt(), indicando el índice del elemento a sustituir y el nuevo elemento. vector.setElementAt(0,"Nuevo valor en la posición cero");

1.4.3. Ordenación en un vector. Un vector sólo se ordena automáticamente según el orden en que se añaden sus elementos. Para provocar otro tipo de orden debe realizarse explícitamente mediante algún algoritmo. También se puede utilizar el método sort() de la clase Collections para ordenar un vector explícitamente por un criterio personalizado, siendo dicho criterio implementado mediante la interfaz Comparator. (Véanse capítulos posteriores sobre la interfaz Comparator y la clase Collections.) 1.4.4. Búsquedas en un vector. Los vectores sólo permiten búsquedas directas mediante el índice de cada elemento. Para ello se utilizan los métodos elementAt(índice) y get(índice), que retornan el elemento buscado o lanzan una excepción si el índice no se corresponde con ningún elemento. También se proporcionan dos métodos para encontrar el índice de un elemento: indexOf() y lastIndexOf().

Page 78: Fundamentos de JAVA

Fundamentos de Java /78

Para personalizar una búsqueda se puede utilizar el método binarySearch() de la clase Collections, siendo el criterio de búsqueda implementado por la interfaz Comparator. (Véanse capítulos posteriores sobre la interfaz Comparator y la clase Collections.)

1.5. Conjunto de bits (clase «BitSet»).

Se llama así lo que en realidad es un vector de valores booleanos. Lo que ocurre es que está optimizado para su interpretación como bits. (Optimizado en cuanto a tamaño, porque en lo que respecta al tiempo de acceso a los elementos, es bastante más lento que el acceso a un array de elementos del mismo tipo básico.) El tamaño mínimo de un BitSet es de 64 bits. Es decir, que si se está almacenando cualquier otra cosa menor, por ejemplo de 8 bits, se estará desperdiciando espacio. Podemos crear un BitSet indicando el tamaño inicial de bits: BitSet bs = new BitSet(32);

Los bits contenidos se inicializan a valor false. Métodos de la clase BitSet:

• void flip(int indice) Asigna el bit del índice especificado a su valor complementario (si era true lo asigna a false y viceversa).

• void set(int indice) Asigna el bit del índice especificado a valor true.

• void clear(int indice) Asigna el bit del índice especificado a valor false.

• boolean get(int indice) Retorna el valor del bit del índice especificado.

• void and(BitSet set) Los bits del conjunto actual son modificados como resultado de hacer una operación AND lógica de bits con el argumento.

• void or(BitSet set) Los bits del conjunto actual son modificados como resultado de hacer una operación OR lógica de bits con el argumento.

• void xor(BitSet set) Los bits del conjunto actual son modificados como resultado de hacer una operación XOR lógica de bits con el argumento.

• void andNot(BitSet set) Los bits del conjunto actual son modificados como resultado de aplicar el argumento como máscara.

1.6. Pilas (clase «Stack»).

La clase Stack representa una pila, o colección de tipo LIFO (last-in, first-out). En este tipo de colecciones los elementos se guardan en el orden en que se introducen, y se quitan en el orden inverso; es decir, el último elemento que se coloque en la pila será el primero que se saque. La clase Stack hereda de Vector e incorpora métodos propios para implementar la funcionalidad de una pila:

• void push(objeto) para insertar un elemento en la cabeza de la pila. • Object pop() para retornar y retirar un objeto de la cabeza de la pila. • Object peek(), devuelve el elemento de la cabeza de la pila sin quitarlo. Si la pila está vacía se lanza una excepción.

1.7. Mapas o diccionarios.

Un mapa es una colección que almacena elementos asociados a una clave. 1.7.1. La clase «Hashtable». La clase Hashtable representa un mapa, un diccionario o un array asociativo compuestos de pares de elementos clave/valor. Este tipo de colecciones no permiten claves repetidas. Tampoco admite valores nulos en las claves y los valores. Una instancia de Hashtable tiene dos parámetros que afectan a su rendimiento: la capacidad inicial y el factor de carga. La capacidad es el número de cubos en la tabla de hash; cada uno de estos cubos puede almacenar varias entradas, las cuales son accedidas secuencialmente. El factor de carga es una medida que indica el tanto por ciento de capacidad que debe llenarse antes de que la tabla se redimensione.

Page 79: Fundamentos de JAVA

Fundamentos de Java /79

La clase a la que pertenezcan las claves debe rescribir los métodos hashCode() y equals() para garantizar así un valor único para cada clave, y el criterio de igualdad de las claves. El método hasdCode() debe devolver un entero único y distinto para cada clave. Métodos de la clase Hashtable:

• Hashtable() • Hashtable(int capacidad) • Hashtable(int capacidad, float factor)

Constructores. Se puede indicar la capacidad inicial y el factor de carga. • clear()

Vacía la tabla. • Object clone()

Retorna una copia de la tabla. • boolean contains(Object valor)

Indica si existe un elemento con el valor dado. • boolean containsKey(Object key)

Indica si existe la clave dada. • Enumeration elements()

Retorna una Enumeration con los valores. • Object get(Object clave)

Obtiene el valor correspondiente a una clave. • boolean isEmpty()

Indica si la tabla está vacía. • Enumeration keys()

Retorna una Enumeration con las claves. • Object put(Object clave, Object valor)

Añade un valor y lo asocia con una clave. Si la clave ya existe simplemente sustituye el valor. • rehash()

Amplia la capacidad de la tabla. • Object remove(Object clave)

Elimina el par clave-valor de la lista. • int size()

Retorna el número de elementos. 1.7.2. Actualizaciones en un mapa. Cada elemento de un mapa se compone de una clave única y un valor asociado. Para insertar un nuevo elemento en el mapa se utiliza el método put(). Hashtable<Integer, String> mapa = new Hashtable();

mapa.put(1, "Uno"); // se realiza una conversión implícita de 1 en un valor de tipo Integer.

mapa.put(new Integer(2), "Dos"); // se pasa el valor 2 como un objeto Integer.

Se puede quitar un elemento del mapa a través de su clave con el método remove(). mapa.remove(2);

Si usamos el método put() con una clave existente en el mapa, en vez de insertarse un nuevo elemento, se sustituye el valor del elemento de la clave dada y retorna el valor anterior. mapa.put(1,"Un"); // Se sustituye “Uno” por “Un”

1.7.3. Ordenación de un mapa. Un mapa Hashtable no garantiza ningún orden específico en las claves o los valores asociados. 1.7.4. Búsquedas en un mapa. Los mapas permiten buscar valores directamente mediante su clave asociada. String valor = mapa.get(1); // retorna el valor “Un”

Se utilizan los métodos constainsKey() y containsValue() para determinar respectivamente si el mapa contiene una clave o valor especificados. booelan sw = mapa.containsKey(1); // retorna true

sw = containsValue(“Tres”); // retorna false

Page 80: Fundamentos de JAVA

Fundamentos de Java /80

2. Nuevas colecciones.

2.1. Introducción.

Desde la versión 1.2 se incluyen nuevas librerías de colecciones que implementan las siguientes interfaces: • Interfaz Collection: define métodos para tratar una colección genérica de elementos. La implementan directamente las siguientes clases predefinidas:

- AbstractCollection, una clase abstracta que implementa las funcionalidades básicas de cualquier colección. • Interfaz List: define una lista que admite elementos repetidos y mantiene un orden inicial. Lo implementan las clases predefinidas:

- AbstractList, una clase abstracta que implementa las funcionalidades básicas de una lista. - ArrayList, una lista basada en un array. - LinkedList, una lista basada en una lista enlazada.

• Interfaz Queue: define métodos para tratar una colección como una cola de una dirección. Las clases predefinidas que la implementan son:

- AbstractQueue, una clase abstracta que implementa las funcionalidades básicas de una cola. - PriorityQueue, una cola que ordena sus elementos por una prioridad.

• Interfaz Deque: extiende la interfaz Queue para crear colas de dos direcciones. Las clases predefinidas que la implementan son:

- ArrayDeque, una cola doble basada en un array. - LinkedList, una lista implementada mediante una lista enlazada.

• Interfaz Set: define un conjunto, una colección que no permite elementos repetidos. Las clases predefinidas que la implementa son:

- AbstractSet, una clase abstracta que implementa las funcionalidades básicas de un conjunto. - HashSet: un conjunto basado en una tabla de hash. - LinkedHashSet, un conjunto basado en una combinación de tablas de hash y listas enlazadas.

• Interfaz SortedSet: define un conjunto cuyos elementos están ordenados según un criterio que debe establecerse. Lo implementan las clases predefinidas:

- TreeSet: un conjunto ordenado basado en un árbol binario. • Interfaz Map: define un conjunto de pares clave-valor, sin repetición de clave. Lo implementan las clases predefinidas:

- AbstractMap, una clase abstracta que implementa las funcionalidades básicas de un mapa. - HashMap: un mapa basado en una tabla hash. - IdentityHashMap, es igual que HashMap excepto en que establece la igualdad de las claves y los valores por las referencias y no por el contenido. - LinkedHashMap, un mapa basado en una combinación de tablas de hash y listas enlazadas. - Properties, es una extensión de Hashtable para soportar archivos de configuración y propiedades del sistema. - WeakHashMap, un mapa que supervisa el uso de sus elementos, eliminando aquellas entradas que referencian elementos descartados.

• Interfaz SortedMap: define un mapa cuyos elementos están ordenados por un criterio que debe establecerse. Lo implementan las clases predefinidas:

- TreeMap: un mapa ordenado basado en un árbol binario. Todas estas nuevas colecciones (HashSet, TreeSet, ArrayList, LinkedList, HashMap...), al igual que las clásicas, se integran en una jerarquía de clases e interfaces. Todas estas colecciones se redimensionan automáticamente y permiten especificar el tipo de elementos que contendrán.

Page 81: Fundamentos de JAVA

Fundamentos de Java /81

La sintaxis para crear una instancia de colecciones de tipo cola, lista y conjunto es la siguiente: TipoDeColeccion <TipoDeLosElementos> miColeccion = new TipoDeColeccion <TipoDeLosElementos> ()

La sintaxis para crear una instancia de colecciones de tipo mapa es la siguiente: TipoDeMapa <TipoClaves, TipoValores> miMapa = new TipoDeMapa <TipoClaves, TipoValores> ()

Donde entre < y > se indica el tipo de los elementos, permitiendo así tipar la colección. Si no se tipa se asume por defecto que los elementos serán de tipo Object.

2.2. Métodos comunes a las colecciones.

Las nuevas colecciones comparten los siguientes métodos: • boolean add( Object ), añade el argumento a la colección, si puede. • boolean addAll( Collection ), añade todos los elementos que se pasan en el argumento, si puede. • void clear(), elimina todos los elementos. • boolean contains( Object ), indica si existe el elemento pasado. • boolean isEmpty(), indica si la colección está vacía. • Iterator iterator(), devuelve un iterador para recorrer los elementos. • boolean remove( Object ), remueve el argumento si existe. • boolean removeAll( Collection ), remueve los elementos del argumento si existen. • boolean retainAll( Collection ), mantiene solamente los elementos que están contenidos en el argumento; es lo que sería una intersección en la teoría de conjuntos. • int size(), devuelve el número de elementos. • Object[] toArray(), devuelve un array conteniendo todos los elementos que forman parte de la colección.

2.3. Recorrido de colecciones: interfaces «Iterator» y «ListIterator».

La interfaz java.util.Iterator es la equivalente a la interfaz Enumeration para recorrer las nuevas colecciones de forma secuencial hacia delante. La interfaz java.util.ListIterator es una extensión Iterator para recorrer la colección en ambos sentidos. La interfaz java.util.Iterator proporciona los métodos:

• hasNext(), que indica si hay más elementos o se ha llegado al final. • next(), que devuelve el elemento actual y avanza al siguiente elemento. • remove(), para eliminar el último elemento accedido por next().

La interfaz java.util.ListIterator añade los métodos: • add(elemento), añade un nuevo elemento a la iteración. • hasPrevious(), indica si hay un elemento previo o se ha llegado al principio. • nextIndex(), retorna el índice del siguiente elemento.

Collection Iterator Map

ListIterator List Set AbstractMap

HashMap

TreeMap

HashTable AbstractCollection

AbstractList AbstractSet

HashSet

Vector AbstractSequentialList ArrayList

Stack LinkedList

Collections

Arrays

Comparable

Comparator

TreeSet

Queue

Deque

AbstractQueu

ArrayDeque

PriorityQueue

LinkedHashMap

IdentityHashMap

WeakHashMap

Page 82: Fundamentos de JAVA

Fundamentos de Java /82

• previous(), devuelve el elemento actual y retrocede al anterior elemento. • previousIndex(), retorna el índice del anterior elemento. • set(elemento), reemplaza el elemento retornado por next() o previous() por otro.

Ejemplo para recorrer un ArrayList de strings mediante un iterador, e imprimir su contenido por consola. ArryList miColeccion = new ArrayList <String> ()

. . .

Iterator <String> it = miColeccion.iterator();

while ( it.hasNext() ) {

System.out.println( it.next() );

}

El bucle for( : ) utilizado para recorrer colecciones indexadas encapsula el uso de un iterador, siendo un método preferible al uso de un iterador. El código anterior se puede sustituir por: ArryList miColeccion = new ArrayList <String> ()

. . .

for ( String s : miColeccion ) {

System.out.println( s );

}

2.4. Ordenación de colecciones: interfaces «Comparable» y «Comparator».

Java proporciona dos interfaces para poder ordenar las colecciones de forma implícita (si los elementos implementan Comparable) o de forma explícita (creando un objeto que implemente Comparator). 2.4.1. Interfaz «Comparable». La interfaz java.lang.Comparable permite ordenar automáticamente los elementos de las colecciones que permiten ordenación (como TreeSet y TreeMap). Ya está implementada por todas las clases predefinidas (String, Date, Integer, Double, etc.) y declara el método int compareTo (Object obj);

que compara el objeto implícito this con el argumento obj. Debe ser reescrito para retornar un valor negativo si this es anterior a obj, 0 si son iguales, y un valor positivo si this es posterior a obj. Son los elementos quienes deben implementar esta interfaz, de forma que su método equals() posea el mismo criterio de igualdad que compareTo(). Las colecciones del tipo TreeSet y TreeMap rechazarán la inserción de elementos repetidos si éstos implementan la interfaz Comparable. Se puede tipar esta interfaz para adaptar su método compareTo() a un tipo de dato determinado. Por ejemplo: public class MiClase implements Comparable<MiClase> {

public int compareTo (MiClase o) {

return ... ;

}

En el siguiente ejemplo se declara la clase Empleado, estableciendo la ordenación de empleados según su nif. public class Empleado implements Comparable<Empleado> {

private String nif;

private String nombre;

public Empleado (String nif, String nombre) { // CONTRUCTOR

this.nif = nif;

this.nombre = nombre;

}

public int compareTo (Empleado e) { // MÉTODO DEFINIDO EN Comparable

return this.nif.compareTo(e.nif);

}

public boolean equals (Object e) { // MÉTODO HEREDADO DE Object

return this.compareTo( (Empleado) e );

}

public static void main (String [] args) {

TreeSet<Empleado> empleados = new TreeSet();

// Se añaden los elementos desordenados

empleados.add( new Empleado("22222222B", "Luis") );

empleados.addnew Empleado("11111111A", "Antonio") );

// El conjunto "empleados" ordena automáticamente sus elementos Empleado según su nif.

Page 83: Fundamentos de JAVA

Fundamentos de Java /83

// El primer empleado del conjunto será Luis y el segundo Antonio.

empleados.addnew Empleado("11111111A", "José") ); // FALLA LA INSERCIÓN

// El empleado José no es añadido al conjunto porque se considera que es igual al empleado Antonio

// porque coinciden sus nif‟s.

}

}

2.4.2. Interfaz «Comparator». La interfaz java.util.Comparator se utiliza para derivar objetos que establezcan un criterio de ordenación para cualquier tipo de colección. Declara el método int compare (Object obj1, Object obj2);

que compara obj1 con obj2 y que debe retornar un valor negativo, cero o un valor positivo, con el mismo significado que el establecido para el método compareTo() de la interfaz Comparable. Al igual que la interfaz Comparable, también se puede tipar Comparator para adaptar su método compare() a un tipo de dato determinado. Por ejemplo: public class MiComparador implements Comparator<String> {

public int compare (String o1, String o2) {

return - o1.compareTo(o2) ; // establece una comparación en orden alfabético inverso

}

}

Podemos usar una instancia que implemente Comparator para establecer la ordenación de la colecciones de tipo TreeSet y TreeMap. Para ello se debe crear una clase que implemente Comparator, rescribir el método compare(), y pasar una instancia de dicha clase en el constructor de la colección. Por ejemplo: . . .

TreeSet<String> setInverso = new TreeSet( new MiComparador() );

setInverso.add("Juan");

setInverso.add("Antonio");

setInverso.add("Benito");

// El conjunto queda establecido con el orden: Juan, Benito, Antonio

Los métodos sort() y binarySearch() de la clase Collections también utilizan objetos Comparator como uno de sus argumentos para establecer ordenaciones y búsquedas.

2.5. Listas (interfaz «List»).

Una lista es una colección de elementos ordenados por el orden en que son añadidos. Se denominan colecciones indexadas porque cada elemento tiene asociado un índice que indica su posición en la lista. El índice 0 indica la primera posición, el índice 1 la segunda, etc. Hay dos implementaciones predefinidas de List: ArrayList y LinkedList; siendo normalmente ArrayList la elección por defecto. Una lista incorpora una serie de métodos a la colección que permiten la inserción y borrado de elementos en medio de la lista. Además, en las listas enlazadas (LinkedList) se puede generar un ListIterator para moverse a través de la lista en ambas direcciones. 2.5.1. Clase «ArrayList». Una ArrayList es una lista volcada en un array. Se debe utilizar en lugar de Vector como almacenamiento de objetos de propósito general. Permite un acceso aleatorio muy rápido a los elementos, pero realiza con bastante lentitud las operaciones de insertado y borrado de elementos en medio de la lista. Se puede utilizar un ListIterator para moverse hacia atrás y hacia delante en la lista, pero no para insertar y eliminar elementos. 2.5.2. Clase «LinkedList». Esta clase proporciona un óptimo acceso secuencial, permitiendo operaciones de inserción y borrado de elementos de en medio de la lista muy rápidas. Sin embargo, es bastante lento el acceso aleatorio en comparación con el ArrayList. Dispone además de los métodos addLast(), getFirst(), getLast(), removeFirst() y removeLast(), que no están definidos en ninguna interfaz o clase base y que permiten utilizar la lista enlazada como una pila, una cola o una cola doble. 2.5.3. Actualizaciones en una lista. Al igual que un vector, las listas se ordenan según el orden en que se añaden los elementos, teniendo en cuenta que el primer elemento se asocia con el índice cero. Para insertar un nuevo elemento en una lista se utiliza el métodos add(). List<String> lista = new ArrayList();

Page 84: Fundamentos de JAVA

Fundamentos de Java /84

lista.add("Primer elemento");

lista.add("Segundo elemento");

Los nuevos elementos se añaden al final de la lista, pero también podemos añadirlos en una posición determinada usando el método add(). lista.add(2, "Tercer elemento"); // inserta el elemento en la posición 2

Se puede quitar un elemento de una lista con el método remove(), pasando el objeto como argumento o bien pasando su índice. String s = “un elemento”;

lista.add(s);

lista.remove(s);

lista.remove(0);

Para cambiar un elemento por otro se usa el método set(), indicando el índice del elemento a sustituir y el nuevo elemento. lista.set(0,"Nuevo valor en la posición cero");

2.5.4. Ordenación en una lista. Una lista sólo se ordena automáticamente según el orden en que se añaden sus elementos. Para provocar otro tipo de orden debe realizarse explícitamente mediante algún algoritmo. También se puede utilizar el método sort() de la clase Collections para ordenar una lista explícitamente por un criterio personalizado, siendo dicho criterio implementado mediante la interfaz Comparator. Como ejemplo se define una lista de nombres y después se ordena en orden alfabético inverso. El criterio de ordenación se establece a través de un objeto Comparator que es creado mediante una clase anónima interna: List<String> nombres = new LinkedList();

nombres.add("Antonio");

nombres.add("Mario");

Collections.sort(nombres, new Comparator<String>() {

public int compare(String o1, String o2) {

return - o1.compareTo(o2);

}

});

// la lista queda ordenada por: Mario, Antonio

2.5.5. Búsquedas en una lista. Las listas sólo permiten búsquedas directas mediante el índice de cada elemento. Para ello se utiliza el método get(índice), que retorna el elemento buscado o lanza una excepción si el índice no se corresponde con ningún elemento. También se proporcionan dos métodos para encontrar el índice de un elemento: indexOf() y lastIndexOf(). Estos dos métodos establecen la búsqueda comparando los elementos de la lista con el objeto que se pasa por argumento. Para realizar la comparación utilizan el método equals() de los elementos; como este método, por defecto, compara por igualdad de referencia, si lo reescribimos podemos personalizar el criterio de búsqueda. Por ejemplo, el siguiente código define la clase Empleado por igualdad en su nif: public class Empleado {

private String nif;

private String nombre;

public Empleado (String nif, String nombre) { // CONSTRUCTOR

this.nif = nif;

this.nombre = nombre;

}

public boolean equals (Object e) { // Dos empleados son iguales si tienen el mismo nif

return this.nif.equals( ((Empleado) e).nif );

}

public static void main (String [] args) {

ArrayList<Empleado> empleados = new ArrayList();

empleados.add( new Empleado("11111111A", "Antonio") );

empleados.add( new Empleado("22222222B", "Luis") );

// Se busca un empleado con el nif "11111111A".

int indice = empleados.indexOf( new Empleado("11111111A", "Andrés" )

// se asigna indice = 0, puesto que el primer empleado de la lista cumple el criterio de igualdad por nif

Page 85: Fundamentos de JAVA

Fundamentos de Java /85

}

}

Para personalizar una búsqueda se puede utilizar el método binarySearch() de la clase Collections, siendo el criterio de búsqueda implementado por la interfaz Comparator. Por ejemplo, en una lista de objetos Empleado se puede crear un método de búsqueda que encuentre el primer empleado con un nombre dado. public class ListaEmpleados extends ArrayList<Empleado> {

public int buscaPorNombre (String nombre) {

int pos;

pos = Collections.binarySearch( this , new Empleado(0,nombre) , new Comparator<Empleado>() {

public int compare(Empleado o1, Empleado o2) {

return o1.getNombre().compareTo( o2.getNombre() );

}

});

return pos; // retorna el índice del elemento o un valor negativo si no lo encuentra

}

}

En el código anterior los parámetros del método binarySearch() son: primero la lista a ordenar (la constante predefinida this representa a la lista que llama al método de búsqueda), segundo un objeto Empleado para comparar en la búsqueda (como se compara por nombre, se crea un objeto empleado con el nombre pasado por parámetro en el método de búsqueda), y tercero un objeto Comparator con el criterio de comparación entre empleados.

2.6. Colas (interfaz «Queue»).

Una cola es una colección diseñada para introducir elementos y quitarlos en el orden en que fueron introducidos. La interfaz java.util.Queue proporciona operaciones adicionales de inserción, extracción e inspección sobre colas de una dirección. Cada uno de estos métodos existe en dos formas: una lanza una excepción si falla la operación, y otra retorna un valor especial (null o false) si falla la operación.

Lanza excepción Retorna un valor especial

Inserción add(e) offer(e)

Extracción remove() poll()

Inspección element() peek()

En una cola de una dirección los elementos se ordenan con la forma FIFO (el primero en entrar es el primero en salir). 2.6.1. La clase «PriorityQueue». La clase java.util.PriorityQueue implementa una cola lineal que ordena sus elementos por una prioridad. El primer elemento de la cola será el que tenga más prioridad y no necesariamente el primero que se insertó. El orden de prioridad se estable de acuerdo al criterio establecido por Comparable en los elementos, o por un objeto Comparator pasado por argumento en el constructor. Este tipo de colas no admite elementos nulos. El iterador retornado por PriorityQueue.iterator() no garantiza que los elementos son recorridos en su orden de prioridad. Como alternativa podemos obtener Arrays.sort(pq.toArray()) para recorrer la cola pq en su orden de prioridad. 2.6.2. Colas de doble dirección (interfaz «Deque»). Una cola de una dirección no soporta la inserción y extracción de elementos por ambos. La interfaz java.util.Dequeu extiende a Queue y proporciona la funcionalidad para implementar colas de doble dirección. Los métodos proporcionados por este interfaz también tienen dos formas: los que lanzan excepciones y los que pueden retornar un valor especial (null o false).

Primer elemento (cabeza) Último elemento (pie)

Lanza excepción Valor especial Lanza excepción Valor especial

Inserción addFirst(e) offerFirst(e) addLast(e) offerLast(e)

Extracción removeFirst() pollFirst() removeLast() pollLast()

Inspección getFirst() peekFirst() getLast() peekLast()

Page 86: Fundamentos de JAVA

Fundamentos de Java /86

2.6.3. La clase «ArrayDeque». La clase java.util.ArrayDeque implementa una cola doble basada en un array. El array subyacente se redimensiona según las necesidades de capacidad. Esta clase no garantiza operaciones de hilos seguros y no admite elementos nulos. Ofrece mayor rapidez que Stack cuando se usa como una pila, y mejor rendimiento que LinkedList cuando se usa como una cola. 2.6.4. Actualizaciones en una cola. Para añadir un nuevo elemento en la cabeza de la cola se usan los métodos add() y offer(): Queue<String> cola = new LinkedList();

cola.add("Primer elemento"); // retorna false si no puede insertar y puede lanzar una excepción

cola.offer("Segundo elemento"); // retorna false si no pudo insertar

En una cola doble se pueden utilizar métodos adicionales para insertar elementos: Deqeue<String> cola = new ArrayDeque();

cola.addFirst("nuevo"); // añade un nuevo elemento por la cabeza de la cola

cola.offerFirst("nuevo"); // añade un nuevo elemento por la cabeza de la cola

cola.addLast("nuevo"); // añade un nuevo elemento por el pie de la cola

cola.offerLast("nuevo"); // añade un nuevo elemento por el pie de la cola

Se puede quitar un elemento de la cabeza de una cola con los método remove() y poll(). String s1 = cola.remove(); // quita el primer elemento de la cola y lo retorna. Puede generar excepción

String s2 = cola.poll(); // quita el siguiente elemento de la cola y lo retorna. Puede retornar null

En una cola doble se pueden utilizar métodos adicionales para extraer elementos: String s1 = cola.removeFirst(); // extrae el primer elemento de la cabeza de la cola

String s2 = cola.poolFirst(); // extrae el primer elemento de la cabeza de la cola

String s3 = cola.removeLast(); // extrae el primer elemento del pie de la cola

String s4 = cola.poolLast(); // extrae el primer elemento del pie de la cola

En una cola doble existen además tres métodos para extraer elementos interiores: boolean b1 = cola.removeFirstOcurrence("e"); // extrae la primer ocurrencia de "e"

boolean b2 = cola.removeFirstOcurrence("e"); // extrae la primer ocurrencia de "e"

boolean b3 = cola.removeLastOcurrence("e"); // extrae la última ocurrencia de "e"

Tras cada operación remove() o poll() válida la cola pasa a tener un elemento menos. Para obtener el primer elemento de la cola lineal sin quitarlo se utilizan los métodos element() y peek(). String s = cola.peek(); // retorna el primer elemento sin quitarlo

En una cola doble se pueden utilizar métodos adicionales para inspeccionar elementos: String s1 = cola.getFirst(); // retorna el primer elemento de la cabeza de la cola sin quitarlo

String s2 = cola.peekFirst(); // retorna el primer elemento de la cabeza de la cola sin quitarlo

String s3 = cola.getLast(); // retorna el primer elemento del pie de la cola sin quitarlo

String s4 = cola.peekLast(); // retorna el primer elemento del pie de la cola sin quitarlo

2.6.5. Ordenación en una cola. Las colas se ordena normalmente por el orden en que se insertan los elementos, excepto en las colas de tipo PriorityQueue que estable un orden por prioridad. Como ejemplo, la siguiente cola de strings da prioridad a sus elementos por el número de caracteres que contienen. PriorityQueue<String> palabras = new PriorityQueue(10, new Comparator<String>() {

public int compare(String o1, String o2) {

int len1 = o1==null? 0 : o1.length();

int len2 = o2==null? 0 : o2.length();

return len2-len1;

}

});

nombres.add("1");

nombres.add("12");

System.out.println(palabras.peek()); // se imprime "12"

2.7. Conjuntos: interfaz «Set».

Un conjunto es una colección compuesta por elementos del mismo tipo que no admite elementos repetidos. Para establecer el criterio de igualdad entre elementos deben rescribirse los métodos equals() y hashcode() de la clase de los elementos para indicar cuándo dos elementos son iguales y para asociar un código de hash con

Page 87: Fundamentos de JAVA

Fundamentos de Java /87

cada elemento. Set tiene la mismo interfaz que Collection y no garantiza el orden en que se encuentren almacenados los objetos que contenga. 2.7.1. Clase «HashSet». La clase java.util.HashSet es la implementación más habitual para un conjunto, excepto para conjuntos que sean muy pequeños. Organiza internamente los elementos usando tablas de hash, y por tanto la clase de los elementos debe tener reescrito el método hashCode(), de forma que elemento equivalentes tenga asociado un mismo código de hash y elementos no equivalentes tengan un código de hash distinto. No garantiza el orden en que están almacenados los elementos, y además permite un elemento con valor null. Esta clase ofrece un rendimiento constante para operaciones básicas (add, remove, contains y size). Iterar a través de este conjunto requiere un tiempo proporcional al número de elementos y a la capacidad de la colección. Por ello es muy importante no asignar una capacidad inicial muy grande si el rendimiento en el recorrido del conjunto es importante. 2.7.2. Clase «LinkedHashSet». La clase java.util.LinkedHashSet implementa conjuntos mediante tablas de hash y listas enlazadas con un orden de iteración predecible. Difiere de HashSet en que mantiene una lista doblemente enlazada a través de todas sus entradas. Esta lista enlazada define el orden de iteración, que coincide con el orden en que son insertados los elementos. Podemos usar esta clase para producir una copia de un conjunto que tenga el mismo orden que el original: Set s = . . .

Set copia = new LinkedHashSet( s );

. . .

Esta técnica es útil si un módulo recibe un conjunto como dato de entrada, lo copia, y más tarde debe retornar resultados cuyo orden esté determinado por la copia. Esta clase proporciona todas las operaciones de conjuntos y permite elementos nulos. Dos parámetros afectan al rendimiento de este tipo de conjuntos: la capacidad inicial y el factor de carga. Esta implementación no está sincronizada. 2.7.3. Clase «TreeSet». La clase TreeSet es una implementación de un conjunto ordenado por el que se puede navegar, basada en un TreeMap. Los elementos se ordenan usando el criterio establecido por la interfaz Comparable en la clase de los elementos, o bien proporcionando un objeto Comparator en el constructor del conjunto. El orden establecido tanto por Comparable como por Comparator debe ser coherente con el método equals() de la clase de los elementos. Esta implementación garantiza un costo de tiempo logarítmico para operaciones básicas (añadir, eliminar y buscar). Esta implementación no está sincronizada. 2.7.4. Actualizaciones en un conjunto. Para añadir un nuevo elemento a un conjunto se utiliza el métodos add(). HashSet<String> textos= new HahsSet();

textos.add("Primer elemento");

textos.add("Segundo elemento");

Si un elemento a insertar ya existe en el conjunto (según el criterio de igualdad establecido por equals()), el método add() retorna false y no inserta el nuevo elemento. La clase HashSet no garantiza ningún orden de inserción, mientras que la clase TreeSet lo garantiza. Se puede quitar un elemento de una colección con el método remove(), pasando el objeto como argumento. String s = “un elemento”;

textos.add(s);

textos.remove(s);

El método remove() retorna true si el elemento fue encontrado y quitado, y false si no se encontraba en el conjunto. 2.7.5. Ordenación en un conjunto. Sólo la clase TreeSet admite un orden en la inserción de elementos. El orden se establece mediante el uso de la interfaz Comparable en la clase de los elementos, o el uso de un objeto Comparator pasado en el constructor de la colección. Como ejemplo se define un conjunto de nombres ordenados en orden alfabético inverso. El criterio de ordenación se establece a través de un objeto Comparator que es creado mediante una clase anónima interna: TreeSet<String> nombres = new TreeSet( new Comparator<String>() {

Page 88: Fundamentos de JAVA

Fundamentos de Java /88

public int compare(String o1, String o2) {

return - o1.compareTo(o2);

}

});

nombres.add("Antonio");

nombres.add("Mario");

// el conjunto queda ordenado por: Mario, Antonio

2.7.6. Búsquedas en un conjunto. La clase HashSet sólo permite averiguar si un elemento está en la colección o no. Para ello se usa el método contains(). Por ejemplo: HashSet<Integer> numeros = new HashSet();

numeros.add(1);

boolean esta = numeros.contains(1); // retorna el valor true

esta = numeros.contains(2); // retorna el valor false

La clase TreeSet además incluye los siguientes métodos para acceder a los elementos: • first(), retorna el primer elemento del conjunto. • last(), retorna el último elemento del conjunto. • lower(e), retorna el elemento más grande del conjunto que sea estrictamente menor que el argumento e, o null si no lo hay. • floor(e), retorna el elemento más grande del conjunto que sea menor o igual que el argumento e, o null si no lo hay. • ceiling(e), retorna el elemento más pequeño del conjunto que sea mayor o igual que el argumento e, o null si no lo hay. • higher(e), retorna el elemento más pequeño del conjunto que sea estrictamente mayor que el argumento e, o null si no lo hay. • pollFirst(), retorna y elimina el primer elemento del conjunto, o retorna null si no hay elementos. • pollLast(), retorna y elimina el último elemento del conjunto, o retorna null si no hay elementos.

2.8. Mapas (interfaz «Map»).

Los mapas almacenan información en base a parejas de datos, formados por una clave y el valor que corresponde a esa clave. Mantiene las asociaciones de pares clave-valor de forma que se puede encontrar cualquier valor a partir de la clave correspondiente. Los elementos de un mapa son objetos del tipo Map.Entry creados a partir de la clave y su valor asociado. 2.8.1. Clase «HashMap». La clase HashMap es una implementación de un mapa basada en una tabla de hash. Proporciona todas las operaciones opcionales y permite valores nulos y una clave nula. (La clase HashMap es realmente equivalente a la clase Hashtable, excepto en que no está sincronizada y que permite nulos.) Esta clase no garantiza el orden del mapa. Proporciona un rendimiento muy constante a la hora de insertar (put) y localizar elementos (get); aunque este rendimiento se puede ajustar a través de los constructores que permiten fijar la capacidad y el factor de carga de la tabla de hash. El tiempo de acceso en las iteraciones sobre la colección es proporcional a la capacidad de la misma más el número de elementos. Por ello es aconsejable no asignar una capacidad muy grande si el rendimiento de las iteraciones es importante. 2.8.2. La clase «IdentityHashMap». La clase java.util.IdentityHashMap implementa un mapa con una tabla de hash usando igualdad por referencia cuando compara las claves (y los valores). Es decir, dos claves k1 y k2 son iguales si k1==k2. Por tanto no utiliza el método equals() como hacen todas las demás implementaciones de los mapas. Esta clase proporcional todas las operaciones de los mapas y permite claves y valores nulos. No garantiza ningún orden en el mapa, ni que permanezca constante el orden de los elementos durante su vida. Proporciona un rendimiento constante en las operaciones básicas (get y put). No es sincronizada en sus operaciones. 2.8.3. La clase «LinkedHashMap». La clase java.util.LinkedHashMap es una implementación de un mapa basada en una tabla de hash y una lista enlazada, proporcionando un orden de iteración predecible. Difiere de HashMap en que mantiene una lista

Page 89: Fundamentos de JAVA

Fundamentos de Java /89

doblemente enlazada a través de las entradas del mapa. El orden de iteración es el establecido por el orden de inserción de los elementos. Proporciona un constructor especial para crear una instancia con un orden de acceso específico (true para orden de acceso, false para orden de inserción): public LinkedHashMap(int capacidadInicial, float factorCarga, boolean ordenAcceso)

Esta clase proporciona todas operaciones de los mapas, y permite elementos nulos. Esta implementación no está sincronizada. 2.8.4. La clase «WeakHashMap». La clase java.util.WeakHashMap es una implementación de un mapa con claves perdidas. Una entrada en este mapa será automáticamente eliminada si la clave no es usada de forma ordinaria. Es decir, la presencia de una clave en el mapa no impide que el recolector de basura la reclame. Cuando la clave es liberada de memoria, la entrada correspondiente en el mapa desaparece. Es preferible usar esta clase con claves cuya igualdad esté establecida por las referencias. Esta clase soporta valores nulos en las claves y los valores. Su rendimiento es similar a un HashMap. Tampoco está sincronizada. 2.8.5. Clase «TreeMap». La clase java.util.TreeMap es una implementación navegable de un mapa basada en un árbol balanceado. El mapa se ordena de acuerdo al criterio determinado por la interfaz Comparable sobre las claves, o de acuerdo al criterio establecido por un objeto Comparator pasado como argumento en algún constructor. La implementación garantiza un tiempo de acceso logarítmico en las operaciones de los métodos containsKey(), get(), put() y remove(). Cuando se observan las claves o los valores, se comprueba que están colocados en un orden concreto, determinado por las interfaces Comparable o Comparator sobre la clase de las claves. Lo importante de un TreeMap es que se pueden recuperar los elementos en un determinado orden. TreeMap es el único mapa que define el método subMap(), que permite recuperar una parte del árbol solamente. 2.8.6. Actualizaciones en un mapa. Cada elemento de un mapa se compone de una clave única y un valor asociado. Para insertar un nuevo elemento en el mapa se utiliza el método put(). HashMap<Integer, String> mapa = new HashMap();

mapa.put(1, "Uno"); // se realiza una conversión implícita de 1 en un valor de tipo Integer.

mapa.put(new Integer(2), "Dos"); // se pasa el valor 2 como un objeto Integer.

Para un TreeMap, los nuevos elementos se ordenan automáticamente por la clave. Se puede quitar un elemento del mapa a través de su clave con el método remove(). mapa.remove(2);

Si usamos el método put() con una clave existente en el mapa, en vez de insertarse un nuevo elemento se sustituye el valor del elemento de la clave dada y retorna el antiguo valor. mapa.put(1,"Un"); // Se sustituye “Uno” por “Un”

2.8.7. Ordenación de un mapa. Sólo la clase TreeMap garantiza un orden automático por las claves basado en el criterio establecido por la interfaz Comparable de la clase de las claves, o por un objeto Comparator pasado como argumento a través de un constructor de la clase. El siguiente código crea un mapa de mensajes de texto asociados con una fecha de llegada, de forma que el último mensaje será siempre el primero del mapa. En este caso se establece el criterio de ordenación mediante un objeto Comparator que invierte la ordenación natural de las fechas. TreeMap<Date,String> mensajes = new TreeMap(new Comparator<Date>() {

public int compare(Date o1, Date o2) {

return - o1.compareTo(o2);

}

});

2.8.8. Búsquedas en un mapa. Los mapas permiten buscar valores directamente mediante su clave asociada. HashMap<Integer, String> mapa = new HashMap();

mapa.put(1, "Uno"); // se realiza una conversión implícita de 1 en un valor de tipo Integer.

String valor = mapa.get(1); // retorna el valor “Un”

Page 90: Fundamentos de JAVA

Fundamentos de Java /90

Se utilizan los métodos constainsKey() y containsValue() para determinar respectivamente si el mapa contiene una clave o valor especificados. booelan sw = mapa.containsKey(1); // retorna true

sw = containsValue(“Tres”); // retorna false

La clase TreeMap además incluye los siguientes métodos para acceder a los elementos: • firstKey(), retorna la primera clave del mapa. • lastKey(), retorna la última clave del mapa. • firstEntry(), retorna el primer elemento del mapa como un objeto Map.Entry. • lastEntry(), retorna el último elemento del mapa como un objeto Map.Entry. • pollFirstEntry(), retorna y elimina el primer elemento del mapa, o retorna null si no hay elementos. • lowerEntry(clave), retorna el elemento más grande del mapa cuya clave sea estrictamente menor que la clave pasada por argumento e, o null si no lo hay. • lowerKey(clave), retorna la clave más grande del mapa que sea estrictamente menor que la clave pasada por argumento e, o null si no la hay. • floorEntry(clave), retorna el elemento más grande del mapa cuya clave sea menor o igual que la clave pasada por argumento e, o null si no lo hay. • floorKey(clave), retorna la clave más grande del mapa que sea menor o igual que la clave pasada por argumento e, o null si no la hay. • ceillingEntry(clave), retorna el elemento más pequeño del mapa cuya clave sea mayor o igual que la clave pasada por argumento e, o null si no lo hay. • ceillingKey(clave), retorna la clave más pequeña del mapa que sea mayor o igual que la clave pasada por argumento e, o null si no la hay. • higherEntry(clave), retorna el elemento más pequeño del mapa cuya clave sea estrictamente mayor que la clave pasada por argumento e, o null si no lo hay. • higherKey(clave), retorna la clave más pequeña del mapa que sea estrictamente mayor que la clave pasada por argumento e, o null si no la hay.

2.9. Clase «Arrays».

La clase Arrays proporciona métodos estáticos para manipular colecciones como si fuesen arrays. Es posible convertir un array en una lista utilizando el método estático Arrays.asList(): private static String s[] = {"uno", "dos", "tres", "cuatro", "cinco"};

static List a = Arrays.asList( s );

static List a2 = Arrays.asList( new String[] { s[3] , s[4] , s[5] } );

Arrays.asList() genera una lista que está basada en un array de tamaño fijo; por lo tanto, solamente soporta aquellas operaciones que no alteran el tamaño del array. 2.9.1. Ordenación y búsqueda en arrays. La clase Arrays tiene los métodos sort() y binarySearch() sobrecargados para todos los tipos básicos de datos, incluidos String y Object que permiten ordenar y hacer búsquedas en un array. String [] s = {"B", "G", "A", "J", "C"};

Arrays.sort( s ); // el array queda ordenado "A", "B", "C", "G", "J"}

int pos = Arrays.binarySearch( s , "C" ); // pos = 2

2.9.2. Modificación de arrays. La clase Arrays tiene el método swap() sobrecargado para todos los tipos básicos de datos, incluidos String y Object que permiten intercambiar dos elementos. String [] s = {"B", "G", "A", "J", "C"};

Arrays.swap(s , 1, 2); // s = {"B", "A", "G", "J", "C"};

El método fill() también está sobrecargado y permite asignar un mismo valor a todos los elementos de un array. Arrays.fill(s , "X"); // s = {"X", "X", "X", "X", "X"};

2.10. Clase «Collections».

Esta clase define métodos estáticos para manipular listas y colecciones indexadas. Los más interesantes son: • static void sort ( List ) • static void sort ( List , Comparator )

Para ordenar una lista pasada como argumento. Si no se pasa como argumento un objeto Comparator, los elementos contenidos en la lista deben implementar Comparable.

Page 91: Fundamentos de JAVA

Fundamentos de Java /91

• static void shuffle ( List ) • static void shuffle ( List , Comparator )

Para desordenar de modo aleatorio una lista. Si no se pasa como argumento un objeto Comparator, los elementos contenidos en la lista deben implementar Comparable.

• static void reverse ( List ) Para invertir el orden de una lista.

• static int binarySearch ( List , Object ) • static int binarySearch ( List , Object , Comparator )

Busca un elemento y retorna su índice. Si no se pasa como argumento un objeto Comparator, los elementos contenidos en la lista deben implementar Comparable.

• static void copy ( List , List ) • static void fill ( List , Object )

Copia una lista en otra o reemplaza todos sus elementos por uno dado. • static Object max ( Collection ) • static Object max ( Collection , Comparator ) • static Object min ( Collection ) • static Object min ( Collection , Comparator )

Cálculo de máximos y mínimos. Si no se pasa como argumento un objeto Comparator, los elementos contenidos en la lista deben implementar Comparable.

• static Collection synchronizedCollection ( Collection ) • static List synchronizedList ( List ) • static Map synchronizedMap ( Map ) • static Set synchronizedSet ( Set ) • static SorteMap synchronizedSortedMap ( SortedMap ) • static SortedSet synchronizedSortedSet ( SortedSet )

Retornan una copia del argumento como clase sincronizada. El siguiente ejemplo crea una clase Persona, donde se establece un ordenamiento implícito por id. Se crea un ArrayList y después se fuerza su ordenación por id y por nombre. public class Persona implements Comparable<Persona> {

public int id;

public String nombre;

// Constructor

public Persona (int id, String nombre) {

this.id = id;

this.nombre = nombre;

}

// Se establece la comparación por id

public int compareTo(Persona o) {

return (id<o.id)? -1 : (id==o.id)? 0 : 1;

}

// Se establece igualdad por id

public boolean equals(Object o) {

return compareTo((Persona) o)==0;

}

}

. . .

// Se crea un ArryList de elementos Persona

ArrayList personas = new ArrayList <Persona> ()

. . .

// Se fuerza la ordenación implícita por id

Colecctions.sort(personas)

// Se fuerza la ordenación por nombre mediante un objeto Comparator

Colecctions.sort(personas, new Comparator<Persona>() {

public int compare (Persona o1, Persona o2) {

return (o1.nombre.compareTo( o2.nombre );

} });

Page 92: Fundamentos de JAVA

Fundamentos de Java /92

3. Clases envoltorio para los tipos primitivos.

Java declara clases envoltorio para cada unos de los tipos primitivos: Character, Byte, Short, Integer, Long, Float y Double. Todas las clases numéricas extienden la clase base Number. Las clases Double y Float permiten operar con valores de tipo infinito (obtenidos por una división por cero) o no definidos (cero dividido entre cero).

3.1. La clase «Character».

Al trabajar con caracteres se necesitan muchas funciones de comprobación y traslación. Estas funciones están disponibles en la clase Character. Para declarar un objeto Character que encapsule el caracter 'c'‟ se utiliza: Character c = new Character('c');

// o bien, podemos dejar que el compilador haga conversiones implícitas

c = 'c';

Funciones estáticas de comprobación son: Character.isLowerCase( char c) devuelve true si el carácter es una letra minúscula Character.isUpperCase( char c) devuelve true si el carácter es una letra mayúscula Character.isDigit( char c) devuelve true para caracteres numéricos Character.isSpace( char c) devuelve true para espacios en blanco

Funciones estáticas de traslaciones de caracteres son: Character.toLowerCase( char ) convierte entre mayúscula y minúscula Character.toUpperCase( char ) convierte entre minúscula y mayúscula

Funciones de traslaciones de carácter/dígito son: Character.digit( char c, int base ) retorna el código entero del caracter en la base indicada char c = Character.forDigit( int n, int base ) retorna el caracter correspondiente al código y base dadas

Otros métodos no estáticos de la clase Character son: Character.charValue() retorna el caracter como un char Character.toString() retorna el caracter como un String

3.2. La clase «Number».

La clase abstracta java.lang.Number es la base para crear subclases que encapsulan valores numéricos. Son clases derivadas BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, y Short. Cualquier otra subclase debe reescribir los siguientes métodos para retornar el valor numérico con el tipo especificado:

• abstract int intValue(), debe retornar el valor como un int, lo cual puede involucrar redondeo. • abstract long longValue(), debe retornar el valor como un long. • abstract float floatValue(), debe retornar el valor como un float. • abstract double doubleValue(), debe retornar el valor como un double. • byte byteValue(), retorna el valor como un byte. • short shortValue(), retorna el valor como un short.

3.3. La clase «Float».

La clase java.lang.Float extiende a Number y encapsula valores literales del tipo float. De la misma forma que con la clase Character, se han codificado muchas funciones útiles dentro de los métodos de la clase Float. Los constructores de esta clase son: Float (float f)

Float (double d)

Float (String s) // genera una excepción si el argumento no contiene un número válido

Valores estáticos de Float: Float.POSITIVE_INFINITY // representa el valor infinito positivo: +1/0

Float.NEGATIVE_INFINITY // representa el valor infinito negativo: -1/0

Float.NaN // representa el valor no definido: 0/0

Float.MAX_VALUE // representa el mayor valor de tipo float

Float.MIN_VALUE // representa el menor valor de tipo float

Conversiones con un string: String s = Float.toString( 3.14f ) // convierte un float en un string

String s = new Float(3.14).toString() // convierte un objeto Float en un string.

Page 93: Fundamentos de JAVA

Fundamentos de Java /93

float f = Float.parseOf( "3.14" ) // convierte un string en un float

Float f = Float.valueOf( "3.14" ) // convierte un string en un objeto Float

Funciones de comprobación: boolean b = Float.isNaN( f ) // comprueba si f es un Float con valor no definido ( 0/0 o raiz cuadrada de -2)

boolean b = Float.isInfinite( f ) // comprueba si f es un Float con valor infinito

Funciones de conversión del objeto (heredadas de Number): Float f = new Float( 3.14 )

String s = f.toString() // convierte a string

int i = f.intValue() // convierte a int

long l = f.longValue() // convierte a long

float ff = f.floatValue() // convierte a float

double d = f.doubleValue() // convierte a double

Otros métodos: int i = f.hashCode() // obtiene el código de hash como la representación entera de sus bits

boolean b = f.equals( new Object() ) // determina la igualdad por contenido

int i = Float.floatToIntBits( 5f ) // obtiene la repesentación int del formato IEEE 754 floating-point

float f = Float.intBitsToFloat( 5 ) // obtiene la repesentación float del formato IEEE 754 floating-point

3.4. La clase «Double».

La clase java.lang.Double también extiende a Number y es análoga a la clase Float, pero en este caso para encapsular valores literales de tipo double. Valores estáticos de Double: Double.POSITIVE_INFINITY // representa el valor infinito positivo: +1/0

Double.NEGATIVE_INFINITY // representa el valor infinito negativo: -1/0

Double.NaN // representa el valor no definido: 0/0

Double.MAX_VALUE // representa el mayor valor de tipo double

Double.MIN_VALUE // representa el menor valor de tipo double

Los constructores de esta clase son: Double (double d)

Double (String s) // genera una excepción si el argumento no contiene un número válido

Conversiones con un string: String s1 = Double.toString( 3.14 ) // convierte un double en un string

String s2 = new Double(3.14).toString() // convierte un objeto Double en un string.

double d = Double.parseOf( "3.14" ) // convierte un string en un double

Double d = Double.valueOf( "3.14" ) // convierte un string en un objeto Double

Funciones de comprobación: boolean b = Double.isNaN( d ); // comprueba si d es un Double con valor no definido

boolean b = Double.isInfinite( d ); // comprueba si d es un Double con valor infinito

Funciones de conversión del objeto (heredadas de Number): Double d = new Double (3.14)

String s = d.toString() // convierte a string

int i = d.intValue() // convierte a int

long l = d.longValue() // convierte a long

float f = d.floatValue() // convierte a float

double dd = d.doubleValue() // convierte a double

Otros métodos: int i = d.hashCode(); // obtiene el código de hash como la representación entera de sus bits

boolean b = d.equals( new Object() ) // determina la igualdad por contenido

long i = Double.doubleToLongBits(5d) // obtiene la repesentación long del formato IEEE 754 floating-point

double d = Float. longBitsToDouble(5L) // obtiene la repesentación double del formato IEEE 754 floating-point

3.5. La clase «Integer».

La clase java.lang.Integer también extiende a Number y encapsula valores literales del tipo int. Valores estáticos de Integer: Integer.MIN_VALUE; // representa el menor valor de tipo int

Integer.MAX_VALUE; // representa el mayor valor de tipo int

Los constructores de esta clase son: Integer (int i)

Page 94: Fundamentos de JAVA

Fundamentos de Java /94

Integer (String s) // genera una excepción si el argumento no contiene un número válido

Conversiones con un string: String s1 = Integer.toString(34) // convierte un int en un string en base 10

String s2 = Integer.toHexString(34) // convierte un int en un string en base 16

String s3 = Integer.toOctalString(34) // convierte un int en un string en base 8

String s4 = Integer.toBinaryString(34) // convierte un int en un string en base 2

String s2 = new Integer(34).toString() // convierte un objeto Integer en un string.

int i = Integer.parseOf( "34" ) // convierte un string en un int

Integer d = Integer.valueOf( "34" ) // convierte un string en un objeto Integer

Funciones de conversión del objeto (heredadas de Number): Integer i = new Integer (34)

String s = i.toString() // convierte a string

int ii = i.intValue() // convierte a int

long l = i.longValue() // convierte a long

float f = i.floatValue() // convierte a float

double d = i.doubleValue() // convierte a double

Otros métodos: int i = Integer.rotateLeft(34, 1) // rota 1 bit a la izquierda el valor 34 (Equivale a 34x2)

int i = Integer.rotateRight(34, 1) // rota 1 bits a la derecha el valor 34 (Equivale a 34/2)

int i = Integer.reverse(34) // invierte el orden de los bits en complemento a 2 (genera 1140850688)

int i = Integer.reverseBytes(34) // invierte el orden de los bytes en complemento a 2 (genera 570425344)

3.6. La clase «Long».

La clase java.lang.Long también extiende a Number y es análoga a la clase Integer, pero en este caso para encapsular valores literales de tipo long. Valores estáticos de Integer: Long.MIN_VALUE; // representa el menor valor de tipo long

Long.MAX_VALUE; // representa el mayor valor de tipo long

Los constructores de esta clase son: Long (long l)

Long (String s) // genera una excepción si el argumento no contiene un número válido

Conversiones con un string: String s1 = Long.toString(34) // convierte un long en un string en base 10

String s2 = Long.toHexString(34) // convierte un long en un string en base 16

String s3 = Long.toOctalString(34) // convierte un long en un string en base 8

String s4 = Long.toBinaryString(34) // convierte un long en un string en base 2

String s2 = new Long(34).toString() // convierte un objeto Long en un string.

long l = Long.parseOf( "34" ) // convierte un string en un long

Long l = Long.valueOf( "34" ) // convierte un string en un objeto Long

Funciones de conversión del objeto (heredadas de Number): Long l = new Long (34)

String s = l.toString() // convierte a string

int i = l.intValue() // convierte a int

long ll = l.longValue() // convierte a long

float f = l.floatValue() // convierte a float

double d = l.doubleValue() // convierte a double

Otros métodos: long l = Long.rotateLeft(34, 1) // rota 1 bit a la izquierda el valor 34 (Equivale a 34x2)

long l = Long.rotateRight(34, 1) // rota 1 bits a la derecha el valor 34 (Equivale a 34/2)

long l = Long.reverse(34) // invierte el orden de los bits (genera 4899916394579099648)

long l = Long.reverseBytes(34) // invierte el orden de los bytes (genera 2449958197289549824)

3.7. La clase «Boolean».

El tipo boolean también tienen su clase asociada java.lang.Boolean, aunque en este caso hay menos métodos implementados que para el resto de las clases envolventes. Valores estáticos de Boolean: Boolean.TRUE; // representa el valor cierto

Boolean.FALSE; // representa el valor falso

Page 95: Fundamentos de JAVA

Fundamentos de Java /95

Los constructores de esta clase son: Boolean (boolean b)

Boolean (String s) // acepta "true" y "True" para true, y cualquier otra cosa será false

Métodos de Boolean son: boolean b = Boolean.parseBoolean("true") // convierte un string en un boolean

boolean b = new Boolean().booleanValue() // convierte un string en un boolean

Boolean b = Boolean.valueOf("true") // convierte un string en un objeto Boolean

String s = Boolean.toString(true) // convierte un boolean en un string

boolean b = Boolean.getBoolean("true") // convierte un string en un objeto boolean

3.8. Precisión arbitraria (clases «BigDecimal» y «BigInteger»).

La inclusión de la conectividad con bases de datos SQL obligó a definir en Java una nueva forma de trabajar con datos SQL de tipo NUMERIC y DECIMAL, que requieren precisión absoluta. Como resultado surgieron las clases java.lang.BigDecimal y java.lang.BigInteger, que permiten trabajar con precisión arbitraria, para representar datos que son habituales de las Bases de Datos. La clase BigInteger también resulta útil cuando los tipos primitivos de Java (byte, int, long) no proporcionan suficiente precisión. Por ejemplo, cuando se está construyendo una clave pública para encriptación que involucra operaciones exponenciales muy grandes y manipular números primos de cientos de bits, esta clase es ideal para estas acciones El constructor más sencillo es el que construye un BigDecimal a partir de su representación como cadena: BigDecimal( String s );

Aunque la cadena no puede contener ningún signo no numérico, ni siquiera espacios en blanco, ya que de lo contrario se produce una excepción de tipo NumberFormatException. Un método interesante es el que permite fijar la precisión que se desea tener, es decir, el número de dígitos significativos que hay después de la coma decimal, y el tipo de redondeo que se va a utilizar: setScale( int precision, int redondeo );

Los operadores que se utilizan con estas clases no son los clásicos, sino que están disponibles métodos especiales para sumar, restar, multiplicar, dividir y para desplazar la coma decimal un número determinado de posiciones a derecha o izquierda: add( BigDecimal a );

substract( BigDecimal a );

multiply( BigDecimal a );

divide( BigDecimal a,int modo_redondeo );

movePointRight( int posiciones );

movePointLeft( int posiciones );

3.9. Empaquetado y desempaquetado automático.

Cuando el compilador necesita convertir un tipo primitivo a un tipo de la clase envolvente correspondiente aplica lo que se denomina un empaquetado automático (autoboxing) y en el proceso inverso un desempaquetado automático (unboxing). Por ejemplo, podemos fácilmente hacer lo siguiente: Integer i = new Integer(5) + 6; // se crea el objeto Integer(11) y se referencia mediante "i"

int j = i; // se asigna en "j" el valor entero 11

En la primera instrucción de este código, el tipo Integer es desempaquetado a int para la suma, y luego es vuelto a empaquetar a Integer para hacer la asignación. En la segunda instrucción, el objeto Integer es desempaquetado para hacer la asignación a int.

4. Operaciones matemáticas.

4.1. La clase «Math».

La clase java.lang.Math representa la librería matemática de Java. Las funciones que contiene son las que suelen aparecer en todos los demás lenguajes, estando aquí agrupadas como métodos estáticos. El constructor de la clase es privado, por lo que no se pueden crear instancias de la clase. Sin embargo, Math es public y final para que se pueda llamar desde cualquier sitio y sus métodos son static para que no haya que inicializarla. La clase Math proporciona las siguientes funciones y constantes matemáticas:

Math.abs( x ) para int, long, float y double, retorna el valor absoluto de x Math.sin( double a ) devuelve el seno del ángulo a en radianes

Page 96: Fundamentos de JAVA

Fundamentos de Java /96

Math.cos( double a ) devuelve el coseno del ángulo a en radianes Math.tan( double a ) devuelve la tangente del ángulo a en radianes Math.asin( double r ) devuelve el ángulo cuyo seno es r Math.acos( double r ) devuelve el ángulo cuyo coseno es r Math.atan( double r ) devuelve el ángulo cuya tangente es r Math.atan2(double a,double b) devuelve el ángulo cuya tangente es a/b Math.exp( double x ) devuelve e elevado a x Math.log( double x ) devuelve el logaritmo natural de x Math.sqrt( double x ) devuelve la raíz cuadrada de x Math.ceil( double a ) devuelve el número completo más pequeño mayor o igual que a Math.floor( double a ) devuelve el número completo más grande menor o igual que a Math.rint( double a ) devuelve el valor double truncado de a Math.pow( double x,double y ) devuelve y elevado a x Math.IEEEremainder(int a, int b) Calcula el resto de la división entera a/b Math.round( x ) para double y float, redondea el valor de x Math.random() devuelve un valor double generado aleatoriamente Math.max( a,b ) para int, long, float y double, retorna el máximo entre a y b Math.min( a,b ) para int, long, float y double, retorna el mínimo entre a y b Math.E para la base exponencial, aproximadamente 2.72 Math.PI para PI, aproximadamente 3.14 toDegrees(dobule x) Pasa de radianes a grados toRadians(double x) Pasa de grados a radianes

4.2. Valores aleatorios (clase «Random»).

Para obtener valores seudo-aleatorios se puede utilizar el método Math.random() o bien la clase java.util.Random. La clase Random proporciona métodos para obtener valores aleatorios en diversos formatos. Sus métodos son no estáticos, y por tanto debemos instanciar siempre esta clase. Los métodos más usados de la clase Random son:

Random()

Random(long semilla) Constructor que utiliza una semilla distinta en cada invocación Constructor que usa una semilla determinada

boolean nextBoolean() Retorna un valor booleano aleatorio int nextInt ()

long nextLong () Retorna un valor entero corto aleatorio Retorna un valor entero largo aleatorio

int nextInt (int n) Retorna un entero aleatorio entre 0 y n-1 double nextDouble ()

float nextFloat () Retornan un valor real entre 0 (inclusive) y 1 (exclusive) Retornan un valor real entre 0 (inclusive) y 1 (exclusive)

void nextBytes (byte [] bytes) Genera bytes al azar y los asigna en el array bytes Si dos instancias de Random son creadas con la misma semilla, y ambas instancias invocan los mismos métodos, entonces se generarán idénticas secuencias de valores. En el siguiente código de ejemplo se obtiene un número aleatorio entero entre 0 y 23 inclusive: Random r = new Random();

int numeroAleatorio = r.nexInt(24);

5. Manipulación de secuencias de caracteres.

5.1. La interfaz «CharSequence».

Un objeto que implementa la interfaz java.lang.CharSequence es una secuencia legible de caracteres. Esta interfaz proporciona una forma uniforme y estándar de acceder a la lectura de muchas clases de secuencias de caracteres. Los métodos que declara son:

• int length(), debe retornar la longitud de la secuencia de caracteres. • char charAt(int índice), debe retornar el caracter de una posición dada contando desde cero. • CharSequence subSequence(int inicio, int fin), debe retorna una subsecuencia. • String toString(), debe retornar la secuencia de caracteres como un string.

A modo de ejemplo, el siguiente código crea una secuencia de caracteres sobre un string subyacente.

Page 97: Fundamentos de JAVA

Fundamentos de Java /97

class TextoCharSequence implements CharSequence {

private String texto; // El contenido de la secuencia

// Constructor para asignar el contenido de la secuencia

public TextoCharSequence(String texto) {

this.texto = texto;

}

// Método de la interfaz que debe retornar la longitud del contenido

public int length() {

return texto.length();

}

// Método de la interfaz que debe retornar el caracter de posición dada

public char charAt(int index) {

return texto.charAt(index);

}

// Método de la interfaz que debe retornar la subsecuencia especificada

public CharSequence subSequence(int start, int end) {

return texto.subSequence(start, end);

}

// Método que retorna el texto como string

public String toString() {

return texto;

}

}

Téngase en cuenta que las clases String, StringBuffer y StringBuilder ya implementan la interfaz CharSequence.

5.2. La interfaz «Appendable».

Un objeto que implementa la interfaz java.lang.Appendable es una secuencia de caracteres a la cual se pueden añadir valores. Los caracteres para añadir deben ser caracteres Unicode válidos. Debe ser implementada por cualquier clase cuyas instancias son utilizadas para recibir un formato de salida. Los métodos que define son:

• Appendable append(CharSequence csq) throws IOException, debe añadir una secuencia de caracteres al objeto. • Appendable append(CharSequence csq, int inicio, int fin) throws IOException, debe añadir una subsecuencia de caracteres. • Appendable append(char c) throws IOException, debe añadir un caracter dado.

5.3. La interfaz «Readable».

Un objeto que implementa la interfaz java.lang.Readable es un origen de caracteres. Los caracteres del objeto pueden ser accedidos mediante un método read, usando como búfer intermedio un CharBuffer. Por ejemplo, el siguiente código crea un Readable (la clase UnTextoReadable) cuyo origen de caracteres es un string: class UnTextoReadable implements Readable {

String texto; // el texto origen

int indice; // índice de lectura para leer un caracter del texto

// Constructor para asignar el texto origen de caracteres

public UnTextoReadable(String texto) {

this.texto = texto;

}

// Método para acceder al origen de datos.

// Este método debe añadir caracteres al argumento "cb" y retornar el número

// de caracteres leídos o -1 si ya no puede suministrar más caracteres.

public int read(CharBuffer cb) throws IOException {

if (indice < texto.length()) {

cb.put(texto.charAt(indice)); // se añade un caracter al CharBuffer

indice++; // se avanza al siguiente índice del texto

return 1; // se ha leído un caracter

} else {

return -1; // no hay más para leer

}

}

Page 98: Fundamentos de JAVA

Fundamentos de Java /98

}

Esta interfaz es usada para crear objetos que sirven como orígenes de caracteres para otras clases, como la clase java.util.Scanner.

5.4. La clase «CharBuffer».

La clase abstracta java.nio.CharBuffer define cuatro categorías de operaciones sobre un búfer de caracteres. Implementa las interfaces Appendable, CharSequence y Readable. El búfer puede ser accedido secuencialmente o mediante posición. Las operaciones que soporta son:

• Métodos absolutos y relativos get() y put() para leer y escribir un único caracter. Las versiones relativas actúan sobre la posición actual del búfer. • Un método relativo get() que permite leer caracteres de la posición actual y que avanza la posición en el búfer. • Un método relativo put() que permite escribir secuencias de caracteres en el búfer. • Métodos para compactar, duplicar y trocear el búfer.

Esta clase implementa la interfaz CharSequence de forma que los caracteres del búfer pueden ser usados como expresiones regulares de las clases del paquete java.util.regex. Se puede crea un CharBuffer asignando memoria mediante el método estático allocate(). Por ejemplo: CharBuffe cb = CharBuffer.allocate(120);

Esta instrucción crea un CharBuffer para almacenar hasta 120 caracteres. También se puede crear un CharBuffer sobre un array o sobre un string modificable. Por ejemplo: char [] secuencia = new char [120] ;

CharBuffer cb1 = CharBuffer.wrap(secuencia, 0, secuencia.length);

StringBuilder sb = new StringBuilder();

CharBuffer cb2 = CharBuffer.wrap(sb);

5.5. La clase «StringBuffer».

Un objeto java.lang.StringBuffer representa una secuencia de caracteres que es manipulada de forma segura en hilos de ejecución. Un StringBuffer es como un String, pero que puede ser modificado. Los StringBuffer son seguros cuando se usan en código compartido por varios hilos de ejecución, puesto que sus métodos están sincronizados. (Véase el tema posterior sobre hilos de ejecución y sincronización.) Constructores de StringBuffer:

• StringBuffer(), crea un búfer vacío contenido y una capacidad inicial de 16 caracteres. • StringBuffer(int capacidad), crea un búfer vacío con la capacidad inicial especificada. • StringBuffer(String str), crea un búfer inicializado con el argumento. La capacidad inicial es 16 caracteres más la longitud del argumento. • StringBuffer(CharSequence seq), crea un búfer que contiene los mismo caracteres que la secuencia especificada.

Los métodos de la clase StringBuffer son: • append( x ), método sobrecargado para varios tipos de argumentos, añade x al final del string subyacente. • long capacity(), retorna la capacidad del búfer. • char charAt(int i), retorna el caracter de índice i. • getChars(int i1, inti2, char[] c, int pos), copia los caracteres entre las posiciones i1 y i2 al array c a partir del índice pos. • insert(int pos, x ), método sobrecargado para varios tipos de argumentos, inserta x a partir de la posición indicada. • long length(), retorna el número de caracteres del contenido. • reverse(), cambia el orden de los caracteres. • setCharAt(int pos, char c), cambia el caracter de posición pos por c. • setLength(int size), cambia la capacidad del búfer. • String toString(), retorna la representación string del contenido.

Las operaciones principales son añadir datos al string subyacente mediante los métodos sobrecargados append() e insert(). Por ejemplo: StringBuffer sb = new StringBuffer("Mar"); // sb => "Mar"

sb.append("no"); // sb => "Marno"

sb.insert(3, "cia"); // sb => "Marciano"

Page 99: Fundamentos de JAVA

Fundamentos de Java /99

Cada StringBuffer tiene una capacidad, que se mantiene mientras el contenido es menor que dicha capacidad. Si el contenido excede de la capacidad se redimensiona el espacio de memoria del búfer a una capacidad mayor.

5.6. La clase «StringBuilder».

Un objeto java.lang.StringBuffer permite crear strings que se pueden modificar. Es una clase análoga a StringBuffer pero sin garantizar la sincronización de sus métodos. Esta clase fue diseñada para usarla en vez de StringBuffer en situaciones donde el código se ejecuta desde un único hilo de ejecución, puesto que ofrece un mejor rendimiento en cuanto a rapidez y uso de memoria. Sus constructores, métodos y operaciones son idénticos a los de StringBuffer.

5.7. La clase «StringTokenizer».

La clase StringTokenizer proporciona uno de los primeros pasos para realizar un análisis gramatical de una cadena de entrada, extrayendo los símbolos que se encuentren en esa cadena. Si se tiene una cadena de entrada cuyo formato es regular y se desea extraer la información que está codificada en ella, StringTokenizer es el punto de partida. Para utilizar esta clase se necesita un String de entrada y un String de delimitación. Los delimitadores marcan la separación entre los símbolos que se encuentran en la cadena de entrada. Se considera que todos los caracteres de la cadena son delimitadores válidos; por ejemplo, para ",;:" el delimitador puede ser una coma, un punto y coma o dos puntos. El conjunto de delimitadores por defecto son los caracteres de espacio habituales: espacio, tabulador, línea nueva y retorno de carro. Una vez que se ha creado un objeto StringTokenizer se utiliza el método nextToken() para ir extrayendo los símbolos consecutivamente. El método hasMoreTokens() devuelve true cuando todavía quedan símbolos por extraer. En el ejemplo siguiente se crea un objeto StringTokenizer para analizar gramaticalmente parejas del tipo "clave=valor" de un string. Los conjuntos consecutivos de parejas van separadas por dos puntos (:) import java.util.StringTokenizer;

class MiPrograma {

static String cadena = "titulo=Tutorial de Java:idioma=castellano:autor=Agustin Froufe";

public static void main( String args[] ) {

StringTokenizer st = new StringTokenizer( cadena,"=:" );

while( st.hasMoreTokens() ) {

String clave = st.nextToken();

String valor = st.nextToken();

System.out.println( clave + "\t" + valor );

}

}

}

Y la salida de este programa, una vez ejecutado se presenta en la reproducción siguiente: C:\> java MiPrograma

titulo Tutorial de Java

idioma castellano

autor Agustin Froufe

5.8. La clase «Scanner».

La clase java.util.Scanner implementa la interfaz Iterator<String>, y representa un escáner de texto plano que puede reconocer términos de tipos primitivos y strings usando expresiones regulares. El escáner se conecta a un origen de caracteres (un flujo de texto plano) y separa el texto en términos usando patrones de delimitación (el patrón por defecto son espacios en blanco contiguos). Cada vez que efectuamos una operación de lectura con el escáner, ésta retorna uno de los términos. Los términos pueden ser convertidos en valores de diversos tipos usando métodos de lectura next especializados. 5.8.1. Creación de un escáner. Se puede instanciar un objeto escáner asociado a varios tipos de orígenes de caracteres:

• Scanner(String origen), crea un escáner tomando como origen un string. Por ejemplo, el siguiente código escanea un string:

// Código de scaneo que imprime los datos del texto separados por un barra.

Scanner sn = new Scanner("23 hola 3,45");

Page 100: Fundamentos de JAVA

Fundamentos de Java /100

System.out.println(sn.nextInt() + "/" + sn.next() + "/" + sn.nextFloat());

sn.close();

Como resultado se imprime: 23/hola/3.45

• Scanner(Readable origen), crea un escáner tomando como origen un objeto Readable. • Scanner(InputStream origen, String charset), crea un escáner tomando como origen de datos un stream de entrada. Los bytes del stream son convertidos en caracteres usando la codificación indicada en el segundo parámetro (si se omite se aplica la codificación subyacente, normalmente Unicode). Por ejemplo, el siguiente código permite leer un número entero desde el teclado:

Scanner sc = new Scanner(System.in);

int i = sc.nextInt();

• Scanner(File origen, String charset), crea un escáner tomando como origen de datos un archivo (cuya ruta se encapsula en un objeto File). Los bytes del archivo son convertidos en caracteres usando la codificación indicada en el segundo parámetro (si se omite se aplica la codificación subyacente, normalmente Unicode). Como ejemplo, el siguiente código permite leer e imprimir números de tipo long desde un archivo de texto denominado "misNumeros.txt":

Scanner sc = new Scanner(new File("misNumeros.txt"));

while ( sc.hasNextLong() ) {

System.out.println( sc.nextLong());

}

sc.close();

Después de usar un escáner debemos cerrarlo usando el método Scanner.close(). Esto hace que el origen de los caracteres subyacente también se cierre. 5.8.2. Modificación del patrón de delimitación. Un escáner puede usar también otros delimitadores en vez de espacios en blanco. El siguiente ejemplo lee varios términos desde un string separados por la palabra "otro": String origen = "1 otro 2 otro rojo otro azul otro";

Scanner s = new Scanner(origen).useDelimiter("\\s*otro\\s*");

System.out.print(s.nextInt());

System.out.print(s.nextInt());

System.out.print(s.next());

System.out.print(s.next());

s.close();

Como resultado se imprime: 12rojoazul

El método Scanner.useDelimiter() permite asignar el patrón delimitador. Este patrón puede usar caracteres especiales y secuencias de escape para crear el patrón. Éstos son:

\\ Indica el caracter barra inclinada. \t Indica el caracter de tabulación. \n Indica el caracter de nueva línea. \r Indica el caracter de retorno de carro. \f Indica el caracter de continuación de línea. \a Indica el caracter de alerta. \e Indica el caracter de escape. \0n Indica un caracter en código octal. \xn Indica un caracter en código hexadecimal. \s Indica un espacio blanco: [ \t\n\x0B\f\r] . Indica cualquier caracter. * Indica una combinación de cero o más caracteres. \d Indica un dígito: [0-9] \D Indica un no dígito: [^0-9] \S Indica un no espacio en blanco: [^\s] \w Indica un caracter alfabético o numérico: [a-zA-Z_0-9] \W Indica un caracter no alfabético o no numérico: [^\w]

Se pueden indicar grupos de caracteres con la sintaxis: [abc] Incluye 'a', 'b', o 'c'.

Page 101: Fundamentos de JAVA

Fundamentos de Java /101

[^abc] Cualquier caracter excepto 'a', 'b', o 'c' (negación) [a-zA-Z] Cualquier caracter entre 'a' y 'z' o entre 'A' y 'Z' (rango) [a-d[m-p]] Incluye ante 'a' y 'd' o entre 'm' y 'p' (unión) [a-z&&[def]] Incluye 'd', 'e' o 'f' (&& provoca la intersección) [a-z&&[^bc]] Incluye entre 'a' y 'z', excepto 'b' y 'c' (sustracción)

Los demás caracteres son interpretados como literales. El método Scanner.reset() vuelve a asigna el delimitador por defecto (espacios en blanco). 5.8.3. Reconocimiento de los términos. El escáner dispone de varios métodos next para leer un término del origen de caracteres según un tipo determinado. Estos métodos son:

• String next(), lee el siguiente término completo, independientemente de su tipo. Se usa normalmente para leer términos de tipo string. • String next(String patron), lee el siguiente término que casa con el patrón especificado. • String nextLine(), lee la línea actual hasta el final (excluyendo el separador de fin de línea). • boolean nextBoolean(), lee el siguiente término como un valor booleano. • byte nextByte(), lee el siguiente término como un byte. • short nextShort(), lee el siguiente término como un valor de tipo short. • int nextInt(), lee el siguiente término como un valor de tipo int. • long nextLong(), lee el siguiente término como un valor de tipo long. • float nextFloat(), lee el siguiente término como un valor de tipo float. • double nextDouble(), lee el siguiente término como un valor de tipo double. • BigInteger nextBigInteger(), lee el siguiente término como un valor de tipo BigInteger. • BigDecimal nextBigDecimal(), lee el siguiente término como un valor de tipo BigDecimal.

Si uno de los métodos next no puede interpretar el siguiente término como del tipo buscado entonces se genera una excepción del tipo java.util.InputMismatchException. Por ejemplo, el siguiente código genera una excepción: Scanner s = new Scanner(" true 1 true")

boolean b1 = s.nextBoolean(); // se asigna b1= true

boolean b2 = s.nextBoolean(); // genera una excepción porque no puede interpretar 1 como booleano

Para evitar estas excepciones podemos utilizar los métodos: hasNext(), hasNextBoolean(), hasNextByte(), hasNextShort(), hasNextInt(), hasNextLong(), hasNextFloat(), hasNextDouble(), hasNextBigInteger() y hasNextBigDecimal(). Todos estos métodos retornan un valor booleano que indica si el siguiente término a recuperar es del tipo especificado. El método hasNext() especifica simplemente si hay algún término para recuperar. Una operación de escaneo puede bloquearse esperando por entradas. Los métodos next y hasNext primero se saltan cualquier entrada que coincida con el patrón delimitador, y entonces se preparan para recuperar el siguiente término. Si el origen de los caracteres es una secuencia abierta que está a la espera de la introducción de más datos, entonces estos métodos se bloquean. Por ejemplo, el siguiente código Scanner sc = new Scanner(System.in);

int i = sc.nextInt();

Hace que el programa quede a la espera hasta que el usuario introduce un número y pulsa la tecla de salto de línea.

6. Formateo y referencia cultural.

6.1. Referencias culturales (la clase «Locale»).

Un objeto java.util.Locale representa una especificación geográfica, política o una región cultural. Una operación que requiere un objeto Locale para realizar su tarea se denomina sensible a la cultura y adapta su información a la referencia cultural establecida. Por ejemplo, cuando se muestra un número en una operación sensible a la cultura hay que adaptar el separador de miles y de decimales a la referencia cultural establecida. 6.1.1. Creación de referencias culturales. Para crear un objeto Locale se puede usar uno de los siguientes constructores: Locale(String lenguaje)

Locale(String lenguaje, String pais)

Locale(String lenguaje, String pais, String variante)

Page 102: Fundamentos de JAVA

Fundamentos de Java /102

El argumento lenguaje es un código de lenguaje ISO válido. Estos códigos consisten normalmente de dos letras en minúsculas. Por ejemplo, el código "es" especifica el lenguaje español, el código "en" especifica el lenguaje inglés, etc. El argumento pais es una código de país ISO válido. Estos códigos consisten normalmente de dos letras en mayúscula. Por ejemplo, el código "ES" especifica España, el código "GR" especifica Gran Bretaña, el código "US" especifica Estados Unidos, etc. El argumento variante es un código específico de navegador o vendedor. Por ejemplo, se usa "WIN" para Windows, "MAC" para Macintosh, y "POSIX" para POSIX. Cuando se quieren especificar dos variantes, hay que separarlas con un guión de subrayado y poner el más importante primero. Por ejemplo, la cultura española tradicional puede ser establecida con un objeto Locale de la siguiente manera: Locale localEsp = new Locale("es", "ES", "Traditional_WIN");

Locale.setDefault( localEsp ); // establece la referencia cultural española en la máquina virtual

La clase Locale también proporciona constantes estáticas para algunos idiomas tradicionales: Locale.ENGLISH, Locale.US, Locales.FRENCH, etc. 6.1.2. Métodos de la clase «Locale». La clase Locale proporciona los siguientes métodos estáticos:

• Locale getDefault(), retorna la referencia cultural establecida en la Máquina Virtual de Java. Esta referencia cultural queda establecida durante el arranque de la máquina virtual y normalmente se basa en la establecida en el sistema. • void setDefault(Locale), asigna una nueva referencia cultural por defecto • Locale[] getAvailableLocales(), retorna una array con todas las referencias culturales soportadas por la máquina virtual. • String[] getISOCountries(), retorna un array con los códigos de país definidos en ISO 3166. • String[] getISOLanguages(), retorna un array con los códigos de idioma definidos en ISO 639.

Otros métodos no estáticos de esta clase son: • String getLanguage(), retorna el código de lenguaje. • String getCountry(), retorna el código de país/región. • String getVariant(), retorna el código de variante. • String getDisplayLanguage(Locale), retorna el nombre del lenguaje de la instancia adaptado al idioma del argumento. Por ejemplo, si el argumento es el idioma español y la instancia tiene cultura "fr_FR", retornará "Francés". Si no se especifica argumento, se aplica el idioma por defecto. • String getDisplayCountry(Locale), retorna el nombre de país de la instancia adaptado al idioma del argumento. Por ejemplo, si el argumento es el idioma español y la instancia tiene cultura "fr_FR", retornará "Francia". Si no se especifica argumento, se aplica el idioma por defecto. • String getDisplayName(Locale), retorna el nombre de cultura (lenguaje, país y variante) de la instancia adaptado al idioma del argumento.

6.1.3. Clases que aplican referencias culturales. La plataforma Java proporciona muchas clases para realizar operaciones sensibles a la cultura. El paquete java.text dispone de clases como DateFormat, DecimalFormat, NumberFormat y SimpleDateFormat, que tienen muchos métodos estáticos convenientes para crear objetos por defecto de esas clases. Por ejemplo, para crear un NumberFormat podemos usar: NumberFormat.getInstance()

NumberFormat.getCurrencyInstance()

NumberFormat.getPercentInstance()

Estos métodos tienen dos variantes; una con un argumento Locale explícito y otra sin argumentos. Si no se especifica la referencia cultura se aplica la establecida por defecto. Las clases DateFormat y SimpleDateFormat dan formato a fechas y hora de forma sensible a la cultura. La clase abstracta NumberFormat da formato a números, monedas, o porcentajes de forma sensible a la cultura. La clase DecimalFormat es una subclase no abstracta de NumberFormat para dar formato a números decimales.

6.2. Localizar una aplicación (la clase «ResourceBundle»).

La clase abstracta java.util.ResourceBundle permite gestionar paquetes de recursos para un idioma o referencia cultural específica. Cuando nuestro programa necesita leer un recurso dependiente de la cultura, un string por ejemplo, podemos utilizar esta clase para localizarlo dentro del paquete de recursos apropiado. De esta forma podemos escribir código que se adapte a la cultura e idioma del usuario.

Page 103: Fundamentos de JAVA

Fundamentos de Java /103

Mediante esta clase podemos crear programas que pueden: - Ser localizados sobre varios idiomas. - Gestionar una o varias culturas. - Ser modificados fácilmente para soportar en el futuro más culturas.

Los paquetes de recursos consisten en archivos que contienen varios recursos comunes. Cada recurso en el archivo se compone de una palabra clave y un valor. 6.2.1. Implementación de la clase «ResourceBundle». Para adaptarse a varios idiomas o culturas, se crea una subclase de ResourceBundle localizada para cada cultura y paquete. Cada subclase de un paquete comparte la parte inicial de su nombre y contiene los mismos recursos. En cada subclase la palabra clave del recurso es la misma pero el valor se adapta a la cultura correspondiente. La clase ResorceBundle tiene dos métodos abstractos que deben ser reescritos:

• public Enumeration<String> getKeys(), debe retornar un enumeración con las claves válidas. • protected Object handleGetObject(String key), debe retornar el valor asociado a una clave o null si ésta no existe.

El siguiente código es un ejemplo donde se crea una subclase personalizada de ResourceBundle, MisRecursos, que gestiona dos recursos (con claves "ok" y "cancel"). // Clase de recursos por defecto (en este caso: lenguaje español, país España)

public class MisRecursos extends ResourceBundle {

public Object handleGetObject(String key) {

if (key.equals("ok"))

return "OK"; // Haremos que este valor sea común para todos los idiomas.

else if (key.equals("cancel"))

return "Cancelar"; // Este valor será distinto para cada idioma

return null; // Si no existe la clave se retorna null.

}

public Enumeration<String> getKeys() {

return Collections.enumeration(keySet());

}

// El método handleKeySet() debe retornar la lista de sólo las claves de esta clase. Se reescribe

// aquí para que la implementación de getKeys() puede realizarse sobre el valor de keySet().

// Donde keySet() retorna un conjunto con las claves de esta clase y las de sus superclases.

protected Set<String> handleKeySet() {

return new HashSet<String>(Arrays.asList("ok", "cancel"));

}

}

// Clase de recursos para el lenguaje inglés

public class MisRecursos_en extends MisRecursos {

public Object handleGetObject(String key) {

// No es necesaria la clave "ok", pues será manejada por la clase base.

if (key.equals("cancel"))

return "Cancel";

return null;

}

protected Set<String> handleKeySet() {

return new HashSet<String>(Arrays.asList("cancel"));

}

}

La parte inicial del nombre de la clase, antes del guión de subrayado, es común a todas las clases de un paquete; después del guión se especifica un código ISO de idioma. Cuando se busque un recurso dependiente de la cultura se localizará en la subclase que coincida con el código de idioma. Si no hay coincidencia se usará la subclase por defecto, aquella que no incluye un código de idioma. Si queremos establecer recursos diferentes para los países podemos especializar las subclases añadiendo a su nombre el código de país. Por ejemplo, la clase MisRecursos_en_US sería utilizada para el inglés de Estados Unidos, y la clase MisRecursos_en_GR para el inglés de Gran Bretaña. Cuando en el programa necesitemos acceder a un recurso debemos usar uno de los métodos estáticos ResourceBundle.getBundle() para establecer el paquete de recursos. Por ejemplo:

Page 104: Fundamentos de JAVA

Fundamentos de Java /104

ResourceBundle rb = ResourceBundle.getBundle("MisRecursos", Locale.getDefault() );

El primer argumento establece el nombre por defecto de la subclase, y el segundo argumento establece la referencia cultural mediante un objeto Locale. La sobrecarga más completa de este método es: ResourceBundle getBundle(String subClase, Locale cultura, ClassLoader loader, ResourceBundle.Control control)

El parámetro ClassLoader es un objeto que permite cargar la subclase indicada en el primer parámetro. Todas las clases disponen de un cargador por defecto que puede obtenerse mediante UnaClase.class.getClassLoader(). El parámetro ResourceBundle.Control es un objeto que controla el proceso de carga de los recursos. Para recuperar un recurso se usa un método get* apropiado al tipo del valor del recurso. Como argumento del método get* se debe pasar la clave del recurso. Por ejemplo: String texto = rb.getString("cancel");

Si el recurso no se encuentra, el método get* lanza una excepción del tipo MissingResourceException. En este código se utiliza el método getString() porque el valor a recuperar es de tipo String, pero la clase también proporciona los métodos getStringArray(), y getObject() para recuperar respectivamente un array de strings y un objeto cualquiera. La plataforma Java proporciona dos subclases de ResourceBundle: ListResourceBundle y PropertyResourceBundle, que proporcionan una forma más simple de crear recursos. 6.2.2. La clase «ResourceBundle.Control». La clase interna ResourceBundle.Control proporciona la información necesaria para realizar el proceso de carga del paquete para los métodos getBundle() que toman una instancia de ResourceBundle.Control. Podemos implementar nuestras propias subclases para permitir formatos de paquetes de recursos no estándar, cambiar la estrategia de búsqueda, o definir parámetros. 6.2.3. Administrador de caché. Las instancias creadas por los métodos getBudle() son almacenadas en caché por defecto, de forma que estos métodos retornen siempre la misma instancia. Podemos cambiar este comportamiento usando los siguientes métodos:

• ResourceBundle.clearCache(), elimina todos los paquetes de recursos de la caché. • long getTimeToLive(…) de la clase ResourceBundle.Control, retorna el tiempo de permanencia, en milisegundos, de los paquetes de recursos indicados en la caché. • boolean needsReload(…) de la clase ResourceBundle.Control, determina el tiempo de expiración en la caché.

6.2.4. La clase «ListResourceBundle». La clase abstracta java.util.ListResourceBundle es una subclase de ResourceBundle que permite localizar recursos de una forma sencilla mediante una lista. Su método abstracto getContents() debe ser reescrito para retornar una matriz de objetos formados por pares de clave y valor. Cada par debe ser un array cuyo primer elemento es la clave, y cuyo segundo elemento es el valor asociado. En el código siguiente se crean subclases localizadas de ListResourceBundle que contienen dos recursos (con claves "saludo" y "despedida"): // Clase de recursos por defecto

class MisRecursos extends java.util.ListResourceBundle {

protected Object[][] getContents() {

return new Object[][] { {"saludo", "Hola"}, {"despedida", "Adios"} };

}

}

// Clase de recursos para el idioma inglés de Estados Unidos

class MisRecursos_en_US extends java.util.ListResourceBundle {

protected Object[][] getContents() {

return new Object[][] { {"saludo", "Hello"}, {"despedida", "Goodbye"} };

}

}

6.2.5. La clase «PropertyResourceBundle». La clase abstracta java.util.PropertyResourceBundle es una subclase de ResourceBundle que permite localizar recursos almacenados en archivos de propiedades. Al contrario que en otros tipos de paquetes de recursos, no es necesario crear subclases de PropertyResourceBundle. En vez de eso, debemos crear archivos que contengan los datos de recurso. El

Page 105: Fundamentos de JAVA

Fundamentos de Java /105

método ResourceBundle.getBundle() busca automáticamente por el archivo de propiedades apropiado y crea una instancia de PropertyResourceBundle que lo referencia. Los nombres de los archivos de propiedades siguen los mismos criterios que los aplicados a las subclases de ResourceBundle, y contienen los recursos con el siguiente formato: # Comentarios opcionales de la primera clave

Clave1 = Valor1

# Comentarios opcionales de la segunda clave

Clave2 = Valor2

El siguiente ejemplo muestra los archivos miembros de una familia de paquete de recursos con el nombre base "recursos", ubicados en una subcarpeta "props" dentro de la carpeta raíz de la aplicación

Archivo «/props/recursos.properties» Archivo «/props/recursos_en.properties»

# Idioma español

fecha=3 de Marzo de 2010

# Idioma inglés

fecha=2010, March 3

// Podemos recuperar el recurso para el idioma por defecto:

ResourceBundle rs = PropertyResourceBundle.getBundle("props/recursos");

// O podemos recuperar el recurso para el idioma inglés de la siguiente manera:

ResourceBundle rs = PropertyResourceBundle.getBundle("props/recursos", Locale.UK);

// Recupero el valor del recurso "fecha"

System.out.println(rs.getString("fecha"));

6.3. Clases para manipular fechas.

6.3.1. Clase «Date». La clase java.util.Date representa un dato de fecha y hora con precisión de milisegundos. Cada instancia de esta clase encapsula un valor de tipo long que representa los milisegundos transcurridos desde la fecha base del 1/1/1970 GMT a las 0 horas. Una vez instanciado un objeto Date, su valor de fecha y hora es inmodificable; es decir, un Date representa un valor constante de fecha y hora. Algunos métodos de la clase Date son:

Date () Crea una instancia a partir de la fecha y hora actual del sistema. Date (long m) Crea una instancia correspondiente a m milisegundos transcurridos desde la fecha

base. boolean after(Date d) Indica si la fecha de la instancia es posterior a d. boolean before(Date d) Indica si la fecha de la instancia es anterior a d. long getTime() Retorna los milisegundos transcurridos desde la fecha base. void setTime(long m) Establece el valor de fecha y hora como m milisegundos transcurridos desde la

fecha base. String toString() Retorna una representación string de la instancia con la información: día de la

semana, mes, día del mes, hora, minutos, segundos, zona horaria y año. Aunque la clase Date contiene otros métodos (la mayoría declarados como obsoletos) que permiten manipular su valor de fecha y hora, se recomienda usar las clases que se describen en los siguientes apartados. El paquete java.sql contiene tres subclases específicas para contener datos de fecha almacenados en bases de datos:

• java.sql.Date, encapsula sólo la parte de fecha en el formato año-mes-día. • java.sql.Time, encapsula sólo la parte de hora. • java.sql.Timestamp, encapsula un formato de fecha y hora más completo.

6.3.2. Clases «TimeZone» y «SimpleTimeZone». La clase abstracta java.util.TimeZone representa una zona horaria. Normalmente se obtiene la zona horaria por defecto usando el método estático TimeZone.getDefault(), el cual crea un objeto TimeZone basado en la zona horaria en la cual se ejecuta un programa. También se puede usar TimeZone.getTimeZone(String) pasándole como argumento un ID de zona horaria. Como ejemplo, para la zona horaria del Pacífico de Estados Unidos el ID es "America/Los_Angeles". Podemos obtener todos los ID’s de zonas horarias disponibles mediante el método TimeZone. getAvailableIDs() que retorna un array de string. La clase java.util.SimpleTimeZone es una subclase concreta de TimeZone que representa la zona horaria usada con el calendario Gregoriano.

Page 106: Fundamentos de JAVA

Fundamentos de Java /106

El formato de una zona horaria Gregoriana es del estilo: "GMT+10" o "GMT+0010". 6.3.3. Clases «Calendar» y «GregorianCalendar». La clase java.util.Calendar es una clase abstracta que dispone de métodos para manipular los valores de fecha y hora de objetos Date. Métodos de la clase Calendar:

Calendar()

Calendar(TimeZone tz, Locale l) Constructores.

void add (int campo, int incremento) Incrementa o decrementa el valor de fecha y hora indicado por la constante de campo.

int getFirstDayOfWeek () Retorna el primer día de la semana según la zona horaria. int get (int campo) Según la constante de campo devuelve el año, mes, día, etc. void set (int campo, int valor) Según la constante de campo asigna el año, mes, día, etc. long getTimeInMillis () Retorna el tiempo en milisegundos. void setTimeInMillis (long m) Asigna el tiempo en milisegundos. Date getTime () Retorna el tiempo como Date. void setTime (Date d) Asigna el tiempo como Date.

Constantes de campo para los métodos get() y set(): HOUR hora de mañana o tarde (de 0 a 11) HOUR_OF_DAY hora del día (de 0 a 23) DAY_OF_WEEK toma valores entre SUNDAY y SATURDAY DAY_OF_WEEK_IN_MOTH día de la semana en el mes DAY_OF_MONTH día del mes (entre 1 y 31) DAY_OF_YEAR día del año (entre 1 y 365) WEEK_OF_MONTH semana del mes WEEK_OF_YEAR semana del año ERA era YEAR año con cuatro dígitos MONTH mes (entre o y 11) MINUTE minutos (entre 0 y 59) SECOND segundos (entre 0 y 59) MILLISECOND milisegundos (entre 0 y 999) ZONE_OFFSET zona horaria DST_OFFSET desfase respecto a la zona GMT AM_PM toma el valor AM o PM

Como la clase Calendar es abstracta, para manipular fechas debemos usar una de sus subclases, java.util.GregorianCalendar. La clase GregorianCalendar añade las constantes BC y AD para la era. El siguiente ejemplo de código obtiene el año, mes y día de la fecha actual del sistema: GregorianCalendar gc = new GregorianCalendar();

gc.setTime( new Date() );

int año = gc.get(Calendar.YEAR);

int mes = gc.get(Calendar.MONTH);

int dia = gc.get(Calendar.DAY_OF_MONTH);

6.3.4. Clases «DateFormat» y «SimpleDateFormat». La clase abstracta java.text.DateFormat aporta métodos para convertir un valor String en Date y viceversa, según un formato y una referencia cultural establecida. DateFormat proporciona métodos estáticos para obtener una instancia basada en varios estilos de formato:

• DateFormat getTimeInstance(), obtiene un formateador de hora con el estilo por defecto de la cultura actual. • DateFormat getTimeInstance(int estilo, Locale locale), obtiene un formateador de hora con un estilo determinado para una cultura. Si se omite la cultura se aplica la actual. Los estilos predefinidos vienen dados por las constantes SHORT, MEDIUM, LONG y FULL. • DateFormat getDateInstance(), obtiene un formateador de fecha con el estilo por defecto de la cultura actual. • DateFormat getDateInstance(int estilo, Locale locale), obtiene un formateador de fecha con un estilo determinado para una cultura. Si se omite la cultura se aplica la actual. Los estilos predefinidos vienen dados por las constantes SHORT, MEDIUM, LONG y FULL.

Page 107: Fundamentos de JAVA

Fundamentos de Java /107

• DateFormat getDateTiemInstance(), obtiene un formateador de fecha y hora con el estilo por defecto de la cultura actual. • DateFormat getDateTimeInstance(int estilo, Locale locale), obtiene un formateador de fecha y hora con un estilo determinado para una cultura. Si se omite la cultura se aplica la actual. Los estilos predefinidos vienen dados por las constantes SHORT, MEDIUM, LONG y FULL. • DateFormat getInstance(), obtiene un formateador de fecha y hora con el estilo SHORT y la cultura actual.

DateFormat ayuda a dar formato a fechas y horas para una referencia cultura. Para dar formato para la cultura actual podemos usar: String fechaFormatoCorto = DateFormat.getDateInstance().format( new Date() );

Si estamos dando formato a varias fechas es más eficiente obtener un formateador y usarlo las veces que sea necesario. Por ejemplo: Date [] misFechas = new Date[] { . . . };

DateFormat df = DateFormat.getDateInstance();

for (int i = 0; i misFechas.length; ++i) {

Systeme.out.println(df.format(misFechas[i]) + "; ");

}

Para dar formato en varias culturas, se especifica en la invocación del método creador: DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.FRANCE);

Y se puede usar para interpretar un string: Date d = df.parse("12-10-2010 04:12:45");

Los métodos de esta clase no están sincronizados. La clase derivada java.text.SimpleDateFormat permite realizar formatos personalizados pasando en el constructor un string con el formato que deseamos utilizar. Por ejemplo: SimpleDateFormat sdf1 = new SimpleDateFormat(“dd-MM-yyyy hh:mm:ss”);

SimpleDateFormat sdf2 = new SimpleDateFormat(“dd-MM-yy”);

Date d = sdf1.parse("12-04-1968 11:23:45");

String s = sdf2.format(d);

Los patrones que se pasan en el constructor usan los siguientes símbolos: d para indicar el día del mes

M para indicar el mes

y para indicar el año

h para indicar la hora

m para indicar los minutos

s para indicar los segundos

S para indicar los milisegundos

6.4. Aplicar formatos (la clase «Formatter»).

Un objeto java.util.Formatter representa un intérprete de strings con formatos al estilo de la función printf del lenguaje C. Esta clase proporciona soporte para justificar y alinear el texto, dar formatos numéricos y de fecha, y especificar otros formatos de salida aplicando una referencia cultural. Soporta los tipos comunes de Java como byte, BigDecimal, y Calendar. 6.4.1. ¿Cómo aplicar formatos según la referencia cultural? Un ejemplo de uso de la clase Formatter es el siguiente: StringBuilder sb = new StringBuilder();

// Envía la salida interpretada al búfer "sb". Se aplica la referencia cultural de Estados Unidos.

Formatter interprete = new Formatter(sb, Locale.US);

// El formato indica la posición de cada argumento en la salida.

interprete.format("%4$2s %3$2s %2$2s %1$2s", "a", "b", "c", "d")

// sb -> " d c b a"

// Localidades en el primer argumento opcional aplican formatos numéricos según la referencia

// cultural. La precisión y tamaño pueden ser establecidos para redondear y alinear el valor.

interprete.format(Locale.FRANCE, "e = %+10.4f", Math.E);

// sb -> "e = +2,7183"

// La bandera numérica '(' puede usarse para que los números negativos apararezcan entre

// paréntesis en vez de con un signo menos. Los separadores de grupo se insertan automáticamente.

float balance = -6217.58f;

interprete.format("Acumulado: $ %(,.2f", balance);

Page 108: Fundamentos de JAVA

Fundamentos de Java /108

// sb -> "Acumulado: $ (6,217.58)"

Podemos usar el método estático String.format() para obtener un string aplicando un formato como primer argumento sobre el resto de argumentos: // Formatear un string que contiene una fecha.

import java.util.*;

Calendar c = new GregorianCalendar(1995, MAY, 23);

String s = String.format("Cumpleaños de Ana: %1$te %1$tm,%1$tY", c);

// s = "Cumpleaños de Ana: 23 May, 1995"

El método String.format() también permite especificar como primer argumento un objeto java.util.Locale para establecer la referencia cultural. Las clases NumberFormat y DecimalFormat también disponen de métodos format() para dar formato a números. Por ejemplo: Locale.setDefault(Locale.ENGLISH);

DecimalFormat df = new DecimalFormat("#,#00.000");

String s = df.format(777734.56);

// s = "777,734.560"

6.4.2. Sintaxis de formato. Cada método que aplica formatos de salida requiere un string de formato y una lista de argumentos. El string de formato contiene texto fijo y uno o más especificadores de formato embebidos. Por ejemplo, en Calendar c = ...;

String s = String.format("Cumpleaños de Ana: %1$te %1$tm,%1$tY", c);

El string de formato es el primer argumento del método String.format(). Contiene tres especificadores de formato: "%1$tm", "%1$te" y "%1$tY". Estos especificadores indican cómo deben ser procesados los argumentos y dónde deben insertarse en el texto de salida. La lista de argumentos consiste de varios argumentos pasados al método después del string de formato. En el ejemplo anterior hay un único argumento de tipo Calendar. Los especificadores de formato tienen la siguiente sintaxis general: %[indiceArgumento$][modificadores][tamaño][.precisión]conversión

El opcional [indiceArgumento] es un entero que referencia la posición de un argumento en la lista de argumentos. El primer argumento está referenciado por "1$", el segundo por "2$", etc. El opcional [modificadores] es un conjunto de caracteres que modifican el formato de salida. El conjunto válido de modificadores depende de la conversión. El opcional [tamaño] es un entero no negativo que indica el tamaño mínimo de caracteres a escribir en la salida. El opcional [precisión] es un entero no negativo usado para restringir el número de caracteres. Su significado depende del tipo de conversión. El obligatorio conversión es un caracter que indica cómo el argumento debe ser interpretado. Depende del tipo de dato del argumento. El formato específico para tipos que son usados para representar fechas y hora tiene la siguiente sintaxis: %[indiceArgumento$][modificadores][tamaño]conversión

El obligatorio conversión se compone de dos caracteres. El primero es 't' o 'T'. El segundo indica el formato que será usado. 6.4.3. Conversiones. Las conversiones se pueden dividir en las siguientes categorías:

• General: puede ser aplicada a cualquier tipo de argumento. • Caracter: puede ser aplicada a tipos básicos que representan caracteres Unicode: char, Character, byte, Byte, short, y Short. Este conversión sólo se puede aplicar a los tipos int e Integer cuando el método Character.isValidCodePoint() retorna true. • Numérica:

- Integral. Puede ser aplicada a los tipos: byte, Byte, short, Short, int e Integer, long, Long, y BigInteger. - Punto flotante. Puede ser aplicado a los tipos: float, Float, double, Double, y BigDecimal.

• Fecha/Hora: puede ser aplicada a tipos que son capaces de codificar fechas y hora: long, Long, Calendar, y Date. • Porcentaje: produce un literal '%'. • Separador de línea: produce saltos de líneas específicos de la plataforma.

Page 109: Fundamentos de JAVA

Fundamentos de Java /109

La tabla siguiente resume las conversiones soportadas. Las conversiones denotadas por caracteres en mayúsculas son las mismas que para los correspondientes caracteres en minúsculas, excepto que el resultado es convertido a mayúsculas según la localidad.

Conversión Categoría Descripción

'b', 'B' general Si el argumento es null, el resultado es "false". Si el argumento es booleano el resultado es el string retornado por String.valueOf(argumento). En otro caso, el resultado es "true".

'h', 'H' general Si el argumento es null, el resultado es "null". En otro caso, el resultado se obtiene de la invocación de Integer.toHexString(arg.hashCode()).

's', 'S' general Si el argumento es null, el resultado es "null". Si el argumento implementa la interfaz Formattable, se invoca argumento.formatTo(). En otro caso, el resultado se obtiene de argumento.toString().

'c', 'C' caracter El resultado es un caracter Unicode.

'd' integral El resultado es formateado como un entero decimal.

'o' integral El resultado es formateado como un entero octal.

'x', 'X' integral El resultado es formateado como un entero hexadecimal.

'e', 'E' flotante El resultado es formateado como un número en notación científica.

'f' flotante El resultado es formateado como un número decimal (float o double).

'g', 'G' flotante El resultado es formateado usando la notación científica o el formato decimal, de pendiendo de la precisión y el valor de redondeo.

'a', 'A' flotante El resultado es formateado como un número en punto flotante hexadecimal con un significante y un exponente.

't', 'T' fecha/hora Prefijo para conversión de fechas y horas.

'%' porcentaje El resultado es un literal '%'.

'n' separador El resultado es un separador de línea específico para la plataforma.

Cualquier otro caracter será ilegal y se reservan para futuras extensiones. Para las conversiones de Fecha/Hora (conversiones), las siguientes tablas resumen los caracteres de sufijo que acompañan al caracter 't' o 'T'.

Caracteres de formato de hora

'H' Hora del día formado por dos dígitos desde 00 a 23.

'I' Hora del día formado por dos dígitos desde 01 a 12.

'k' Hora del día para 24 horas, desde 0 a 23.

'l' Hora del día para 12 horas, desde 1 a 12.

'M' Minutos formado por dos dígitos desde 00 a 59.

'S' Segundos formado por dígitos desde 00 a 60. (60 es un valor especial.)

'L' Milisegundos formados con tres dígitos desde 000 a 999.

'N' Nanosegundos formados con nuevo dígitos desde 000000000 a 999999999.

'p' Prefijo de mañana o tarde según la localidad. Por ejemplo, "am" o "pm".

'z' Zona de tiempo de estilo numérico RFC 822 desde GMT. Por ejemplo, -0800.

'Z' Un string que representa la abreviatura para la zona horaria.

's' Segundos desde el comienzo de la referencia temporal.

'Q' Milisegundos desde el comienzo de la referencia temporal.

Caracteres de formato de fecha

'B' Nombre completo del mes según la localidad. Por ejemplo, "Enero", "Febrero".

'b' Abreviatura del mes según la localidad. Por ejemplo, "Ene", "Feb".

'h' Como 'b'.

'A' Nombre completo del día de la semana según la localidad. Por ejemplo, "Lunes", "Martes".

'a' Abreviatura del día de la semana según la localidad. Por ejemplo, "Lun", "Mar".

'C' Año formado por dos dígitos. Por ejemplo, 09 para indicar el año 2009.

'Y' Año formado por cuatro dígitos. Por ejemplo, 2010.

'y' Los dos últimos dígitos del año. Por ejemplo, 09.

'j' Día del año formado por tres dígitos. Por ejemplo, de 001 a 366 para el calendario Gregoriano.

'm' Mes formado por dos dígitos desde 01 a 13.

'd' Días del mes formado por dos dígitos desde 01 a 31.

'e' Día del mes como número entre 1 a 31.

Caracteres de composición de fecha y hora

'R' Hora y minutos formada para 24 horas como "%tH:%tM".

Page 110: Fundamentos de JAVA

Fundamentos de Java /110

'T' Horas, minutos y segundos formada para 24 horas como "%tH:%tM:%tS".

'r' Horas, minutos y segundos formada para 12 horas como "%tI:%tM:%tS %Tp".

'D' Fecha con mes, día y año formada como "%tm/%td/%ty".

'F' Fecha completa ISO 8601 formada como "%tY-%tm-%td".

'c' Fecha y hora formada como "%ta %tb %td %tT %tZ %tY". Por ejemplo, "Sun Jul 20 16:17:00 EDT 1969".

6.4.4. Modificadores. La tabla siguiente resume los modificadores soportados. Se indica si el modificador es soportado según el tipo del argumento.

Mod. General Caracter Integral Flotante Fecha Descripción

'-' sí sí sí sí sí El resultado será justificado a la izquierda.

'#' sí 1 - sí 3 sí - El resultado usará una conversión dependiente de forma alternativa.

'+' - - sí 4 sí - El resultado siempre incluirá un signo

' ' - - sí 4 sí - El resultado incluirá un espacio obligado para valores positivos.

'0' - - sí sí - El resultado será completado con ceros.

',' - - sí 2 sí 5 - El resultado incluirá separadores de miles y/o decimales según la localización.

'(' - - sí 4 sí 5 - El resultado pondrá paréntesis para números negativos.

1 Dependiente de la definición de Formattable. 2 Sólo para conversiones 'd'. 3 Sólo para conversiones 'o', 'x', y 'X'. 4 Para conversiones 'd', 'o', 'x', y 'X' aplicadas a BigInteger o 'd' aplicado a enteros estándar. 5 Sólo para conversiones 'e', 'E', 'f', 'g', y 'G'.

6.4.5. Tamaño. El tamaño es el menor número de caracteres que serán escritos en la salida. Para la conversión de separador de línea no es aplicable y producirá una excepción. 6.4.6. Precisión. Para los tipos general, la precisión es el máximo número de caracteres que serán escritos en la salida. Para la conversión en punto flotante 'e', 'E', y 'f' la precisión es el número de dígitos después del separador decimal. Si la conversión es 'g' o 'G', entonces la precisión es el número total de dígitos en la magnitud resultante del redondeo. Si la conversión es 'a' o 'A', la precisión no se debe especificar. Para los tipos de caracter, integral, y fecha/hora y para la conversión de porcentaje y separador de línea, la precisión no es aplicable y se generará una excepción. 6.4.7. Índice de argumento. El índice de argumento es un entero decimal que indica la posición del argumento en la lista de argumentos. El primer argumento es referenciado como "1$", el segundo como "2$", etc. Otra forma de referenciar los argumentos por posición es usar el modificador '<', el cual hace que el argumento previo sea reutilizado. Por ejemplo, las dos siguientes instrucciones son equivalentes: String s1 = String.format("Cumpleaño: %1$tm %1$te,%1$tY", c);

String s2 = String.format("Cumpleaños: %1$tm %<te,%<tY", c);

7. Objetos observadores y observables.

7.1. La clase «Observable».

La clase java.util.Observable representa un objeto observable, o “data” en el paradigma modelo-vista. Se pueden crear subclases para representar un objeto que la aplicación quiere tener en observación. Un objeto observable puede tener uno o más observadores. Un observador puede ser cualquier objeto que implemente la interfaz java.util.Observer. Después de que una instancia observable se modifica, la aplicación puede invocar al método Observable.notifyObservers() para notificar a todos los observadores del cambio. Los observadores deben registrarse en el objeto observable mediante el método Observable.addObserver(). Y los observadores, por defecto, son notificados en el mismo orden en que se han registrado. Los métodos de la clase Observable son:

• void addObserver(Observer), añade un observador al conjunto de observadores. • void deleteObserver(Observer), elimina un observador del conjunto de observadores.

Page 111: Fundamentos de JAVA

Fundamentos de Java /111

• void deleteObservers(), elimina a todos los observadores. • void notifyObservers(Object arg), notifica a los observadores de algún cambio. Si el objeto observable ha cambiado, como indica el método hasChanged(), entonces notifica a todos los observadores y después llama al método clearChanged(). Cada observador tiene un método update() que es invocado con dos argumentos: el objeto observable y arg. • void notifyObservers(), es equivalente a notifyObservers(null). • boolean hasChanged(), indica si se ha producido un cambio en el observable. • int countObservers(), retorna el número de observadores.

Además posee dos métodos protegidos que pueden ser reescritos por las subclases: • void setChanged(), marca el observable con un cambio de forma que el método hasChanged() retorne true. • void clearChanged(), quita la marca de cambio en el observable de forma que el método hasChanged() retorne false. Este método es invocado automáticamente por los métodos notifyObservers().

7.2. La interfaz «Observer».

La interfaz java.util.Observer permite implementar objetos observadores que son informados de cambios en objetos observables. Posee un único método que es invocado desde el objeto observable cada vez que notifica de un cambio:

• void update(Observable o, Object arg)

El segundo argumento, arg, es el mismo objeto que se pasa en el método notifiyObservers() del objeto observable.

7.3. Ejemplo del modelo observable/observadores.

Para ilustrar el uso de este modelo de objetos, se crea primero una clase observable. La clase Alarma representa una entidad que es capaz de lanzar una alarma de cierta gravedad. Define dos propiedades: el nombre de la alarma y la gravedad de la misma. Cada vez que cambie el valor de la gravedad se avisa a objetos observadores. class Alarma extends Observable {

private String nombre;

private int gravedad;

// Constructor

public Alarma(String nombre) {

this.nombre = nombre;

}

// Se reescribe para retornar el nombre de la alarma

public String toString() {

return nombre;

}

// Método accesor para asignar la gravedad. Se encarga de avisar a los observadores.

public void setGravedad(int gravedad) {

this.gravedad = gravedad;

super.setChanged(); // esta invocación es necesaria para que

super.notifyObservers(gravedad); // se avisen a los observadores registrados

}

}

Ahora se crea un objeto observador que recibe el aviso de que ha cambiado la gravedad de un objeto Alarma. class Observador implements Observer {

// Este método es invocado por el objeto observable

public void update(Observable o, Object arg) {

System.out.println("La alarma "+ o + " tiene gravedad " + arg);

}

// Se prueba la funcionalidad de las clases

public static void main(String[] args) {

Observador obs = new Observador(); // se instancia el observador

Alarma alarm = new Alarma ("alarma1"); // se instancia una alarma

alarm. addObserver(obs); // se registra el observador

alarm.setGravedad(50); // se cambia la gravedad de la alarma

}

}

Page 112: Fundamentos de JAVA

Fundamentos de Java /112

Si se ejecuta el código anterior se obtiene el siguiente mensaje: La alarma alarma1 tiene gravedad 50

8. Gestión de recursos del sistema.

8.1. La clase «Properties».

8.1.1. ¿Qué son las propiedades? Hay ocasiones en que es necesario que los programas sean capaces de leer atributos determinados en el sistema, y ser capaces de leer y/o modificar atributos específicos de la aplicación. Los mecanismos que Java proporciona para resolver estas tareas son tres: argumentos en la línea de comandos, parámetros en la llamada a applets y las propiedades. Las propiedades definen un entorno persistente; es decir, se pueden fijar valores necesarios para las aplicaciones mediante propiedades. En otras palabras, si hay alguna información que deba proporcionarse a un programa cada vez que se ejecute, entonces las propiedades pueden ser la solución. Las propiedades vienen representadas por objetos de la clase java.util.Properties, la cual implementa una colección de objetos formados por una clave y un valor. Estos objetos se pueden almacenar en disco y recuperarse cuando se necesiten, si se dispone del adecuado acceso a disco. Cada propiedad individual se identifica a través de la clave. Tanto la clave como el valor deben ser strings. 8.1.2. Propiedades del sistema. El siguiente ejemplo de código instancia un objeto Properties para presentar en pantalla las propiedades por defecto del sistema en una estación de trabajo. import java.util.*;

import java.lang.*;

class MiPrograma {

public static void main( String args[] ) {

// Instancia y presenta un objeto Properties para presentar las caracteristicas del sistema

Properties obj = new Properties( System.getProperties() );

obj.list( System.out );

}

}

Las propiedades del sistema en un momento dado se pueden obtener llamando al método getProperties() de la clase System. El método list() de la clase Properties sería luego el utilizado para visualizar el contenido del objeto. La siguiente tabla lista las propiedades del sistema que se pueden obtener cuando arranca el sistema y lo que significan.

Clave: Significado: Accesible por applets: "file.separator" Separador de ficheros ("/" o "\") si "java.class.path" La ruta de clases de Java no "java.class.version" Número de versión de Java si "java.home" Carpeta de instalación de Java no "java.vendor" La cadena del vendedor específico de Java si "java.vendor.url" La URL del vendedor de Java si "java.version" Número de versión de Java si "line.separator" Separador de líneas si "os.arch" Arquitectura del sistema operativo si "os.name" Nombre del sistema operativo si "path.separator" Separador de rutas (p.e., ":") si "user.dir" Directorio actual de trabajo no "user.home" Directorio home del usuario actual no "user.name" Nombre de la cuenta de usuario no

8.1.3. Archivos de configuración. Una vez que se ha creado un objeto Properties para el programa, se pueden guardar en un dispositivo de almacenamiento fijo utilizando el método save() y posteriormente recuperarlos a través del método load(). Para añadir un nuevo para de clave/valor se usa el método put(). Estas funcionalidades de la clase Properties nos permiten crear archivos de configuración para una aplicación. Por ejemplo podemos tener un archivo llamado “aplicacion.ini” con el contenido siguiente:

Page 113: Fundamentos de JAVA

Fundamentos de Java /113

clave1=valor1

clave2=valor2

Desde código del programa podemos leer el contenido de este archivo y sus valores de la siguiente forma: Properties p = new Properties();

try {

p.load(new FileInputStream("aplicacion.ini"));

String clave1 = p.getProperty("clave1")

String clave2 = p.getProperty("clave2")

} catch (IOException ex) {

// tratamos la excepción

}

También podemos modificar el contenido de estas variables de inicialización y grabarlas al archivo de configuración: p.setProperty("clave1", "Nuevo valor");

try {

p.store(new FileOutputStream("aplicacion.ini"),”comentario opcional”);

} catch (Exception ex) {

// tratamos la excepción

}

Así mismo, Properties permite trabajar con archivos en formato XML. En este caso el contenido del archivo debe corresponderse con el siguiente formato: <?xml version="1.0" encoding="UTF-8" standalone="no"?>

<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">

<properties>

<comment>Aquí van comentarios opcionales</comment>

<entry key="Clave1">valor1</entry>

<entry key="Clave2">valor2</entry>

</properties>

El código para abrir y grabar archivos XML es análogo el del ejemplo anterior, sustituyendo el método load() por loadFromXML(), y el método store() por storeToXML().

8.2. La clase «Runtime».

La clase java.lang.Runtime encapsula el proceso del intérprete Java que se ejecuta. No se puede crear una instancia de Runtime; sin embargo, se puede obtener una referencia al objeto Runtime que se está ejecutando actualmente llamando al método estático Runtime.getRuntime(). Aunque Java tiene un sistema de recogida de basura automática, o liberación de memoria automática, se podría desear el conocer la cantidad de objetos y el espacio libre que hay, para comprobar la eficiencia del código escrito. Para proporcionar esta información, la clase Runtime dispone de los métodos totalMemory(), que devuelve la memoria total en la Máquina Virtual Java, y freeMemory(), que devuelve la cantidad de memoria libre disponible. En entornos seguros se puede hacer que Java ejecute otros procesos intensivos en un sistema operativo multitarea. Hay varios constructores del método exec() que permiten que se indique el nombre del programa que se va a ejecutar, junto con los parámetros de entrada. El método exec() devuelve un objeto Process, que se puede utilizar para controlar la interacción del programa Java con el nuevo proceso en ejecución. El problema a la hora de documentar exec() es que los programas que se ejecutan son muy dependientes del sistema. Se podría utilizar exec("/usr/bin/ls") en Solaris/Linux y exec("notepad") en Windows para abrir un editor de texto en cada sistema operativo. En el ejemplo siguiente, una aplicación ejecutada en Windows usa exec() para lanzar el bloc de notas, el editor de textos simple, en uno de los archivos fuente de Java. Nótese que exec() convierte automáticamente el carácter "/ " en el separador de camino de Windows "\". class MiPrograma {

public static void main( String args[] ) {

Runtime r = Runtime.getRuntime();

Process p = null;

String comando[] = { "notepad","MiPrograma.java" };

// Datos de la memoria del Sistema

System.out.println( "Memoria Total = "+ r.totalMemory() + " Memoria Libre = "+ r.freeMemory() );

Page 114: Fundamentos de JAVA

Fundamentos de Java /114

// Intenta ejecutar el comando que se le indica, en este caso lanzar el bloc de notas

try {

p = r.exec( comando ); // abre el bloc de notas de forma asíncrona y sigue ejecutando el código

} catch( Exception e ) {

System.out.println( "Error ejecutando "+comando[0] );

}

}

}

Hay varias formas alternativas de exec(), pero ésta es la más habitual. El proceso que devuelve exec() se puede manipular después de que el nuevo programa haya comenzado a ejecutarse. Se puede eliminar el subproceso con el método Process.destroy(). El método Process.waitFor(), esperar a, provoca que el programa espere hasta que el subproceso termine, y el método Process.exitValue(), valor de salida, recupera el valor que ha devuelto el subproceso cuando terminó. En el ejemplo siguiente, se presenta el mismo código del ejemplo anterior, modificado para que espere a que termine el proceso en ejecución. class MiPrograma {

public static void main( String args[] ) {

Runtime r = Runtime.getRuntime();

Process p = null;

String comando[] = { "notepad","MiPrograma.java" };

try {

p = r.exec( comando );

p.waitFor();

} catch( Exception e ) {

System.out.println( "Error ejecutando "+comando[0] );

}

System.out.println( comando[0]+" ha devuelto "+p.exitValue() );

}

}

Este ejemplo espera hasta que se cierre el bloc de notas, y después imprime el código devuelto, que es 0 si no hubo errores.

8.3. La clase «System».

Hay ocasiones en que se necesita acceder a recursos del sistema, como son los dispositivos de entrada/salida, el reloj del sistema, etc. Java dispone de la clase java.lang.System, que proporciona acceso a estos recursos, independientemente de la plataforma. Es decir, que si se ejecuta un programa en una plataforma diferente a la que se ha desarrollado, no es necesaria ninguna modificación para tener en cuenta las peculiaridades de la nueva plataforma. 8.3.1. Propiedades y métodos de System. En la clase System se definen los dispositivos estándar de entrada/salida: static PrintStream err; // representa el dispositivo de salida de error (por defecto, la pantalla)

static InputStream in; // representa la consola de entrada (el teclado)

static PrintStream out; // representa la consola de salida (la pantalla)

Y dispone de varios métodos: static void arraycopy( Object,int,Object,int,int ) // para copiar un array en otro

static long currentTimeMillis() // para obtener los milisegundos del sistema

static void exit( int ) // para finalizar la máquina virtual

static void gc() // para invocar al recolector de basura

static Properties getProperties() // para obtener propiedades del sistema

static String getPropertie( String ) // para obtener una propiedad del sistema

static void setProperties( Properties ) // para asignar una propiedad del sistema

static SecurityManager getSecurityManager() // para obtener el gestor de seguridad del sistema

static void setSecurityManager( SecurityManager ) // para asignar el gestor de seguridad del sistema

static native int identityHashCode( Object ) // para obtener el código hash de un objeto dado

static void load( String ) // para cargar un archivo dado como una librería dinámica

static void loadLibrary( String ) // para cargar una libería dada

static void runFinalization() // para finalizar cualquier objeto pendiente

static void setErr( PrintStream ) // para asignar el dispositivo de salida de error

Page 115: Fundamentos de JAVA

Fundamentos de Java /115

static void setIn( InputStream ) // para asignar el dispositivo de entrada estándar

static void setOut( PrintStream ) // para asignar el dispositivo de salida estándar

No se puede instanciar ningún objeto de la clase System, porque es una clase final y todos sus contenidos son privados; por ellos es por lo que no hay una lista de constructores en la enumeración de métodos. 8.3.2. Entrada/Salida estándar. La clase System proporciona automáticamente, cuando comienza la ejecución de un programa, un canal para leer del dispositivo estándar de entrada (normalmente, el teclado), un canal para presentar información en el dispositivo estándar de salida (normalmente, la pantalla) y otro canal donde presentar mensajes de error, que es el dispositivo estándar de error (normalmente, la pantalla). Los tres canales de entrada/salida están controlados por esta clase y se referencian como: System.in entrada estándar System.out salida estándar System.err salida de error estándar Las variables internas out y err son de tipo PrintStream. La clase PrintStream proporciona variaos métodos para poder visualizar información: print(), println(), printf() y write(). Los métodos print() y println() son semejante, la única diferencia es que println() coloca automáticamente un carácter nueva línea en el canal, tras la lista de argumentos que se le pase. El método printf() se utiliza de la misma forma que el mismo método de la clase Formatter. En ese sentido es similar al método printf() del lenguaje C. El método write() se utiliza para escribir bytes en el canal; es decir, para escribir datos que no pueden interpretarse como texto, como pueden ser los datos que componen un gráfico. Los métodos print() y println() aceptan un argumento de cualquiera de los siguientes tipos: Object, String, char[], int, long, float, double o boolean. En cada caso, el sistema convierte el dato a un conjunto de caracteres que transfiere al dispositivo estándar de salida. Si se invoca al método println() sin argumentos, simplemente se inserta un carácter nueva línea en el canal. Para leer desde teclado se pueden utilizar las siguiente expresiones:

• System.in.read(), lee un caracter y retorna su código como un valor entero. Tiene el inconveniente de que los caracteres pulsados por el usuario se acumulan en el buffer de teclado y no son enviados al código hasta que el usuario provoca un salto de línea. • System.console().readLine, lee una línea de caracteres hasta que se pulsa la tecla [enter], y retorna un String. • (new BufferedReader(new InputStreamReader(System.in)).readLine(), también permite leer una línea por teclado.

8.3.3. Propiedades del Sistema. Ya se ha indicado al tratar la clase Properties que las propiedades son pares de clave/valor que los programas Java pueden utilizar para establecer y mantener diversos atributos o parámetros, que estarían disponibles en todas sus invocaciones. El sistema Java también mantiene un conjunto de Propiedades del Sistema que contienen información acerca del entorno en que se está ejecutando como: el usuario actual, la versión actual del ejecutable de Java, etc. Estas propiedades se fijan cuando arranca el sistema. La clase System dispone de varios métodos para leer y escribir las propiedades del sistema. A estas propiedades se puede acceder a través de la clave o se puede leer el conjunto completo de propiedades. Los métodos que proporciona la clase System para leer las propiedades del sistema son:

• static String getProperty( String clave ) • static String getProperty( String clave, String def )

El primer método solamente tiene un argumento y devuelve un objeto de tipo String. Si no es capaz de encontrar la propiedad indicada en la clave, devolverá una referencia nula. El segundo método necesita dos argumentos. El primero es la clave de la propiedad que se quiere consultar y el segundo argumento es el valor por defecto que devolverá el método si la propiedad clave no se encuentra, o si esa propiedad clave sí se encuentra pero no tiene asignado ningún valor.

• Properties getProperties() Devuelve un objeto Properties conteniendo el conjunto completo de pares clave/valor del sistema. Una vez obtenido este objeto, se puede usar cualquiera de sus métodos para obtener la información que se necesite.

Las propiedades del sistema también se pueden modificar a través del método setProperties() de esta clase System. Este método coge un objeto Properties que haya sido inicializado con el adecuado conjunto de pares

Page 116: Fundamentos de JAVA

Fundamentos de Java /116

clave/valor que se desee y reemplaza el conjunto completo de propiedades del sistema por los nuevos valores representados por el objeto. 8.3.4. Finalización. Todos los objetos en Java son instanciados dinámicamente, en tiempo de ejecución, en la pila. Cuando ya no exista variable alguna que referencie al objeto, éste será marcado para su reciclado. El reciclador de memoria, o garbage collector, se ejecuta asincrónicamente en segundo plano, recogiendo los objetos que ya no estén referenciados y haciendo que la memoria que ocupaban quede libre y sea devuelta al sistema para su reutilización. El método finalize() de un objeto siempre se ejecuta antes de que la memoria ocupada por ese objeto sea liberada. Este método se puede sobrescribir para las clases que se desee, de forma que se ejecuten un conjunto de sentencias determinado antes de liberar la memoria. Se puede forzar una ejecución del reciclador de memoria invocando al método gc(). Además, se puede forzar a que el sistema ejecute la finalización de los objetos utilizando la llamada al método runFinalization(), que invocará a todos los métodos finalize() de los objetos que estén marcados para ser reciclados. 8.3.5. Copia de arrays. Para copiar eficientemente datos desde un array a otro se puede utilizar el método arraycopy(). Este método requiere cinco argumentos, de forma que se indiquen los arrays de origen y destino y el número de elementos que se van a copiar. static void arraycopy( Object src,int src_position,Object dst, int dst_position,int length );

El array destino debe estar localizado en memoria antes de llamarlo y debe ser lo suficientemente largo para contener los datos que se le van a pasar. 8.3.6. Salida del sistema. Se puede abandonar el intérprete Java llamando al método exit() y pasándole un entero como código de salida. La instrucción System.exit(0);

provoca la finalización del programa y retorna al sistema operativo un valor (cero) que indica terminación normal. Cuando se finaliza con exit() ya no se ejecuta el código de los bloques finally si los hubiera. 8.3.7. Seguridad. El controlador de seguridad es un objeto que asegura una cierta política de seguridad a la aplicación Java. Se puede fijar el controlador de seguridad para las aplicaciones utilizando el método setSecurityManager() y se puede recuperar el que esté actualmente definido utilizando el método getSecurityManager(). Se pueden establecer problemas de seguridad por ejemplo cuando se serializa un objeto. Para evitarlo, hay que instalar un objeto SecurityManager y un fichero java.policy adecuado. Para instalar un SecurityManager debemos ejecutar este código System.setProperty ("java.security.policy", "/una_ruta/java.policy");

if (System.getSecurityManager()==null)

System.setSecurityManager(new RMISecurityManager());

La primera línea es opcional, pues se buscará el fichero java.policy en el directorio del usuario. El fichero java.policy puede dar todos los permisos a todo el mundo poniendo algo como esto:

Archivo "java.policy" para conceder todos los permisos grant {

permission java.security.AllPermission;

};

Evidentemente no es aconsejable otorgar todos los permisos, puesto que sería equivalente a no establecer un gestor de seguridad. En el tutor de permisos en java de Sun se describen todos los tipos de permisos que se pueden conceder o denegar. El controlador de seguridad para una aplicación solamente se puede fijar una vez. Normalmente un navegador fija su controlador de seguridad al arrancar, con lo cual, en acciones posteriores los applets no pueden fijarlo de nuevo, o se originará una excepción de seguridad si el applet lo intenta.

8.4. El API Preferences.

El API de Java Preferences permite gestionar configuraciones de programas. En el sistema operativo Windows, estas configuraciones se almacenan de forma persistente en el Registro de Windows; mientras que en los sistemas operativos UNIX se almacenan en un fichero oculto en el direcotrio home del usuario. En todo caso este API intenta abstraer al usuario de dónde se almacenan sus preferencias ofreciendo una interfaz

Page 117: Fundamentos de JAVA

Fundamentos de Java /117

estandarizada para todos los sistemas subyacentes. Se pueden almacenar configuraciones mediante valores de los tipos primitivos. 8.4.1. La clase base «Preferences». La clase principal de este API es la clase abstracta Preferences. Esta clase representa un nodo de una colección jerárquica de preferencias (pares de clave-valor), distinguiendo entre tres tipos de datos: de usuario, de sistema y de configuración. Hay dos árboles independientes de nodos:

• Árbol de preferencias del usuario. Se crea un árbol de preferencias de usuario diferente para cada usuario. La información típica guardada aquí incluye configuraciones de las ventanas de una aplicación, como el color, posición y tamaño.

• Árbol de preferencias del sistema. Todos los usuarios comparten el mismo árbol de preferencias del sistema. La información típica guardada aquí incluye configuraciones de instalación de una aplicación.

Los nodos de los árboles se nombran de forma similar a los directorios de una sistema de archivos jerárquicos. Cada nodo de un árbol tiene un nombre de nodo (no necesariamente único), una ruta absoluta única, y una ruta relativa a cada ancestro incluido él mismo. El nodo raíz tiene como nombre de nodo el string vacío (""). Todos los demás nodos reciben un nombre arbritrario en el momento de su creación. La única restricción para estos otros nodos es que no pueden ser el string vacío ni contener en su nombre el caracter de barra /. La ruta absoluta del nodo raíz es "/". Los hijos del nodo raíz tienen como ruta absoluta "/Nombre del nodo". Los demás nodos tiene como ruta absoluta "Ruta absoluta del nodo padre/Nombre del nodo". Métodos de recuperación e identificación de nodos. Esta clase proporciona los siguientes métodos estáticos para recuperar nodos:

• static Preferences userNodeForPackage(Class c) Retorna un nodo del árbol de preferencias del usuario que esté asociado con el paquete de una clase especificada. Aplica las siguiente convención: el nombre absoluto de nodo retornado se compondrá de "/", más la ruta del paquete donde se sustituyen los puntos por barras /. Por ejemplo, el nombre absoluto de un nodo asociado con la clase com.acme.widget.Foo será "/com/acme/widget". Una clase Foo que desea acceder a las preferencias de su paquete puede obtener el nodo correspondiente con el siguiente código:

static Preferences prefs = Preferences.userNodeForPackage(Foo.class);

• static Preferences systemNodeForPackage(Class c) Retorna un nodo del árbol de preferencias del sistema que esté asociado con el paquete de una clase especificada. Utiliza las mismas convenciones que el método userNodeForPackage().

• static Preferences userRoot() Retorna el nodo raíz del árbol de preferencias del usuario.

• static Preferences systemRoot() Retorna el nodo raíz del árbol de preferencias del sistema.

Los métodos de instancia para buscar e identificar los nodos son: • String[] childrenNames() throws BackingStoreException

Retorna un array con los nombre de los hijos del nodo. Si no tiene hijos, retorna un array de tamaño cero. • Preferences parent()

Retorna el nodo padre del actual, o null si es el nodo raíz. • Preferences node(String pathName)

Retorna un nodo que está en el mismo árbol que el actual, creándolo si no existe. El parámetro permite nombres absolutos o relativos al nodo actual.

• boolean nodeExists(String pathName) throws BackingStoreException Retorna true si existe un nodo especificado en el mismo árbol del nodo actual. El parámetro permite nombres absolutos o relativos al nodo actual.

• String name() Retorna el nombre relativo del nodo actual respecto a su padre.

• String absolutePath() Retorna el nombre absoluto del nodo actual.

• boolean isUserNode()

Page 118: Fundamentos de JAVA

Fundamentos de Java /118

Retorna true si el nodo actual pertenece al árbol de preferencias del usuario, y false si pertenece al árbol de preferencias del sistema.

Modificación de preferencias. Todos los métodos que modifican preferencias pueden operar asincrónicamente; permiten retornar un valor inmediatamente, y los cambios serán eventualmente propagados a los almacenes persistentes en algún momento. Puede utilizarse el método flush() para forzar actualizaciones síncronas. Aún así, la terminación normal del la Máquina Virtual de Java nunca producirá pérdida de datos. Los métodos de escritura de preferencias son los siguientes:

• void put(String key, String value) • void putInt(String key, int value) • void putLong(String key, long value) • void putBoolean(String key, boolean value) • void putFloat(String key, float value) • void putDouble(String key, double value) • void putByteArray(String key, byte[] value)

Cada uno de estos métodos asocia el valor especificado con una clave especificada del nodo. • void remove(String key)

Elimina el valor asociado con la clave especificada del nodo. • void clear() throws BackingStoreException

Elimina todas las preferencias (pares clave-valor) del nodo. No tiene efecto sobre los nodos descendientes.

• void removeNode() throws BackingStoreException Elimina el nodo y sus descendientes, invalidando cualquier preferencia contenida en los nodos eliminados.

Lectura de preferencias. Todos los métodos que leen preferencias requieren proporcionar el nombre de una clave del nodo y un valor por defecto. El valor por defecto es retornado si no había ningún valor previo. Los métodos de lectura de preferencias son los siguientes:

• String[] keys() throws BackingStoreException Retorna un array con todas las claves que tiene asociado un valor en el nodo. Retornará un array con tamaño cero si no hay definida ninguan preferencia.

• String get(String key, String def) • int getInt(String key, int def) • long getLong(String key, long def) • boolean getBoolean(String key, boolean def) • float getFloat(String key, float def) • double getDouble(String key, double def) • byte[] getByteArray(String key, byte[] def)

Cada uno de estos métodos retorna el valor asociado con una clave específica del nodo, o bien el valor por defecto pasado como segundo argumento.

Los métodos de esta clase están sincronizados y por tanto no podrán ser ejecutados concurrentemente por varios hilos. Importar y exportar preferencias. Esta clase contiene una facilidad de exportación/importación de preferencias a/desde un documento XML. El documento XML de respaldo de preferencias debe tener la siguiente declaración:

<!DOCTYPE preferences SYSTEM "http://java.sun.com/dtd/preferences.dtd">

El DTD que valida este tipo de documentos es el siguiente:

<?xml version="1.0" encoding="UTF-8"?>

<!ELEMENT preferences (root)>

<!-- El elemento "preferences" contiene un atributo opcional "version", para especificar la versiónd el DTD -->

<!ATTLIST preferences EXTERNAL_XML_VERSION CDATA "0.0" >

<!-- El elemento "root" tiene un mapa que representa la preferencias raíces (si las hay),

Page 119: Fundamentos de JAVA

Fundamentos de Java /119

y un nodo por cada hijo (si lo hay) -->

<!ELEMENT root (map, node*) >

<!-- Adicionalmente, "root" contiene un atributo "type" para indicar si es de usuario o de sistema. -->

<!ATTLIST root type (system|user) #REQUIRED >

<!-- Cada nodo tiene un mapa que representa sus preferencias, y un nodo por cada hijo -->

<!ELEMENT node (map, node*) >

<!-- Adicionalment, cada nodo tiene un atributo "name" -->

<!ATTLIST node name CDATA #REQUIRED >

<!-- Un "map" representa las preferencias almacenadas para un nodo -->

<!ELEMENT map (entry*) >

<!-- Un "entry" representa un única preferencias, como un para clave-valor -->

<!ELEMENT entry EMPTY >

<!ATTLIST entry key CDATA #REQUIRED value CDATA #REQUIRED >

Los métodos para exportar e importar son los siguientes: • void exportNode(OutputStream os) throws IOException, BackingStoreException

Exporta todas las preferencias contenidas en el nodo a un canal de salida. Utiliza la codificación UTF-8. • void exportSubtree(OutputStream os) throws IOException, BackingStoreException

Exporta todas las preferencias contenidas en el nodo y sus descendientes a un canal de salida. Utiliza la codificación UTF-8.

• static void importPreferences(InputStream is) throws IOException, InvalidPreferencesFormatException Este método estático importa todas las preferencias procedentes de un canal de entrada.

8.4.2. Usar el Registro de Windows con el API Preferences. Bajo el sistema operativo Windows podemos usar la clase Preferences para acceder al Registro de Windows. Con Preferences.userRoot() podemos acceder a la raíz HKEY_CURRENT_USER\Software\JavaSoft\Prefs, que es la que usa Java para guardar los ajustes del usuario. Si queremos usar el árbol de nodos de la maquina local (HKEY_LOCAL_MACHINE) debemos usar Preferences.systemRoot(). El siguiente ejemplo muestra cómo leer y escribir en el registro de Windows y cómo hacer algunas comprobaciones: import java.util.logging.Level;

import java.util.logging.Logger;

import java.util.prefs.BackingStoreException;

import java.util.prefs.Preferences;

public class Main {

public static void main(String[] args) {

try {

Preferences mipref = Preferences.userRoot();

// Accedemos a la ruta del registro a la que queremos acceder o escribir. Si no existe, se crea

mipref = mipref.node("/software/miprograma/minodo");

// Se comprueba de que el nodo es de usuario

if (mipref.isUserNode()) {

System.out.println("isUserNode: " + mipref.name() );

}

// Se escribe la ruta absoluta del nodo.

System.out.println("La ruta absoluta es: " + mipref.absolutePath());

// Se comprueba que existe otro nodo.

if (mipref.nodeExists(”/software/miprograma/otronodo”)) {

System.out.println("Existe el nodo " + Mipref.name());

// Se recupera el valor de una clave, o un valor por defecto

System.out.println(mipref.get("unaclave", "No se encuentra"));

}else

System.out.println("No existe el nodo");

// Se crea la clave con un valor

mipref.put("unaclave", "nuevo valor");

}

} catch (BackingStoreException ex) {

Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);

Page 120: Fundamentos de JAVA

Fundamentos de Java /120

}

}

}

Es importante que los nombres de nodos y claves se escriban en minúsculas. Si alternásemos letras en mayúsculas en los nombres se producirían efectos indeseados.

8.5. Recolección de basura.

8.5.1. El Garbage Collection. El proceso de recolección de basura se refiere a la limpieza de instancias (objetos) que han dejado de ser utilizadas en un programa Java. Esto es llevado acabo directamente por la Máquina Virtual de Java mediante un proceso denominado Garbage Collection, el cual permite liberar recursos, en su mayoría de memoria para ser reutilizados por el sistema. En este proceso se plantean dos preguntas:

1) ¿Qué instancias son elegidas y liberadas? Toda aquella instancia cuyas referencias sean asignadas a un valor nulo provoca que sea elegible por el recolector de basura, si la instancia mantiene una referencia entonces no podrá ser elegible. El hecho de que una instancia sea elegible no implica que sea liberada inmediatamente; esto se producirá después de que el recolector de basura sea sea iniciado.

2) ¿Quién invoca el proceso de recolección de basura? La Máquina Virtual de Java se hace cargo de iniciar el Garbage Collection únicamente cuando éste (según sus algoritmos internos) determine que la memoria está en proceso de agotamiento. Es entonces que se libera la memoria de las instancias que se han declarado como elegibles.

Las clases System y Runtime proporcionan métodos estáticos para administrar el recolector de basura. Concretamente, los métodos System.gc() y Runtime.gc() provocan que se inicie el recolector de basura. Sin embargo, a pesar de su nombre, el hecho de invocar estos métodos directamente no implica que se realice el proceso de recolección inmediatamente; lo único que se logra es acelerar los mecanismos para iniciar el proceso de recolección. 8.5.2. Referencias débiles. Una referencia débil a un objeto permite al recolector de basura recolectarlo si el espacio en el montón de memoria es mínimo. Las referencias débiles se usan normalmente en aplicaciones que crean y destruyen continuamente grandes cantidades de datos. Si los objetos de referencias débiles no tienen recursos de memoria a liberar por el recolector de basura, entonces la aplicación puede evitar el coste de reconstruir los datos. El recolector de basura reclama la memoria de los objetos inaccesibles, pero no así de los accesibles. Un objeto se convierte en inaccesible si todas sus referencias han sido invalidadas (por ejemplo, si la referencia ha sido asignada al valor null). Una referencia a un objeto accesible se denomina una referencia fuerte. Una referencia débil también referencia un objeto accesible, u objetivo (target). Se adquiere la referencia fuerte al objetivo pasando el valor como argumento de un constructor de un objeto WeakReference, PhantomReference o SoftReference. Si el objeto objetivo sólo mantiene referencias débiles, entonces el objetivo se convierte en elegible por el recolector de basura. La clase «WeakReference». La clase java.lang.ref.WeakReference permite crear referencias débiles que no impiden que el objeto referenciado sea finalizado, finalice y sea reclamado por el recolector de basura. Puede pasar un tiempo entre que un objeto es elegible y que es recolectado por el recolector de basura. Si se intenta recuperar el objeto objetivo después de que ha sido recolectado, entonces se obtiene el valor nulo; pero si el objetivo todavía no ha sido recolectado, se recupera una referencia válida. El siguiente código muestra cómo usar una referencia débil para mejorar el rendimiento. Object obj = new Object(); // se crea un referencia fuerte

WeakReference<Object> wr = new WeakReference<Object>( obj );

obj = null; // se elimina la referencia fuerte

//...

obj = wr.get(); // se recupera la referencia débil

if (obj != null) {

// la recolección de basura no ha ocurrido y la referencia es válida.

} else {

// ocurrió la recolección y la referencia es nula.

Page 121: Fundamentos de JAVA

Fundamentos de Java /121

}

La clase WeakReference dispone de dos constructores: public WeakReference (T objetivo)

public WeakReference (T objetivo, ReferenceQueue<? super T> q)

El primer argumento es el objeto sobre el que queremos crear una referencia débil. Esta referencia puede recuperarse mediante el método get(). El segundo argumento es una cola en la que el objetivo será registrado, o null si no es requerido ningún registro.

Este tipo de referencia débil es la más usada para implementar mapeados. (Véase la colección WeakHashMap.) La clase «PhantomReference». La clase java.lang.ref.PhantomReference permite crear referencias fantasmas que son añadidas a una cola después de que el recolector de basura determina que las referencias pueden ser reclamadas. Estas referencias fantasmas son normalmente usadas para planificar acciones de limpiado de un modo más flexible que el proporcionado por el mecanismo de finalización de Java. Si el recolector de basura determina que en un determinado momento que el objetivo de la referencia fantasma debe ser recolectado, entonces, en ese momento o más tarde la referencia será encolada. Para asegurarse de que los objetos reclamados permanezcan, las referencias fantasmas no pueden ser recuperadas. El método get() siempre retorna null. La clase PhantomReference tiene un único constructor: public PhantomReference(T referent, ReferenceQueue<? super T> q)

El primer argumento es la referencia del objeto que quiere registrarse dentro de una cola cuando sea reclamado. El segundo argumento es la cola donde será registrada la referencia fantasma.

El siguiente código muestra cómo usar esta clase: // Se crea una cola de registro de referencias

ReferenceQueue<Object> cola = new ReferenceQueue();

// Se crean dos referencias fantasmas y se pasa la cola de registro

PhantomReference<Object> pr1 = new PhantomReference("string fantasma", cola);

PhantomReference<Object> pr2 = new PhantomReference(new Date(), cola);

// Como los objetos no tienen referencias fuertes son inmediatamente encolados por Java.

// Se obtiene la pirmera refencia todavía accesible de la cola

Reference<Object> ref = (Reference<Object>) cola.poll();

// Se quita de la cola la siguiente referencia. La cola se bloquea hasta que haya alguna accesible.

try {

ref = (Reference<Object>) cola.remove();

} catch (InterruptedException ex) {

// Se lanza una excepción si la espera fue interrumpida

}

// Se destruye la cola y todas sus referencias fantasmas

cola = null;

La clase «SoftReference». La clase java.lang.ref.SoftReference permite crear referencias blandas, lo cual permite que sean liberadas al libre albedrio del recolector de basura en respuesta a demandas de memoria. Este tipo de referencias débiles normalmente se usan para implementar cachés de memoria sensibles. Supongamos que el recolector de basura determina en un momento dato que un objeto objetivo es accesible sólo mediante referencias blandas. En ese momento puede decidir liberar automáticamente todas las referencias blandas al objetivo y todas las referencias blandas de cualquier otro objeto con referencias blandas desde el cual el objetivo es referenciado. En ese momento o más tarde encolara la referencia en una cola si se ha registrado a alguna. Se garantiza que todas las referencias blandas serán eliminadas antes de que la Máquina Virtual de Java lance un OutOfMemoryError porque no tenga espacio de memoria para asignar. Mientras el objetivo mantenga una referencia fuerte no podrá ser recolectado. La clase SoftReference dispone de dos constructores: public SoftReference (T objetivo)

public SoftReference (T objetivo, ReferenceQueue<? super T> q)

El primer argumento es el objeto sobre el que queremos crear una referencia blanda. Esta referencia puede recuperarse mediante el método get().

Page 122: Fundamentos de JAVA

Fundamentos de Java /122

El segundo argumento es una cola en la que el objetivo será registrado, o null si no es requerido ningún registro.

9. Reflexión y metadatos.

La reflexión es comúnmente utilizada cuando se quiere examinar o modificar en tiempo de ejecución el comportamiento de las aplicaciones. Por ejemplo, sabiendo el nombre de una clase, podríamos saber qué propiedades, constructores, o métodos tiene. La información que se guarda sobre una clase o estructura de java se denominan metadatos. Se puede instanciar objetos solo con saber el nombre de la clase, o invocar sus métodos en tiempo de ejecución. La reflexión es una técnica poderosa y puede permitir que las aplicaciones realicen operaciones que de otro modo sería imposible.

9.1. La clase «Class».

La clase java.lang.Class implementa Type y contiene propiedades y métodos para acceder a los metadatos de una clase cualquiera. Una instancia de Class representa a una clase o interfaz en la aplicación que se está ejecutando. Una enumeración (enum) es también una especie de clase. Cada array también pertenece a una clase que es reflejada como un objeto de Class que es compartido por todos los arrays con el mismo tipo de elemento y número de dimensiones. Los tipos primitivos de Java (boolean, byte, char, short, int, long, float y double), y la palabra clave void también son representados como objetos de Class. Class no tiene un constructor público. Podemos obtener el objeto Class de cualquier clase mediante la propiedad estática class o el método getClass() de un objeto: String s = ""

Class claseString = String.class; // esta instrucción produce la misma asignación

claseString = s.getClass(); // que esta otra

Algunos de los miembros más interesantes de Class se describen en la siguiente tabla:

Miembros Descripción Class forName(String clase) Método estático que retorna el objeto Class asociado con el nombre de una

clase o interfaz. Invocar este método es equivalente a: Class.forName(clase, true, this.getClass().getClassLoader())

Por ejemplo, la siguiente instrucción retorna la clase Thread: Class t = Class.forName("java.lang.Thread")

Object newInstance() Crea y retorna la referencia de una nueva instancia de la clase representada. La clase es instanciada utilizando el constructor por defecto.

boolean isInterface()

boolean isArray()

boolean isPrimitive()

boolean isSynthetic()

boolean isEnum

Determinan el tipo del objeto Class, indicando respectivamente si es una interfaz, un array, un tipo primitivo, una clase sintética según las especificaciones del lenguaje Java, o una enumeración.

Object cast(Object obj) Moldea un objeto al tipo representado por la entidad. String getName() Retorna el nombre de la entidad. Si es una clase o interfaz incluye la ruta de

paquetes. ClassLoader getClassLoader() Retorna el cargador de clase para la clase. Si el objeto representa un tipo

primitivo o void, retorna null. Class getSuperclass() Retorna la superclase de la entidad, o null si este concepto no es aplicable. Class [] getInterfaces() Retorna un array con todas las interfaces implementadas por la entidad. Method [] getMethods() Retorna un array con todos los métodos públicos de la entidad.

Page 123: Fundamentos de JAVA

Fundamentos de Java /123

Method getMethod(String

nombre, Class ... tiposParams) Retorna un objeto Method que representa un método público de nombre dado cuya lista de parámetros se corresponde con los tipos pasados por argumento. Por ejemplo, el siguiente código invoca el método valueOf(int) de la clase String usando reflexión: try {

Method m = String.class.getMethod("valueOf", int.class);

String s=null;

s = (String) m.invoke(String.class, 27); // s = "27"

} catch (Exception ex) { }

Constructor []

getConstructors() Retorna un array con todos los constructores públicos de la entidad.

Constructor getConstructor

(Class ... tiposParams) Retorna un objeto Constructor que representa un constructor público cuya lista de parámetros se corresponde con los tipos pasados por argumento.

Field[] getFields() Retorna un array con todos los campos públicos de la entidad. Field getField(String campo) Retorna un objeto Field que representa un campo público de nombre dado.

9.2. La clase «Method».

La clase java.lang.reflect.Method implementa Type y proporciona información acerca de un método de una clase o interfaz. El método reflejado puede ser una método estático o de instancia (incluidos los abstractos). Podemos obtener los métodos reflejados de una clase mediante dos métodos de instancia de la clase Class:

• Method getMethod(String nombre, Class ... parámetros), busca un método por su nombre y por los tipos de sus parámetros. • Method [] getMethods(), retorna un array con todos los métodos de la clase.

Algunos de los miembros más interesantes de Method se describen en la siguiente tabla:

Miembros Descripción String getName() Retorna el nombre del método. int getModifiers() Retorna los modificadores del método. Se puede usar la clase Modifier para

decodificar el valor devuelto por este método. Class getReturnType() Retorna el objeto Class que representa el tipo de retorno del método. Object invoke(Object obj,

Object... args) Invoca el código del método sobre un objeto pasado como primer argumento. El resto de argumentos son los parámetros de invocación del método. El valor de retorno es el valor que retorna el método. Si el método es estático se ignora el primer argumento.

boolean isVarArgs() Retorna true si el método fue declarado con argumentos variables.

En el siguiente ejemplo de código se declara la clase Amigo y se muestra cómo aplicar reflexión sobre ella. import java.lang.reflect.Method;

public class Amigo {

public String saludo(String nombre) {

return "Hola " + nombre;

}

public String despedida(String nombre) {

return "Nos vemos " + nombre;

}

}

public class Principal {

public static void main(String ... x) throws Exception {

Amigo amigo = new Amigo();

// Carga en memoria la clase Amigo.

Class claseAmigo = Class.forName("Amigo");

String nombreMetodoAEjecutar = "saludo";

String parametroAEnviar = "Juan Pérez";

// Crea un método. El primer parametro indica el nombre del método.

// El segundo indica el tipo de parametro que va a aceptar (puede haber más).

Method metodoAEjecutar = claseAmigo.getMethod(nombreMetodoAEjecutar, String.class);

Page 124: Fundamentos de JAVA

Fundamentos de Java /124

// Se ejecuta el método sobre el objeto Amigo, enviandole un parámetro.

Object respuesta = metodoAEjecutar.invoke(amigo, parametroAEnviar);

// Se muestra lo que resulta de ejecutar el método.

System.out.println( metodoAEjecutar.getName() + " : " + respuesta);

}

}

9.3. La clase «Field».

La clase java.lang.reflect.Field implementa Type proporciona información acerca de un campo público de una clase o una interfaz. El campo reflejado puede ser un campo estático o de instancia. Podemos obtener los campos reflejados de una clase mediante dos métodos de instancia de la clase Class:

• Field getField(String nombre), busca un campo por su nombre. • Field [] getFields(), retorna un array con todos los campos de la clase.

Algunos de los miembros más interesantes de Field se describen en la siguiente tabla:

Miembros Descripción String getName() Retorna el nombre del campo. int getModifiers() Retorna los modificadores del campo. Se puede usar la clase Modifier para

decodificar el valor devuelto por este método. Class getClass() Retorna el objeto que representa el tipo del campo. Class getDeclaringClass() Retorna la clase o interfaz dónde se declara el campo. Boolean isEnumConstant() Indica si en una de las constantes de una enumeración.

9.4. Anotaciones en Java.

Las anotaciones son un mecanismo para dotar a las clases de meta-información o auto-información. Las anotaciones ya existían en versiones anteriores de Java, por ejemplo para generar documentación de Javadoc: @deprecated, @version, @author, etc. Pero desde la versión 1.5 se han convertido en una parte de los tipos del lenguaje y podemos trabajar con ellas tanto en tiempo de compilación como en tiempo de ejecución. Anotar una porción de código sirve para procesarla de alguna manera en algún script, herramienta de despliegue o de diseño, hacer uso de ellas en un framework, … Para trabajar con las anotaciones en tiempo de ejecución se debe hacer uso de la reflexión. La clase java.lang.reflect.Class dispone de los métodos siguientes:

• Annotation getAnnotation(Class claseAnotacion), retorna la anotación correspondiente al tipo de anotación pasado como argumento, o bien null si no se encuentra. • Annotation[] getDeclaredAnnotations(), retorna una array con todos los objetos de anotación que contiene la clase. • boolean isAnnotation(), indica si la clase es un tipo de anotación.

Asimismo, las clases java.lang.reflect.Field y java.lang.reflect.Method disponen de métodos análogos, puesto que las anotaciones pueden ir en cada uno de estos niveles. 9.4.1. Definición de las anotaciones. Las anotaciones se preceden con el símbolo @ y se pueden clasificar en 3 tipos:

• Anotaciones de marca: aquellas que no reciben ningún parámetro. Sirven para marcar algún elemento y la única información que proporcionan es que existe o no existe. • Anotaciones normales: son aquellas que reciben parámetros en forma de pares nombre=valor. • Anotaciones de un miembro: son aquellas que sólo tiene un miembro (denominado el valor), y no necesitan indicar el nombre del parámetro, solo el valor.

Además existen anotaciones predefinidas, aquellas que forman parte del lenguaje, y que en Java 5 son tres: • @Overrides: indica que un método está siendo sobrescrito. • @Deprecated: indica que un método es obsoleto y no debería usarse. Provoca que se genere un aviso en compilación. • @SupressWarnings: recibe una lista de argumentos y le indican al compilador que no muestre los avisos de los tipos pasados como argumento ("all", "checked", "finally").

Las anotaciones pueden a su vez estar anotadas; es decir, la definición de una anotación podemos anotarlas usando meta-anotaciones. Éstas son:

Page 125: Fundamentos de JAVA

Fundamentos de Java /125

• @Target: indica sobre qué elementos se puede usar la anotación. Recibe un array de argumentos y las posibilidades son: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE. • @Retention: indica el alcance de la anotación. Las posibilidades son:

SOURCE, la anotación sólo existe en el archivo fuente, no en la clase compilada. CLASS, (por defecto) la anotación existe en la clase pero la maquina virtual la ignora en ejecución. RUNTIME, la máquina virtual tiene acceso a ella y es necesaria si queremos acceder mediante reflexión a ella.

• @Inherited: indica que la anotación la heredan las subclases. • @Documented: indica que las herramientas de documentación tipo Javadoc tienen que documentarla.

Así pues, si queremos crear un tipo nuevo de anotación, es decir la clase de los objetos anotación, usaremos la siguiente sintaxis: @Meta-anotacion1(miembros)

@Meta-anotacion2(miembros)

. . .

modificadores @interface NombreTipoAnotación {

TipoMiembro1 nombreMiembro1();

TipoMiembro2 nombreMiembro2() default valorPorDefecto;

. . .

}

Por ejemplo, la anotación predefinida SupressWarnings está definida de la siguiente forma: package java.lang;

import java.lang.annotation.*;

import java.lang.annotation.ElementType;

import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})

@Retention(RetentionPolicy.SOURCE)

public @interface SuppressWarnings {

String[] value();

}

Debemos notar que al crear tipos de anotaciones se usa la notación @interface aunque en realidad se esté extendiendo de la interfaz java.lang.annotation.Annotation. Si se extendiese directamente no se conseguiría un nuevo tipo de anotación. 9.4.2. Anotaciones personalizadas. Si queremos crear nuestras propias anotaciones debemos definir un interfaz para la anotación tal como se definiría una clase. Por ejemplo: public @interface MiAnotacion {

int id(); // define un parámetro 'id'

String comentario(); // define un parámetro 'comentario'

}

Y ya podríamos usarla en algún código. Por ejemplo para anotar una clase y un método: @MiAnotacion (id=1, comentario="clase Test")

public class Test {

@MiAnotacion (id=100, comentario="método Test.unMetodo")

public void unMetodo() { }

}

9.5. Cómo cargar recursos de un proyecto JAR.

A veces puede ser interesante distribuir una aplicación de java como un único archivo jar. Si en la aplicación usamos archivos de texto o imagen de solo-lectura puede ser muy conveniente incluirlos dentro del archivo jar. Para poder leer estos archivos en el código de nuestra aplicación podemos usar los siguiente métodos de la clase Class:

• URL getResource( String recurso), retorna la ruta del archivo recurso como una ruta URL. • InputSteam getResourceAsStream(String recurso), retorna un stream de lectura asociado al recurso dado.

Es conveniente que estos métodos sean invocados por la clase que contiene el método main() que ejecuta la aplicación. El nombre del recurso debe ser construido usando las siguientes reglas:

Page 126: Fundamentos de JAVA

Fundamentos de Java /126

- Si comienza por '/', entonces la ruta absoluta del recurso es la parte del nombre que sigue a '/'. - Si no, la ruta absoluta debe ser de la forma: ruta_de_paquetes/nombre. Si el recurso está ubicado en el mismo paquete de la clase que invoca los métodos de carga, entoces debemos sustituir ruta_de_paquetes por el string "./".

Como ejemplo, supongamos un proyecto jar con la siguiente estructura: MiPrograma.jar

\control

Principal.class

\recursos

test.txt

La clase Principal contiene el método main() y deseamos leer el archivo de texto "test.txt". Podríamos utilizar un código como éste: import java.io.*;

class Principal {

public static void main(String args []) {

try {

// Obtenemos un canal con el archivo de texto

InputStream input = Principal.class.getResourceAsStream("/recursos/test.txt");

// Accedemos a su contenido línea y línea y se imprimen

DataInputStream br = new DataInputStream(input);

String linea;

while ((linea = br.readLine()) != null) {

System.out.println(linea);

}

br.close();

} catch (Exception ex) {

}

}

}

9.6. La clase «ClassLoader».

El Java Classloader (en español, cargador de clases Java) es una parte del Java Runtime Environment que carga dinámicamente las clases Java en la Java Virtual Machine. Normalmente las clases sólo son cargadas bajo demanda. El cargador de clases es responsable de localizar bibliotecas, leer sus contenidos, y cargar las clases contenidas dentro de las mismas. Esta carga es normalmente hecha "bajo demanda", por lo que no ocurre hasta que la clase sea usada por el programa. Una clase con un nombre dado sólo puede ser cargada una vez por un classloader dado. La clase ClassLoadar es abstracta y de ella derivan objetos que permiten localizar o generar los datos que constituyen una definición para una clase. Cada clase u objeto contiene una referencia al ClassLoader que lo define. Se puede obtener de la siguiente manera: ClassLoader cargadorString = String.class.getClassLoader();

La clase ClassLoader posee los siguientes métodos estáticos:

Miembros Descripción URL getSystemResource(String name) Busca un recurso del nombre especificado dentro de la ruta usada

para cargar clases y retorna su localización como un objeto URL. Si no encuentra el recurso retorna null.

Enumeration<URL>

getSystemResources(String name) Busca todos los recursos del nombre especificado dentro de la ruta usada para cargar clases y retorna una enumeración de objetos URL.

InputStream

getSystemResourceAsStream(String

name)

Abre para lectura un recurso del nombre especificado dentro de la ruta usada para cargar clases. Retorna un InputStream o null si no localiza el recurso.

ClassLoader getSystemClassLoader() Retorna el cargador de clases delegado. Se trata del cargador por defecto para la nuevas instancias y es normalmente el cargador usado para iniciar la aplicación.

Page 127: Fundamentos de JAVA

Fundamentos de Java /127

Podemos usar la clase java.net.URLClassLoader para obtener las rutas y recursos disponibles en el classpath. Un ejemplo de código para hacer esto es el siguiente: java.net.URLClassLoader cargador = (java.net.URLClassLoader) ClassLoader.getSystemClassLoader();

java.net.URL[] cjto = Cargador.getURLs();

for (java.net.URL url : cjto) {

System.out.println(url.toString());

}

Como resultado se imprimirán las rutas de librerías y recursos disponibles para la aplicación.

Page 128: Fundamentos de JAVA

Fundamentos de Java /128

V - ENTRADA Y SALIDA DE DATOS

En Java, las entradas y salidas de datos se realizan mediante objetos stream, los cuales representan canales para los flujos de datos.

1. Modelo de flujo de datos en Java.

Un stream es una conexión entre el programa y la fuente o destino de los datos. La información se traslada en serie a través de esta conexión. Esto da lugar a una forma general de representar muchos tipos de comunicaciones.

1.1. Clases para lectura y escritura de datos.

El paquete java.io contiene clases que soportan entrada/salida. Las clases del paquete son principalmente streams; sin embargo, se incluye una clase para ficheros de acceso aleatorio. Las clases centrales del paquete son InputStream y OutputStream las cuales son clases abstractas base para leer de y escribir a streams de bytes, respectivamente. Las clases relacionadas Reader y Writer son clases abstractas base para leer de y escribir a streams de caracteres, respectivamente. El paquete también tiene unas pocas clases misceláneas para soportar la interacción con el Sistema de ficheros del computador

Estas clases tienen los métodos básicos read() y write() que manejan bytes, y no se suelen usar directamente. Desde Java 1.1 aparecieron dos nuevas familias de clases, derivadas de Reader y Writer, que manejan caracteres en vez de bytes.

Page 129: Fundamentos de JAVA

Fundamentos de Java /129

Las clases de fondo resaltado definen de dónde o a dónde se están enviando los datos, el decir, el dispositivo con que conecta el stream. Las demás clases añaden características particulares a la forma de enviarlos. La intención es que se combinen para obtener el comportamiento deseado. Por ejemplo: BufferedReader in = new BufferedReader( new FileReader (“autoxex.bat”) );

A la hora de definir una comunicación con un dispositivo siempre se comenzará determinando el origen o destino de la comunicación (clase resaltada) y luego se le añadirán otras características (clases en blanco).

1.2. Nombres de las clases de «java.io».

Las clases stream siguen una nomenclatura sistemática que permite deducir su función a partir de las palabras que componen el nombre:

Palabra Significado

InputStream, OutputStream Lectura/escritura de bytes

Reader, Writer Lectura/escritura de caracteres File Archivos

String, CharArray, ByteArray, StringBuffer Memoria (a través del tipo indicado) Piped Tubo de datos Buffered Búfer intermedio Filter Filtrado Data Intercambio de datos en formato propio de Java Object Persistencia de objetos Print Imprimir

La siguiente tabla explica el uso de las clases que definen el lugar con el cual se conecta el stream:

Clases Función que realizan

FileReader, FileWriter, FileInputStream, FileInputStream

Son las clases que leen y escriben en archivos de disco.

StringReader, StringWriter, CharArrayReader, CharArrayWriter, BtyeArrayInputStream, ByteArrayOutputStream, StringBufferedSteam

Se comunican con la memoria del ordenador. En vez de acceder del modo habitual al contenido de un string, por ejemplo, lo leen como si llegar caracter a caracter.

PipedReader, PipedWriter, PipedInputStream, PipedOutputStream

Se utilizan como un tubo o conexión bilateral para transmisión de datos. Por ejemplo, en un programa con dos hilos de ejecución pueden permitir la comunicación entre ellos. Un hilo tendrá un objeto PipedReader y el otro el PipedWriter. También pueden comunicar a dos programas distintos.

La siguiente tabla explica las funciones de las clases que alteran el comportamiento de un stream ya definido:

Clases Función que realizan

BufferedReader, BufferdWriter, BufferedInputStream, BufferedOutputStream

Añaden un búfer intermedio para el manejo de los datos. Es decir, se reducen las operaciones directas sobre el dispositivo para hacer más eficientes su uso.

InputStreamReader, OutputStreamReader Permiten convertir streams que utilizan bytes en otros que manejan caracteres.

ObjectInputReader, ObjectOutputReader Pertenecen al mecanismo de serialización.

Page 130: Fundamentos de JAVA

Fundamentos de Java /130

FilterReader, FilterWriter, FilerInputStream, FilterOutputSteam

Son clases base para aplicar diversos filtros o procesos al stream de datos. También se pueden extender para conseguir comportamientos a medida.

DataInputStream, DataOutputStream Se utilizan para escribir y leer datos directamente en los formatos propios de Java.

PrintWriter, PrintStream Tienen métodos adaptados para imprimir las variables de Java con la apariencia normal. A partir de un boolean escriben “true” o “false”, colocan la coma de un número decimal, etc.

1.3. Clase «File».

La clase java.io.File representa una ruta de archivo o de directorio, y proporciona métodos para manipular los archivos y directorios que representa. Los constructores de esta clase reciben la ruta como un string con el formato válido del sistema operativo Windows o Unix, y dicha ruta es codificada en un formato independiente del sistema. Métodos de la clase File:

• File(String ruta) • File(String carpeta, String archivo) • File(File carpeta, String archivo)

Constructores. • boolean isFile() • boolean isDirectory()

Indican si es un archivo o una carpeta realmente existentes. • long length()

En caso de representar un archivo existente, retorna el tamaño del mismo en bytes. • boolean canRead() • boolean canWrite()

Indican si el archivo físico puede ser leído o reescrito. • delete()

Borra el archivo o carpeta físicamente. • RenameTo(File f)

Cambia el nombre del archivo o directorio físicamente. • mkdir()

Crea la carpeta físicamente. • String [] list()

Retorna una lista con los archivos contenidos en el directorio que representa. • String getPath() • String getName() • String getAbsolutePath() • String getParent()

Retornan la ruta, el nombre, la ruta absoluta y la carpeta padre respectivamente. El siguiente ejemplo muestra cómo usar la clase File para leer e imprimir el contenido de una carpeta: class Test() {

public void muestraCarpeta(File ruta) throws Exception {

File[] fs = ruta.listFiles(); // se obtiene un array con los subdirectorios y ficheros contenidos en "ruta"

if (fs == null) { // si no es una ruta válida se lanza una excepción

throw new Exception("La ruta no es directorio válido");

} else {

System.out.println("Contenido del directorio "+ruta.getAbsolutePath());

for (File f : fs) { // por cada directorio y fichero del array se imprime su nombre

System.out.print( f.isDirectory()? "[] " : " ");

System.out.println(f.getName());

}

}

public static void main(String[] args) {

Page 131: Fundamentos de JAVA

Fundamentos de Java /131

try {

muestraCarpeta(new File("c:/"));

} catch (Exception ex) {

}

}

}

Un posible resultado de ejecutar el código anterior puede ser el siguiente: Contenido del directorio c:\

[] $Recycle.Bin

[] Archivos de programa

autoexec.bat

[] Boot

bootmgr

config.sys

[] Documents and Settings

hiberfil.sys

[] MSOCache

muxman.log

pagefile.sys

[] Windows

1.4. Lectura y escritura de archivos.

Para trabajar con archivos existen las siguientes clases de streams: • FileInputStream, para leer bytes desde un archivo binario. • FileOutputStream, para escribir bytes a un archivo binario. • FileReader, para leer caracteres o texto desde un archivo de texto. • FileWriter, para escribir caracteres o texto a un archivo de texto.

Los constructores de estas clases admiten la ruta del archivo con el que queremos trabajar o un objeto File. Si no se encuentra el archivo indicado, los constructores de FileReader y FileInputStream pueden lanzar la excepción java.io.FileNotFoundException. Los constructores de FileWriter y FileOutputStream pueden lanzar java.io.IOException, y si no encuentran el archivo indicado, lo crean nuevo sin lanzar excepción. Para trabajar con los archivos los streams definen un puntero virtual que indica la posición actual sobre el archivo. Cada vez que se lee o escribe, el puntero virtual se desplaza al siguiente dato del archivo. Al instanciar una de estas clases, se abre el archivo asociado y el puntero se sitúa al comienzo del archivo. Para posicionarlo al final debe especificarse en el constructor un segundo argumento con valor true. Por ejemplo: FileWriter fw = new FileWriter ("archivo.txt" , true); // abre el archivo en modo añadir

Para leer un byte o un carácter se utiliza e método read(). Cuando no hay datos para leer se lanza una excepción. Para escribir un byte o un carácter se utiliza el método write(). Al final debe cerrarse el stream con el método close().

1.5. Archivos de texto.

Para leer archivos de texto es preferible crear un objeto BufferedReader de la siguiente forma: BufferedReader b = new BufferedReader(new FileReader(ruta));

Esta clase proporciona el método readLine() para leer una línea de texto. El siguiente código muestra cómo leer un archivo de texto: String texto = new String();

try {

BufferedReader entrada = new BufferedReader ( new FileReader(“archivo.txt”) );

String s;

while ( (s=entrada.readLine()) != null ) // readLine() retona null cuando no hay más datos para leer

texto += s;

entrada.close();

} catch (java.io.FileNotFoundException ex) {

} catch (java.io.IOException ex) {

}

Page 132: Fundamentos de JAVA

Fundamentos de Java /132

De forma análoga, se puede crear un objeto BufferedWriter a partir de un FileWriter. Sin embargo, para escribir a archivos de texto es preferible crear un objeto PrintWriter que proporciona los métodos print() y println(). Ejemplo: try {

PrintWriter salida = new PrintWriter(new BufferedWriter(new FileWriter(“archivo.txt”)));

salida.println(“Primera línea”);

salida.close();

} catch (java.io.IOException ex) {

}

1.6. Archivos no de texto.

Para archivos no de texto es preferible trabajar con las clases DataInputStream y DataOutputStream, que proporcionan métodos para trabajar con datos de tipo primitivo. Son clases diseñadas para trabajar de manera conjunta. Una puede leer lo que la otra escribe, que en sí no es algo legible, sino el dato como una secuencia de bytes. Por ello se utilizan para almacenar datos de manera independiente de la plataforma. Proporcionan métodos específicos para leer/escribir cada tipo de dato del estilo readTipo() y writeTipo(). Por ejemplo: // Escritura de un número decimal

DataOutputStream os = new DataOutputStream (

new BufferedOutputStream (

new FileOutputStream (“archivo.dat”) ) );

os.writeDouble(7.65);

os.close();

// Lectura del dato decimal

DataInputStream is = new DataInputStream (

new BufferedInputStream (

new FileInputStream (“archivo.dat”) ) );

double d = is.readDouble(); // d= 7.65

is.close();

1.7. Archivos de acceso aleatorio.

La clase java.io.RandomAccessFile permite trabajar con archivos de acceso aleatorio. Este tipo de stream permite leer/escribir desde/hacia un archivo mediante un cursor que nos permite situarnos en una posición determinada del archivo. En el constructor de esta clase debemos indicar la ruta del archivo (como String o File) y el modo de apertura ("r" para sólo lectura, "rw" para lectura y escritura, "rws" o "rwd" para lectura y escritura sincronizada). Si el archivo asociado no existe se generará una excepción. Para leer se utilizan los siguientes métodos:

int read() lee un byte. int read(byte[] b, int off, int len) lee len bytes y los copia en el array b a partir de su posición off.

Retorna el número de bytes leídos o -1 si se llegó a fin de fichero. int read(byte[] b) lee b.lenght bytes y los copia en el array b. Este método retorna el

número de bytes leídos o -1 si se llegó a fin de fichero. boolean readBoolean() lee un valor booleano. byte readByte() lee un valor byte con signo. int readUnsignedByte() lee un valor byte sin signo. short readShort() lee un valor short con signo. int readUnsignedShort() lee un valor short sin signo. char readChar() lee un caracter. int readInt() lee un valor int. long readLong() lee un valor long. float readFloat() lee un valor float. double readDouble() lee un valor double. String readLine() lee un línea de texto. String readUTF() lee un texto con formato UTF-8 (Unicode).

Page 133: Fundamentos de JAVA

Fundamentos de Java /133

Cada método read provoca que el cursor se desplace el número de bytes leídos, y puede generar una excepción si nos desplazamos al final del archivo. Para escribir se utiliza el correspondiente método write: write(), writeBytes(), writeBoolean(), writeByte(), writeShort(), writeChar(), writeInt(), writeLong(), writeFloat(), writeDouble(), writeChars(), writeUTF(). Los cuales también provocan un desplazamiento del cursor. Si escribimos más allá del tamaño del archivo, éste se redimensiona automáticamente. Para desplazar o trabajar con el cursor podemos usar los siguientes métodos:

int skipBytes(int n) salta el cursor un número determinado de bytes desde la posición actual, y retorna el número de bytes realmente desplazados o -1.

long getFilePointer() retorna la posición actual absoluta del cursor sobre el archivo. void seek(long pos) sitúa el cursor en una posición absoluta sobre el archivo.

El tamaño del archivo lo podemos establecer con los métodos:

long length() retorna la longitud del archivo en bytes. void setLength(long len) pone una nueva longitud al archivo. Si es una longitud menor que la

actual, el archivo se trunca.

2. Compresión zip y jar.

Los paquetes java.util.zip y java.util.jar contienen clases e interfaces para poder comprimir archivos a estos dos formatos y posteriormente descomprimirlos.

2.1. Compresión zip.

Los archivos ".zip" son archivos que contienen archivos comprimidos y varios programas. Se utilizan sobre todo para intercambiar datos a través de internet porque reducen notablemente los tamaños. Hay varios tipos de archivos comprimidos y varios programas para comprimir y ampliar los datos: en archivos RAR, en los CAB de Windows, en los ARJ. 2.1.1. El paquete «java.util.zip». El paquete java.util.zip proporciona clases para tratar datos comprimidos según los estándares ZIP y GZIP, que utilizan el algoritmo de compresión llamado DEFLATE. Este paquete incluye también utilidades que controlan los códigos checksum CRC-32 y Adler-32 de un flujo arbitrario de entrada. La interfaz que incluye este paquete es:

• Checksum, es una interfaz que representa el código checksum de los datos. Las clases que incluye este paquete son:

• CheckedInputStream, es un canal de entrada para recibir datos que trata también el checksum de los mismos. • CheckedOutputStream, es un canal de salida que trata también el checksum de los mismos. • CRC32, clase usada para calcular el código checksum de tipo CRC-32 de un flujo de datos. • Deflater, clase que se ocupa de las compresiones de los datos utilizando la compresión según la biblioteca ZLIB. • DeflaterOutputStream, flujo de salida que comprime los datos utilizando el algoritmo Deflate. • GZIPInputStream, filtro para el stream de ingreso para leer datos comprimidos según el formato GZIP. • GZIPOutputStream, filtro para el flujo de salida para escribir datos con el código zip utilizando GZIP. • Inflater, soporte para la compresión de tipo ZLIB. • InflaterInputStream, filtro para el flujo de entrada, para ampliar datos comprimidos según el algoritmo Deflate. • ZipEntry, utilizada para representar un archivo de entrada de tipo ZIP. • ZipFile, utilizada para leer el contenido de un archivo ZIP. • ZipInputStream, utilizada para leer los archivos contenidos en un archivo ZIP. • ZipOutputStream, utilizada para escribir datos comprimidos de formato ZIP.

Las clases de excepciones que son tratadas por estas clases son: • DataFormatException, error de formato de los datos. • ZipException, error en alguna operación.

En este manual no se hará un repaso de todos los métodos de estas clases. Sólo se mostrará un ejemplo de cómo comprimir y descomprimir un archivo zip.

Page 134: Fundamentos de JAVA

Fundamentos de Java /134

2.1.2. Ejemplo de compresión de archivos a zip. El siguiente ejemplo muestra cómo comprimir todos los archivos de una carpeta a un archivo zip. import java.util.*;

import java.util.zip.*;

import java.io.*;

public class ComprimirZip {

public static void main(String [] args) {

File archivoZip = new File("./miArchivo.zip"); // Ruta del archivo zip.

File carpeta = new File ("./unaCarpeta"); // Carpeta que se quiere comprimir

try {

comprime( carpeta, archivoZip );

} catch (IOException e1) { // excepción al comprimir el archivo

return;

}

}

public static void comprime( File carpeta, File archivo ) throws IOException {

FileInputStream in = null; // canal de entrada para leer un archivo

FileOutputStream destino = new FileOutputStream( archivo ); // canal de salida al archivo comprimido

ZipOutputStream out = new ZipOutputStream( destino ); // canal para comprimir al archivo de salida

for (File f : carpeta.listFiles() ) { // se itera sobre cada archivo de la carpeta

if ( f.isFile() ) {

in = new FileInputStream( f ); // se abre el canal con el archivo actual

ZipEntry entry = new ZipEntry( f.getName() ); // se crea una entrada para el archivo actual

out.putNextEntry( entry ); // se añada la entrada al archivo comprimido

int ch;

while ((ch = in.read()) != -1) { // se leen los bytes del archivo actual

out.write(ch); // y se escriben al archivo comprimido

}

in.close(); // se cierra el canal de entrada

}

}

out.close(); // se cierra el archivo comprimido

}

}

2.1.3. Ejemplo de descompresión de un archivo zip. El siguiente ejemplo muestra cómo leer el contenido de un archivo zip. import java.util.*;

import java.util.zip.*;

import java.io.*;

public class LeerZip {

public static void main(String [] args) {

File archivoZip = new File("./miArchivo.zip"); // Ruta del archivo zip.

abreArchivoZip( archivoZip );

}

private static void abreArchivoZip (File archivo ) {

ZipFile zFile = null;

try {

zFile = new ZipFile(archivo); // se instancia un ZipFile a partir de la ruta del archivo

} catch (ZipException e) { // excepción por el formato del archivo zip

return;

} catch (IOException e1) { // excepción al abrir el archivo

return;

}

Enumeration files = zFile.entries(); // se obtiene una enumeración de las entradas contenidas

while( files.hasMoreElements() ) {

ZipEntry ze = (ZipEntry) files.nextElement(); // referencia a la entrada actual

boolean esDirectorio = ze.isDirectory(); // indica si la entrada es un directorio

long sizeComprimido = ze.getCompressedSize(); // tamaño del archivo comprimido

Page 135: Fundamentos de JAVA

Fundamentos de Java /135

long sizeOriginal = ze.getSize(); // tamaño del archivo original

String comentarios = ze.getComment(); // comentarios opcionales

long cheksum = ze.getCrc(); // el valor de checksum

byte [] datosExtra = ze.getExtra(); // bytes extras

int metodoCompresion = ze.getMethod(); // método de compresión o -1

String nombre = ze.getName(); // nombre de la entrada

Date fecha = new Date(ze.getTime()); // fecha de modificación de la entrada

}

}

}

El siguiente ejemplo muestra cómo descomprimir un archivo zip. import java.util.*;

import java.util.zip.*;

import java.io.*;

public class DescomprimirZip {

public static void main(String [] args) {

File archivoZip = new File("./miArchivo.zip"); // Ruta del archivo zip.

try {

descomprime( archivoZip, "./" );

} catch (IOException e1) { // excepción al descomprimir el archivo

return;

}

}

public static void descomprime( File archivo , String carpeta ) throws IOException {

ZipFile zFile = null;

try {

zFile = new ZipFile ( archivo ); // se instancia un ZipFile a partir de la ruta del archivo

} catch (ZipException e) {

throw new IOException(e);

}

Enumeration files = zFile.entries(); // se obtiene una enumeración de las entradas contenidas

while( files.hasMoreElements() ) {

ZipEntry tmpFile = (ZipEntry ) files.nextElement(); // referencia a la entrada actual

InputStream in = zFile.getInputStream(tmpFile); // el canal de entrada de los datos

// se crea un canal de salida para escribir la entrada descomprimida

// en la carpeta dada y en un archivo con el nombre de la entrada.

FileOutputStream out = new FileOutputStream(carpeta + tmpFile.getName());

int ch;

do {

ch = in.read(); // se lee un caracter del canal de entrada.

if ( ch != -1 )

out.write( ch ); // se escribe el caracter al canal de salida.

} while ( ch != -1 ); // finaliza si no hay caracteres para leer

out.close(); // se cierra el canal de salida

in.close(); // se cierra el canal de entrada

}

}

}

2.2. Compresión jar.

Los archivos JAR se basan en el estándar ZIP, con un archivo opcional llamado manifesto. Es un formato propio que utiliza Sun para encapsular todas las clases y recursos de una aplicación en un único archivo. 2.2.1. El paquete «java.util.jar». El paquete java.util.jar pone a disposición del programador clases para tratar los archivos de tipo Java Archive (JAR), sobre todo para leerlos y escribirlos. El contenido del paquete es el siguiente:

• Attributes, esta clase mapea los nombre de atributos del manifiesto a valores string asociados. • Attributes.Name, representa cada nombre de atributos almacenado

Page 136: Fundamentos de JAVA

Fundamentos de Java /136

• JarEntry, representa una entrada del archivo comprimido JAR. • JarFile, se usa para leer el contenido de un archivo jar desde otro archivo que puede ser abierto con java.io.RandomAccessFile. Extiende a la clase java.util.zip.ZipFile para soportar la lectura de la entrada opcional del manifiesto. • JarInputStream, utilizada para leer los archivos contenidos en un archivo JAR. • JarOutputStream, utilizada para leer los escribir contenidos en un archivo JAR. • Manifest, se usa para mantener las entradas del manifiesto y sus atributos asociados. • JarException, representa una excepción lanzada por una operación fallida.

Al ser el formato JAR una variante del formato ZIP, la forma de leer y escribir archivos jar es análoga a leer y escribir archivos ZIP.

3. Serialización.

Para enviar objetos desde nuestros programas a otras localizaciones necesitamos convertirlos a un formato apropiado. Java proporciona clases predefinidas para convertir datos a formatos que son portables, o fácilmente transportables a otras localizaciones. Este proceso de conversión de datos a un formato portable es denominado serialización. A veces podemos necesitar restaurar el objeto o dato serializado a su forma original. Este proceso de restauración se denomina deserialización. Para comprender el concepto de serialización y deserialización mejor podemos considerar una aplicación de comercio electrónico que almacena compras y detalles de productos. Estos detalles son gestionados por un servidor, y los detalles relativos al pago son gestionados por otro servidor. Los detalles de compra son mantenidos a través de objetos de la clase Cliente. Los productos y sus precios son almacenados como un estado de un objeto de la clase Cliente, además de otra información como el número de tarjeta de crédito. Necesitamos enviar el número de la tarjeta de crédito a un servidor remoto que gestiona todos los detalles financieros. Para hacer esto necesitamos convertir el número de tarjeta a un formato portable antes de enviarlo al servidor remoto. El servidor remoto reconstruye estos datos en un objeto apropiado. La aplicación que reside en el servidor remoto usa este objeto para recuperar la información, como el producto y el número de tarjeta para continuar con la transacción. En este ejemplo, el proceso de conversión de la información en un formato portable es la serialización y el proceso de restauración del objeto o dato a su estado original es la deserialización.

3.1. ¿Qué es serialización y deserialización?

Para poder almacenar los datos de una aplicación en una localización necesitamos convertirlos a un formato común, como a una serie de bytes. El proceso de convertir objetos y datos a un formato común para su almacenaje y transporte es llamado serialización. Java proporciona la clase ObjectOutputSteam para gestionar la serialización de objetos y datos. Cuando los datos son recuperados, el código que recupera los datos serializados debe identificar cómo están representados. Si los datos serializados representan un objeto, el código que los recupera debe poder convertirlos al objeto original. Este proceso es denominado deserialización y Java proporciona la clase ObjectInputStream para gestionarla.

3.2. Interfaz «Serializable».

Para que un objeto sea serializable su clase debe implementar la interfaz Serializable, la cual no define ningún método. Por ejemplo: class MiClase implements Serializable { . . . }

Para serializar un objeto se utiliza el método writeObject() de la clase ObjectOutputStream. Para deserializar un objeto previamente guardado se utiliza el método readObject() de la clase ObjectInputStream. En el siguiente código se guarda y se recupera un objeto de la clase MiClase en el archivo "datos.ser". // Se guarda un objeto del tipo MiClase

ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ( "datos.ser" ) );

MiClase mc1 = new MiClase ( );

oos.writeObject ( mc1 );

oos.close();

// Se recupera el objeto previamente guardado

ObjectInputStream ois = new ObjectInputStream (new FileInputStream ( “datos.dat” ) );

MiClase mc2 = (MiClase) ois.readObject ( );

Page 137: Fundamentos de JAVA

Fundamentos de Java /137

ois.close();

Debemos tener en cuenta que readObject() devuelve un Object sobre el que se deberá hacer un moldeo para que el objeto devuelto sea utilizable.

3.3. Características de la serialización.

Al serializar un objeto automáticamente se serializan todas sus variables y objetos miembro. A su vez se serializan los que estos objetos miembro puedan tener (todos deben ser serializables). También se reconstruyen de igual manera. Las consideraciones a tener en cuenta sobre el proceso de serialización son las siguientes:

• Para que funcione la serialización de un objeto, es necesario que todas las variables miembros de su clase sean serializables. • Se pueden serializar vectores y colecciones. Si dos elementos referencian el mismo objeto, el objeto se guarda una sola vez y se mantienen las referencias. • El modificador transient permite indicar que un objeto o variable miembro no sea serializable. Al recuperar un objeto, lo marcado como transient tomará valor 0, null o false. • Las variables y objetos static no se serializan. Deberá escribirse código adicional para hacerlo, incluyendo los siguientes métodos en la clase serializable:

private void writeObject (ObjectOutputStream s) throws IOException

private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException

Ejemplo para serializar variables estáticas. class MiClase implements Serializable {

static double varX = 3.416;

static int varY = 10;

private void writeObject(ObjectOutputStream s) throws IOException {

s.defaultWriteObject(); //guarda el objeto

s.writeDouble(varX); //guarda varX

s.writeInteger(varY); //guarda varY

};

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {

s.defaultReadObject(); //lee el objeto

varX = s.readDouble(); //recupera el valor varX

varY = s.readInteger(); //recupera el valor varY

}

}

Debemos destacar que la primera instrucción del método writeObject() es una invocación al método defaultWriteObject(). Este método aplica la serialización binaria por defecto, y posteriormente se guardan los datos adicionales. Análogamente, la primera instrucción del método readObject() es una invocación al método defaultReadObject(). Este método garantiza la deserialización por defecto, y posteriormente se recuperan los datos adicionales en el mismo orden en que se guardaron. Una vez incluidos estos dos métodos, son invocados automáticamente por el proceso de serialización y deserialización. Se pueden utilizar también los métodos writeObject() y readObject() para modificar el valor o formato de los datos antes de serializarlos y después de deserializarlos. Por ejemplo, en la siguiente clase, el campo descripcion es limpiado de espacios en blanco innecesarios antes de ser serializado y si su valor es null es deserializado como un string vacío. class MiClase implements Serializable {

private String descripcion;

private void writeObject(ObjectOutputStream s) throws IOException {

if (this.descripcion != null)

this.descripcion = this.descripcion.trim();

s.defaultWriteObject(); //guarda el objeto

};

private void readObject(ObjectInputStream s) throws IOException, classNotFoundException {

s.defaultReadObject(); //lee el objeto

if (this.descripcion == null)

this,descripcion = ""

}

Page 138: Fundamentos de JAVA

Fundamentos de Java /138

}

3.4. Interfaz «Externalizable».

La interfaz Externalizable extiende a la interfaz Serializable. Hace lo mismo que ésta, pero no tiene un comportamiento automático; el programador debe rescribir sus métodos public void writeExternal(ObjectOutput out) throw IOException

public void readExternal(ObjectInput in) throw IOException, ClassNotFoundException

para escribir y leer respectivamente los objetos. Al transformar un objeto, el método writeExternal() es responsable de todo lo que se hace. Sólo se guarda lo que dentro de este método se indique. El método readExternal() debe ser capaz de recuperar lo guardado en el mismo orden en que fue escrito. Importante: El proceso de deserialización utiliza el constructor por defecto de la clase antes de invocar el método readExternal(). Si la clase no posee un constructor sin argumentos se lanzará una excepción del tipo ClassNotFoundException. Por ejemplo, el siguiente código define un objeto Persona cuyos datos serán serializados a un formato de texto XML, mediante etiquetas que encapsulan los datos. class Persona implements java.io.Externalizable {

private String nif;

private String nombre;

. . .

public void writeExternal(ObjectOutput out) throws IOException {

out.writeChars("<Persona>\n");

out.writeChars("<nif>\n" + nif + "\n</nif>\n");

out.writeChars("<nombre>\n" + nombre + "\n</nombre>\n");

out.writeChars("</Persona>");

}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

in.readLine(); // lee "<Persona>"

in.readLine(); // lee "<nif>"

nif = in.readLine(); // recupera el nif

in.readLine(); // lee "</nif>"

in.readLine(); // lee "<nombre>"

nombre = in.readLine(); // recupera el nombre

}

}

El código para serializar y deserializar instancias de una clase Externalizable es el mismo que para serializar y deserializar una clase Serializable.

3.5. Serialización a formato XML.

El estándar XML (Extensible Markup Language) es un formato de texto simple y muy flexible derivado de SGML (Standard Generalizad Markup Language). XML fue diseñado para describir datos y actualmente tiene mucha importancia en el intercambio de una gran variedad de datos en el Web. Desde Java 2 es posible serialziar objetos como documentos XML. Java ofrece la clase java.beans.XMLEncoder para permitir la persistencia de objetos como documentos XML. Esta clase se encarga de convertir el objeto y todos sus datos (incluidos los campos que también son objetos) a un documento XML. Por su parte, la clase java.beans.XMLDecoder se encarga de deserializar los documentos XML generados por XMLEncoder. 3.5.1. Uso de XMLEncoder: La clase java.beans.XMLEncoder se puede utilizar para crear un documento XML que describa el estado de un componente JavaBean, de igual manera que la clase ObjectOutputStream se usa para crear archivos binarios que representan objetos serializables. Como ejemplo, supongamos la siguiente clase: public class Empleado {

private String nombre;

private String telefono;

private int antiguedad;

public Empleado() {

}

Page 139: Fundamentos de JAVA

Fundamentos de Java /139

public Empleado(String nombre, String telefono, int antiguedad) {

this.nombre = nombre;

this.telefono = telefono;

this.antiguedad = antiguedad;

}

public String getNombre() {

return nombre;

}

public void setNombre(String nombre) {

this.nombre = nombre;

}

public String getTelefono() {

return telefono;

}

public void setTelefono(String telefono) {

this.telefono = telefono;

}

public int getAntiguedad() {

return antiguedad;

}

public void setAntiguedad(int antiguedad) {

this.antiguedad = antiguedad;

}

}

Para poder serializar la clase Empleado a formato XML no es necesario que implemente ninguna interfaz, pero si es necesario que cumpla las condiciones de un JavaBean: debe ser una clase pública, debe tener un constructor sin argumentos, y usar métodos accesores para acceder a las propiedades que definen su estado. El siguiente código crearía un documento XML llamado "Test.xml" donde se serializa un objeto Empleado: try {

XMLEncoder e = new XMLEncoder( new BufferedOutputStream( new FileOutputStream("Test.xml") ) );

e.writeObject(new Empleado("Juan Pérez", "981111111", 8));

e.close();

} catch (FileNotFoundException ex) {

}

El contenido del archivo "Test.xml" será: <?xml version="1.0" encoding="UTF-8"?>

<java version="1.6.0_07" class="java.beans.XMLDecoder">

<object class="javaapplication3.Empleado">

<void property="antiguedad">

<int>8</int>

</void>

<void property="nombre">

<string>Juan Pérez</string>

</void>

<void property="telefono">

<string>981111111</string>

</void>

</object>

</java>

XMLEncoder trabaja reproduciendo el grafo del objeto y registrando los pasos que fueran necesarios para crear la copia. De esta manera XMLEncoder tiene una “copia de trabajo” del grafo del objeto que imita los pasos que XMLDecoder tomaría para descifrar el archivo. Supervisando el estado de esta copia de trabajo, el codificador puede omitir las operaciones que fijarían valores de característica a su valor prefijado, elaborando documentos breves con poca información redundante. 3.5.2. Uso de XMLDecoder: La clase java.beans.XMLDecoder se puede utilizar para recuperar un objeto serializado en un documento XML, de igual manera que la clase ObjectInputStream se usa para deserializar archivos binarios que representan objetos serializables.

Page 140: Fundamentos de JAVA

Fundamentos de Java /140

Continuando con el ejemplo previo, el siguiente código deserialzia el objeto Empleado del archivo "Test.xml": try {

XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("c:\\Test.xml")));

Empleado e = (Empleado) d.readObject();

d.close();

} catch (FileNotFoundException ex) {

}

3.5.3. ¿Cómo serializar clases que no son JavaBeans? Aquellas clases que no posean un constructor sin argumentos también pueden ser serialziadas a formato XML. Para ello debemos especificar un delegado de persistencia para XMLEncoder. El delegado de persistencia permite especificar el nombre de las propiedades que son pasadas en el constructor del objeto. Dichas propiedades deben poseer sus correpondientes métodos accesor. El sigiente código establece un delegado de persistencia para serializar un objeto Empleado: XMLEncoder e = new XMLEncoder(System.out);

e.setPersistenceDelegate(Empleado.class,

new DefaultPersistenceDelegate( new String[] { "nombre", "telefono", "antiguedad" } ) );

e.writeObject( new Empleado("Juan Pérez", "981111111", 8) );

e.close();

La salida será: <?xml version="1.0" encoding="UTF-8"?>

<java version="1.6.0_07" class="java.beans.XMLDecoder">

<object class="javaapplication3.Empleado">

<string>Juan Pérez</string>

<string>981111111</string>

<int>8</int>

</object>

</java>

Page 141: Fundamentos de JAVA

Fundamentos de Java /141

VI - HILOS DE EJECUCIÓN

Un subproceso es una tarea que se ejecuta en paralelo al resto de procesos de una aplicación. Se le denomina también hilo de ejecución (o thread).

1. Implementación de hilos.

Considerando el entorno multihilo, cada hilo representa un proceso individual ejecutándose en un sistema. A veces se les llama procesos ligeros o contextos de ejecución. Normalmente cada hilo controla un único aspecto dentro de un programa, como puede ser supervisar la entrada en un determinado periférico o controlar toda la entrada/salida del disco. Todos los hilos comparten los mismos recursos, al contrario que los procesos, en donde cada uno tiene su propia copia de código y datos (separados unos de otros). Gráficamente, los hilos se parecen en su funcionamiento a lo que muestra la figura siguiente:

Hay que distinguir multihilo (multithread) de multiproceso. El multiproceso se refiere a dos programas que se ejecutan "aparentemente" a la vez bajo el control del Sistema Operativo. Los programas no necesitan tener relación unos con otros, simplemente el hecho de que el usuario desee que se ejecuten a la vez. Multihilo se refiere a que dos o más tareas se ejecutan "aparentemente" a la vez dentro de un mismo programa. Tanto en el multiproceso como en el multihilo (multitarea), el Sistema Operativo se encarga de que se genere la ilusión de que todo se ejecuta a la vez. Sin embargo, la multitarea puede producir programas que realicen más trabajo en la misma cantidad de tiempo que el multiproceso, debido a que la CPU está compartida entre tareas de un mismo proceso. Además, como el multiproceso está implementado a nivel de sistema operativo, el programador no puede intervenir en el planteamiento de su ejecución; mientras que en el caso del multihilo, como el programa debe ser diseñado expresamente para que pueda soportar esta característica, es imprescindible que el autor tenga que planificar adecuadamente la ejecución de cada hilo o tarea. Actualmente hay diferencias en la especificación del intérprete de Java, porque el intérprete de Windows conmuta los hilos de igual prioridad mediante un algoritmo circular (round-robin), mientras que el de Solaris 2.X deja que un hilo ocupe la CPU indefinidamente, lo que implica la inanición de los demás.

1.1. Programas de flujo único.

Un programa de flujo único o mono-hilo (single-thread) utiliza un único flujo de control para controlar su ejecución. Muchos programas no necesitan la potencia o utilidad de múltiples flujos de control. Sin necesidad de especificar explícitamente que se quiere un único flujo de control, muchos de los applets y aplicaciones son de flujo único. Por ejemplo, en la archiconocida aplicación estándar de saludo: public class HolaMundo {

static public void main( String args[] ) {

System.out.println( "Hola Mundo!" );

}

}

Aquí, cuando se llama a main(), la aplicación imprime el mensaje y termina. Esto ocurre dentro de un único hilo de ejecución. Debido a que la mayor parte de los entornos operativos no solían ofrecer un soporte razonable para múltiples hilos de control, los lenguajes de programación tradicionales, tales como C++, no incorporaron mecanismos para describir de manera elegante situaciones de este tipo. La sincronización entre las múltiples partes de un programa se llevaba a cabo mediante un bucle de suceso único. Estos entornos son de tipo sincrónico, gestionados por eventos.

Page 142: Fundamentos de JAVA

Fundamentos de Java /142

1.2. Programas de flujo múltiple.

En la aplicación de saludo no se ve el hilo de ejecución que corre el programa. Sin embargo, Java posibilita la creación y control de hilos de ejecución explícitamente. La utilización de hilos en Java permite una enorme flexibilidad a los programadores a la hora de plantearse el desarrollo de aplicaciones. La simplicidad para crear, configurar y ejecutar hilos de ejecución permite que se puedan implementar aplicaciones/applets muy poderosas y portables, cosa que con otros lenguajes de tercera generación sería muy difícil. En un lenguaje orientado a Internet como es Java, esta herramienta es vital. Mientras que los programas de flujo único pueden realizar su tarea ejecutando las subtareas secuencialmente, un programa multihilo permite que cada hilo comience y termine tan pronto como sea posible. Este comportamiento presenta una mejor respuesta a las entradas en tiempo real. Hay dos formas de convertir una clase en un hilo:

1) Que extienda la clase java.lang.Thread y rescriba el método run() de dicha clase. 2) Que implemente la interfaz java.lang.Runnable e implemente el método run() de dicha interfaz. Después debe crear un objeto de tipo Thread y a su constructor pasarle como argumento una instancia de la clase.

Para ejecutar el hilo debe invocarse a su método start(). Vamos a modificar el programa de saludo creando tres hilos de ejecución individuales, que imprimen cada uno de ellos su propio mensaje de saludo, MultiHola.java: // Definimos unos sencillos hilos. Se detendrán un rato antes de imprimir sus nombres y retardos

class TestTh extends Thread {

private String nombre;

private int retardo;

// Constructor para almacenar nuestro nombre y el retardo

public TestTh( String s, int d ) {

nombre = s;

retardo = d;

}

// El metodo run() es similar al main(), pero para hilos. Cuando run() termina el hilo muere

public void run() {

// Retasamos la ejecución el tiempo especificado

try { sleep( retardo ); } catch( InterruptedException e ) { }

// Ahora imprimimos el nombre

System.out.println( "Hola Mundo! "+nombre+" "+retardo );

}

}

public class MultiHola {

public static void main( String args[] ) {

TestTh t1, t2, t3;

// Creamos los hilos

t1 = new TestTh( "Thread 1",(int)(Math.random()*2000) );

t2 = new TestTh( "Thread 2",(int)(Math.random()*2000) );

t3 = new TestTh( "Thread 3",(int)(Math.random()*2000) );

// Arrancamos los hilos

t1.start();

t2.start();

t3.start();

}

}

1.3. La clase «Thread».

La clase java.lang.Thread encapsula todo el control necesario sobre los hilos de ejecución. Aunque hay que distinguir claramente un objeto Thread de un hilo de ejecución. Esta distinción resulta complicada, aunque se puede simplificar si se considera al objeto Thread como el panel de control de un hilo de ejecución (thread). La clase Thread es la única forma de controlar el comportamiento de los hilos y para ello se sirve de los métodos que se exponen en las secciones siguientes. 1.3.1. Métodos de clase. Éstos son los métodos estáticos de la clase Thread.

Page 143: Fundamentos de JAVA

Fundamentos de Java /143

Thread currentThread() Devuelve el objeto thread que representa al hilo de ejecución que está ejecutando el código que invoca a esta instrucción.

void yield() Hace que el intérprete dé más preferencia a otros hilos que el hilo en cuyo código se invoca este método. Es una manera de asegurar que los hilos de menor prioridad no sufran inanición.

void sleep( long ms) Provoca que el intérprete ponga al hilo en curso a dormir durante el número de milisegundos que se indiquen en el parámetro de invocación. Una vez transcurridos esos milisegundos, dicho hilo volverá a estar disponible para su ejecución. Los relojes asociados a la mayor parte de los intérpretes de Java no serán capaces de obtener precisiones mayores de 10 milisegundos, por mucho que se permita indicar hasta nanosegundos en la llamada alternativa a este método.

1.3.2. Métodos de instancia. Aquí no están recogidos todos los métodos de la clase Thread, sino solamente los más interesantes.

void start() Este método indica al intérprete de Java que cree un contexto del hilo del sistema y comience a ejecutarlo. A continuación, el método run() de este hilo será invocado en el nuevo contexto del hilo. Hay que tener precaución de no llamar al método start() más de una vez sobre un hilo determinado.

void run() Este método constituye el cuerpo de un hilo en ejecución. Es el único método de la interfaz Runnable. Es llamado por el método start() después de que el hilo correspondiente del sistema se haya inicializado. Después de que el método run() finalice su ejecución, el hilo actual se detendrá y ya no podrá volver a ser ejecutado.

void stop() Provoca que el hilo se detenga de manera inmediata. A menudo constituye una manera brusca de detener un hilo, especialmente si este método se ejecuta sobre el hilo en curso. En tal caso, la línea inmediatamente posterior a la llamada al método stop() no llega a ejecutarse jamás, pues el contexto del hilo muere antes de que stop() devuelva el control. Una forma más elegante de detener un hilo hacer que el método run() termine de manera ordenada. En realidad, nunca se debería recurrir al uso de este método.

void join()

void join(milisegundos) Provoca que el código que invoca este método espere a que el hilo muera. Si se pasa un parámetro, el código se reanuda si el hilo muere o pasan los milisegundos indicados (cero milisegundos indican una espera indefinida). Una vez que un proceso principal invoca el método join() de un hilo, tiene que esperar a que finalice la ejecución de dicho hilo o transcurra el tiempo indicado.

void suspend() Provoca que se detenga la ejecución de hilo sin destruirlo del sistema subyacente. Si la ejecución de un hilo se suspende, puede llamarse a resume() sobre el mismo hilo para lograr que vuelva a ejecutarse de nuevo.

void resume() Se utiliza para revivir un hilo suspendido. No hay garantías de que el hilo comience a ejecutarse inmediatamente, ya que puede haber un hilo de mayor prioridad en ejecución actualmente, pero resume() ocasiona que el hilo vuelva a ser un candidato a ser ejecutado.

void setPriority( int

prior) Asigna al hilo la prioridad indicada por el valor pasado como parámetro. Hay bastantes constantes predefinidas para la prioridad, definidas en la clase Thread, tales como MIN_PRIORITY, NORM_PRIORITY y MAX_PRIORITY, que toman los valores 1, 5 y 10, respectivamente. Como guía aproximada de utilización, se puede establecer que la mayor parte de los procesos a nivel de usuario deberían tomar una prioridad en torno a NORM_PRIORITY. Las tareas en segundo plano, como una entrada/salida a red o el nuevo dibujo de la pantalla, deberían tener una prioridad cercana a MIN_PRIORITY. Con las tareas a las que se fije la máxima prioridad, en torno a MAX_PRIORITY, hay que ser especialmente cuidadosos, porque si no se hacen llamadas a sleep() o

Page 144: Fundamentos de JAVA

Fundamentos de Java /144

yield(), se puede provocar que el intérprete Java quede totalmente fuera de control.

int getPriority() Devuelve la prioridad del hilo de ejecución en curso, que es un valor comprendido entre uno y diez.

void setName( String

name) Permite identificar al hilo con un nombre mnemónico. De esta manera se facilita la depuración de programas multihilo. El nombre mnemónico aparecerá en todas las líneas de trazado que se muestran cada vez que el intérprete Java imprima excepciones no capturadas.

String getName() Este método devuelve el valor actual, de tipo string, asignado como nombre al hilo en ejecución mediante setName().

int getState() Este método retorna el estado del hilo. El valor retornado es una de las constantes de la enumeración java.lang.Thread.State:

• State.NEW indica que el hilo ha sido creado pero todavía no ha sido ejecutado. • State.RUNNABLE indica que el hilo está en ejecución. • State.BLOCKED indica que el hilo ha sido bloqueado y esperando se desbloqueado por un monitor. • State.WAITING indica que el hilo está esperando indefinidamente a que otro hilo realice una acción determinada. • State.TIMED_WAITING indica que el hilo está esperando a que otro hilo realice una acción o transcurra un tiempo determinado. • State.TERMINATED indica que el hilo está muerto.

Un hilo sólo puede estar en uno de estos estados de cada vez.

1.4. Creación de un hilo de ejecución.

Como hemos visto, hay dos modos de conseguir hilos de ejecución en Java. Una es extender la clase Thread, y la otra es implementando la interfaz Runnable. El primer método de crear un hilo de ejecución es simplemente extender la clase Thread: class MiThread extends Thread {

public void run() {

// código a ejecutar por el hilo.

}

public static void main(String[] args) {

MiThread t = new MiThread(); // se instancia la clase

t.start(); // y se lanza el hilo

}

}

El ejemplo anterior crea una nueva clase MiThread que extiende la clase Thread y sobrescribe el método Thread.run(). El método run() es donde se realizará todo el trabajo de la clase. Extendiendo la clase Thread se pueden heredar los métodos y variables de la clase padre. Sin embargo, en este caso, solamente se puede extender una clase (Thread) y no ninguna otra. Esta limitación de Java puede ser superada a través de la implementación de Runnable: public class MiThread implements Runnable {

public void run() {

// código a ejecutar por el hilo.

}

public static void main(String[] args) {

MiThread mt = new MiThread(); // se instancia la clase

Thread t = new Thread(mt); // se crea un hilo sobre la instancia

t.start(); // y se lanza

}

}

En este caso necesitamos crear una instancia de Thread antes de que el sistema pueda ejecutar el proceso como un hilo. Además, el método abstracto run() está definido en la interfaz Runnable y tiene que ser implementado.

Page 145: Fundamentos de JAVA

Fundamentos de Java /145

La única diferencia entre los dos métodos es que este último es mucho más flexible. En el ejemplo anterior, todavía está la oportunidad de extender la clase MiThread, si fuese necesario. La mayoría de las clases creadas que necesiten ejecutarse como un hilo implementarán la interfaz Runnable, ya que probablemente extenderán alguna funcionalidad de otra clase. La interfaz Runnable está definida de la siguiente forma: package java.lang;

public interface Runnable {

public void run() ;

}

Para ejecutar el código de método abstracto run() haremos uso del hilo creado por la clase que implementa Runnable. A continuación se presenta un ejemplo que implementa el interfaz Runnable para crear un programa multihilo. public class MiHilo implements Runnable {

private Thread unthread;

public String nombre;

public MiHilo (String nombre) {

this.nombre = nombre;

unthread = new Thread(this); // creamos el hilo pasando como argumento el objeto “runnable”

unthread.start(); // Se arranca el hilo, para que comience su ejecución

}

// método de la interfaz Runnable

public void run () {

System.out.print(“Hilo “ + nombre + “: “);

for (int i = 1; i <= 100; i++)

System.out.print( i + “ “);

}

public void main (String [] args) {

// se crea un objeto y automáticamente lanza el hilo

MiHilo mihilo = new MiHilo (“ejemplo”);

}

}

Como se puede observar, el programa define una clase MiHilo que implementa la interfaz Runnable. Se implementa el método run() para mostrar el nombre del hilo y los números del 1 al 100. Para ejecutar el hilo debemos instanciar un objeto Thread y en su constructor pasar como argumento la instancia de la clase (mediante this). De esta forma, al lanzar el hilo con el método start(), éste ejecuta el método run() de la clase MiHilo. En el siguiente código se muestra el mismo programa básicamente, pero en este caso extendiendo la clase Thread, en lugar de implementar el interfaz Runnable. public class MiHilo extends Thread {

MiHilo (String nombre) {

super(nombre);

}

public void run () {

System.out.print(“Hilo “ + nombre + “: “);

for (int i = 1; i <= 100; i++)

System.out.print( i + “ “);

}

public void main (String [] args) {

MiHilo mihilo = new MiHilo(“ejemplo”); // crea el hilo

mihilo.start(); // y se lanza

}

}

1.5. Manipulación de un hilo.

Supongamos creada la clase TestTh, que extiende la clase Thread y tiene un constructor con dos parámetros: el nombre del hilo y un tiempo de espera. public class TestTh extends Thread {

Page 146: Fundamentos de JAVA

Fundamentos de Java /146

int espera;

MiHilo (String nombre, int espera) {

super(nombre);

this.espera = espera;

}

public void run () {

try { Thread.currentThread().sleep( espera ); }catch( InterruptedException e ){ }

System.out.println( “Se ejecutó “ + this.getName());

}

public void main (String [] args) {

MiHilo mihilo = new MiHilo(“ejemplo”); // crea el hilo

mihilo.start(); // y se lanza

}

}

1.5.1. Arranque del hilo. La línea de código: t1 = new TestTh( "Hilo 1",(int)(Math.random()*2000) );

crea un nuevo hilo de ejecución, con el nombre “Hilo 1” y un tiempo de espera aleatorio. Los hilos hay que hay que arrancarlos explícitamente. En el ejemplo con: t1.start();

El método start() en realidad es un método oculto en el hilo de ejecución que llama a run(). Si todo fue bien en la creación del hilo, la variable t1 debería referenciar un hilo válido, que controlaremos en el método run(). Una vez dentro de run(), se pueden comenzar las sentencias de ejecución como en otros programas. El método run() sirve como rutina main() para los hilos; cuando run() termina, también lo hace el hilo. Todo lo que se quiera que haga el hilo de ejecución ha de estar dentro de run(), por eso cuando se dice que un método es Runnable, es obligatorio escribir un método run(). En este ejemplo se intenta inmediatamente esperar durante una cantidad de tiempo aleatoria (pasada a través del constructor). El método sleep() simplemente le dice al hilo de ejecución que duerma durante los milisegundos especificados. Se debería utilizar sleep() cuando se pretenda retrasar la ejecución del hilo, ya que no consume recursos del sistema mientras el hilo duerme. De esta forma otros hilos pueden seguir funcionando. Una vez hecho el retardo, se imprime un mensaje. 1.5.2. Suspensión del hilo. Puede resultar útil suspender la ejecución de un hilo sin marcar un límite de tiempo. Si, por ejemplo, se construye un applet con un hilo de animación, seguramente se querrá permitir al usuario la opción de detener la animación hasta que quiera continuar. No se trata de terminar la animación, sino desactivarla. Para este tipo de control de los hilos de ejecución se puede utilizar el método suspend(). t1.suspend();

Este método no detiene la ejecución permanentemente. El hilo es suspendido indefinidamente y para volver a activarlo de nuevo se necesita realizar una invocación al método resume(): t1.resume();

Estos métodos están definidos como obsoletos en las últimas versiones de Java. Su funcionalidad puede conseguir usando sincronización y objetos de bloqueo como se verá posteriormente. 1.5.3. Parada del hilo. El último elemento de control que se necesita sobre los hilos de ejecución es el método stop(). Se utiliza para terminar la ejecución de un hilo: t1.stop();

Esta llamada no destruye el hilo, sino que detiene su ejecución. La ejecución no se puede reanudar ya con t1.start(). Cuando se desasignen las variables que referencian el hilo, el objeto Thread (creado con new) quedará marcado para eliminarlo y el recolector de basura se encargará de liberar la memoria que utilizaba. Este método está definido como obsoleto en las últimas versiones de Java. Se recomienda parar un hilo haciendo que finalice la ejecución de su método run(). En el ejemplo no se necesita detener explícitamente el hilo de ejecución. Simplemente se le deja terminar. Los programas más complejos necesitarán un control sobre cada uno de los hilos que lancen, y el método stop() puede utilizarse en esas situaciones. Si se necesita, se puede comprobar si un hilo está vivo o no; considerando vivo un hilo que ha comenzado y no ha sido detenido.

Page 147: Fundamentos de JAVA

Fundamentos de Java /147

t1.isAlive();

Este método devolverá true en caso de que el hilo t1 esté vivo, es decir, ya se haya llamado a su método run() y no haya sido parado con un stop() ni haya terminado el método run() en su ejecución. Si a un hilo de ejecución, que puede no estar vivo, se le invoca su método stop(), se generará una excepción. En este caso, en los que el estado del hilo no puede conocerse de antemano es donde se requiere el uso del método isAlive().

2. Bloqueos y sincronización de hilos.

El problema de la sincronización de hilos tiene lugar cuando varios hilos intentan acceder al mismo recurso o dato. A la hora de acceder a datos comunes los hilos necesitan establecer cierto orden, por ejemplo en el caso de un subproceso productor de datos y un subproceso consumidor de dichos datos. Para asegurarse de que hilos concurrentes no se estorban y operen correctamente con datos (o recursos) compartidos, un sistema estable debe prevenir la inanición, el punto muerto y el interbloqueo. La inanición tiene lugar cuando uno o más hilos están bloqueados al intentar conseguir acceso a un recurso compartido de ocurrencias limitadas. El interbloqueo es la última fase de la inanición; ocurre cuando uno o más hilos están esperando una condición que no puede ser satisfecha. Esto ocurre muy frecuentemente cuando dos o más hilos están esperando a que el otro u otros se desbloquee respectivamente.

2.1. Acceso concurrente.

En una aplicación multihilo pude ocurrir que varios hilos invoquen simultáneamente un mismo método desde el código de sus respectivos métodos run(). En este caso se hablará de un método concurrente. Cuando un hilo ejecuta un método crea un entorno de ejecución para dicho método en la pila de llamadas, donde crea sus propias copias de las variables locales. Sin embargo, si el método concurrente trabaja con objetos no locales al propio método, pueden producirse efectos indeseables cuando varios hilos modifican a la vez un mismo dato concurrentemente. Un ejemplo típico del caso productor/consumidor es el de un proceso que introduce datos en una lista, y otro proceso retira datos de la misma lista. Si ambos procesos operan independientemente puede ocurrir que el proceso que retira intente quitar datos de la lista cuando ésta está vacía. Para prevenir estas condiciones, en nuestro ejemplo, el almacenamiento de un nuevo elemento en la lista por parte del Productor debe estar sincronizado con la recuperación del elemento por parte del Consumidor. Los programas productor-consumidor utilizan dos mecanismos diferentes para sincronizar los hilos: secciones críticas y monitores.

2.2. Secciones críticas.

Los objetos a los que acceden varios hilos son llamados “condiciones variables”. Las secciones críticas son los segmentos del código donde los hilos concurrentes acceden a las condiciones variables. Estas secciones, en Java, se marcan normalmente con la palabra reservada synchronized: Por ejemplo, el método consumidor podría ser como sigue: public Object remueve() {

Object dato = null;

synchronized(lista) { // El hilo se apropia del objeto lista. Si está marcado por otro hilo debe esperar

if ( ! lista.isEmpty()) // Si la lista no está vacía

dato = lista.get(0); // Lee el primer elemento

lista.remove(0); // y lo elimina

}

}

return dato;

}

El bloque synchronized lleva entre paréntesis la referencia a un objeto (que debe ser distinto de null). Cada vez que un hilo intenta acceder a un bloque sincronizado le pregunta a ese objeto si hay algún otro hilo que ya tenga el bloqueo para ese objeto. En otras palabras, le pregunta si no hay otro hilo ejecutando algún bloque sincronizado con ese objeto. Si el bloqueo está tomado por otro hilo, entonces el hilo actual es suspendido y puesto en espera hasta que el bloqueo se libere. Si el bloqueo está libre, entonces el hilo actual toma el bloqueo del objeto y entra a ejecutar el código del bloque sincronizado. Al tomar el bloqueo, cuando venga el próximo hilo a intentar ejecutar un bloque sincronizado con ese objeto, será puesto en espera.

Page 148: Fundamentos de JAVA

Fundamentos de Java /148

El bloqueo se libera cuando el hilo que lo tiene tomado finaliza el bloque sincronizado por cualquier razón: termina la ejecución del bloque normalmente, ejecuta un return o lanza una excepción. El hilo productor también debe sincronizarse sobre el mismo objeto. Por ejemplo, el método productor podría ser como sigue: public void inserta (Object dato) {

synchronized(lista) { // El hilo se apropia del objeto. Si está marcado por otro hilo debe esperar

lista.add(dato); // Añade un elemento a la lista

}

}

Esta sincronización provoca que mientras un proceso está insertando un dato en la lista (mediante inserta) ningún otro proceso puede quitar elementos de la lista (mediante remueve) y viceversa. Un único hilo puede ejecutar a la vez un bloque sincronizado sobre el mismo objeto. Podemos ocultar los bloques synchronized implementado métodos sincronizados: public class MiListaSincronizada {

private LinkedList lista = new LinkedList();

public synchronized void inserta(Object dato) {

lista.add(dato);

}

public synchronized Object remueve() {

Object dato = null;

if ( ! lista.isEmpty()) // Si la lista no está vacía

dato = lista.get(0); // Lee el primer elemento

lista.remove(0); // y lo elimina

}

return dato;

}

}

Los métodos sincronizados utilizan this como objeto de bloqueo. Por tanto, el método inserta anterior sería equivalente a: public void inserta(Object dato) {

synchronized(this) {

lista.add(dato);

}

}

Podemos forzar el desbloqueo de un hilo con el método interrupt() del propio hilo.

2.3. Monitores.

Otra de las formas de controlar el acceso a estas condiciones variables y de, por tanto, sincronizar los hilos, son los monitores. Los monitores actúan a través de un objeto de bloqueo mediante invocaciones a métodos wait() y notify(). Todos los objetos heredan el método wait() de la clase Object. Cuando un objeto invoca este método, el objeto bloquea el hilo desde donde es invocado y este hilo queda suspendido. El hilo reanudará su ejecución y saldrá del wait() cuando algún otro hilo invoque el método notify() o notifyAll() sobre el mismo objeto. Por ejemplo, podemos modificar el método consumidor para que sólo ejecute su código si hay algún dato en la lista: public Object remueve() {

Object dato = null;

synchronized(lista) { // El hilo se apropia del objeto lista y lo marca para otros hilos

if (lista.size()==0) // Si la lista está vacía

try {

lista.wait(); // el hilo se suspende hasta que se invoca un método notify() desde otro hilo

} catch (InterruptedException ex) { }

// el siguiente código se ejecuta cuando la lista es desbloqueada porque ya tiene elementos

dato = lista.get(0); // Lee el primer elemento

lista.remove(0); // y lo elimina

}

return dato;

}

Page 149: Fundamentos de JAVA

Fundamentos de Java /149

El método wait() sólo puede ser invocado desde un bloque synchronized sobre el mismo objeto. Además, la invocación de wait() provoca que otros bloques synchronized sobre el mismo objeto ya no provoquen bloqueos y se puedan ejecutar en paralelo. El hilo bloqueado por wait() reanudará su ejecución cuando algún otro hilo (en el ejemplo, aquel que mete datos en la lista) llame a lista.notify() o lista.notifyAll(). La invocación a notify también debe realizarse dentro un bloque synchronized. Por ejemplo, el método productor podría ser como sigue: public void inserta (Object dato) {

synchronized(lista) { // Si previamente se ha invocado lista.wait(), ya no se bloquea

lista.add(dato); // Añade un elemento a la lista

lista.notify(); // Notifica a otros hilos de que la lista ya tiene elementos

}

}

Los métodos wait() y notify() funcionan como una lista de espera. Si varios hilos van llamando a wait() quedan bloqueados y en una lista de espera por orden de llamada. Cada llamada a notify() despierta al primer hilo en la lista de espera, pero no al resto, que siguen dormidos. Podemos despertar a todos con notifyAll(). Las invocaciones a notify() no se acumulan. Si invocamos un notify() sin un wait() previo no tendrá ningún efecto sobre un wait() posterior.

3. Gestión de hilos.

3.1. Hilos Demonio (Daemon).

Un proceso demonio es un proceso que debe ejecutarse continuamente en modo “background” (en segundo plano), y generalmente se diseña para responder a peticiones de otros procesos a través de la red. Los hilos demonio también se llaman servicios, porque se ejecutan, normalmente, con prioridad baja y proporcionan un servicio básico a un programa o programas cuando la actividad de la máquina es reducida. Un ejemplo de hilo demonio que está ejecutándose continuamente es el recolector de basura (garbage collector). Este hilo, proporcionado por la Máquina Virtual Java, comprueba las variables de los programas a las que no se accede nunca y libera estos recursos, devolviéndolos al sistema. Un hilo puede fijar su indicador de demonio pasando un valor true al método setDaemon(). Si se pasa false a este método, el hilo será devuelto por el sistema como un hilo de usuario. No obstante, esto último debe realizarse antes de que se arranque el hilo con el método start().

3.2. Estados en el ciclo de vida de un thread.

El comportamiento de un hilo depende del estado en que se encuentre; este estado define su modo de operación actual, por ejemplo, si está ejecutándose o no. A continuación se describen los estados en los que puede encontrarse un hilo Java.

• Nuevo (New): Un hilo está en el estado new la primera vez que se crea y hasta que el método start() es llamado. Los hilos en estado new ya han sido inicializados y están listos para empezar a trabajar, pero aún no han sido notificados para que empiecen a realizar su trabajo. • Ejecutando (Runnable): Cuando se llama al método start() de un hilo nuevo el método run() es invocado y el hilo entra en el estado runnable. Sin embargo, debemos tener en cuenta la prioridad de los hilos. Aunque cada hilo está corriendo desde el punto de vista del usuario, en realidad todos los hilos, excepto el que en estos momentos está utilizando la CPU, están en el estado runnable (ejecutables, listos para correr) en cualquier momento dado. • No ejecutando (Not running): El estado not running se aplica a todos los hilos que están parados por alguna razón. Cuando un hilo está en este estado, está listo para ser usado y es capaz de volver al estado runnable en un momento dado. Los hilos pueden pasar al estado not running a través de varias vías:

- El método suspend() ha sido invocado.

Nuevo Muerto Ejecutando

Bloqueado Esperando

start() finaliza run()

stop()

sleep() suspend()

join()

operación I/O yield()

wait() notify()

resume() …

Page 150: Fundamentos de JAVA

Fundamentos de Java /150

- El método sleep() ha sido invocado. - El método wait() ha sido invocado. - El hilo está bloqueado por una operación de entrada salida con el sistema. Por ejemplo, se ha solicitado leer un archivo que en ese momento está siendo usado por otro proceso.

Para cada una de estas acciones hay una forma para hacer que el hilo vuelva a correr. - Si un hilo está suspendido, invocando el método resume(). - Si un hilo está durmiendo con el método sleep(), transcurridos los milisegundos que se ha especificado que debe dormir. - Si un hilo está esperando debido a una invocación de wait(), la llamada a notify() o notifyAll() por parte del objeto por el que espera. - Si un hilo está bloqueado por I/O, la finalización de la operación I/O en cuestión

• Muerto (Dead): Un hilo entra en estado dead cuando ya no es un objeto útil. Los hilos en estado dead no pueden ser resucitados y ejecutados de nuevo. Un hilo puede entrar en estado dead a través de dos vías:

- El método run() termina su ejecución. - El método stop() es llamado.

La primera opción es el modo natural de que un hilo muera. Una llamada al método stop() mata al hilo de modo asíncrono.

3.3. Agrupamiento de hilos.

Todo hilo de Java es un miembro de un grupo de hilos. Los grupos de hilos proporcionan un mecanismo de reunión de múltiples hilos dentro de un único objeto y de manipulación de dichos hilos en conjunto, en lugar de una forma individual. Por ejemplo, se pueden arrancar o suspender todos los hilos que están dentro de un grupo con una única llamada al método. Los grupos de hilos de Java están implementados por la clase java.lang.ThreadGroup. El hilo es un miembro permanente de aquel que sea el grupo de hilos al cual se unió en el momento de su creación. No puede moverse un hilo a un nuevo grupo una vez que ha sido creado. 3.3.1. El grupo de hilos por defecto. Cuando se arranca una aplicación Java, el intérprete de Java crea una instancia de la clase ThreadGroup llamada main. A menos que se especifique lo contrario, todos los nuevos hilos que se creen se convertirán en miembros del grupo de hilos main. 3.3.2. Creación de un hilo en un grupo de forma explícita. Si queremos poner un nuevo hilo en un grupo de hilos distinto del grupo por defecto, debemos especificarlo explícitamente cuando lo creemos. La clase Thread tiene tres constructores que permiten establecer un nuevo grupo de hilos. public Thread( ThreadGroup group, Runnable runnable )

public Thread( ThreadGroup group, String name )

public Thread( ThreadGroup group, Runnable runnable, String name )

Cada uno de estos constructores crea un nuevo hilo, lo inicializa en base a los parámetros Runnable y String, y hace al nuevo hilo miembro del grupo especificado. Por ejemplo, el siguiente código crea un grupo de hilos (myThreadGroup) y entonces crea un hilo (myThread) en dicho grupo: ThreadGroup myThreadGroup = new ThreadGroup( "Mi grupo de hilos" );

Thread myThread = new Thread( myThreadGroup, "un hilo de mi grupo" );

3.3.3. La clase «ThreadGroup». La clase ThreadGroup es la implementación del concepto de grupo de hilos en Java. Ofrece, por tanto, la funcionalidad necesaria para la manipulación de grupos de hilos para las aplicaciones Java. Un objeto ThreadGroup puede contener cualquier número de hilos. Los hilos de un mismo grupo generalmente se relacionan de algún modo, ya sea por quién los creó, por la función que llevan a cabo, o por el momento en que deberían arrancarse y parar. La clase ThreadGroup tiene métodos que pueden ser clasificados como sigue:

• Métodos de administración del grupo: métodos que manipulan la colección de hilos y subgrupos contenidos en el grupo de hilos.

El método activeCount() retorna el número de hilos activos que actualmente hay en el grupo. El método enumerate() retorna un array con las referencias a todos los hilos activos en el grupo.

• Métodos que operan sobre el grupo: estos métodos establecen u obtienen atributos del objeto ThreadGroup.

Page 151: Fundamentos de JAVA

Fundamentos de Java /151

Se incluyen métodos como getMaxPriority() y setMaxPriority(), getDaemon() y setDaemon(), getName(), getParent() y parentOf(), y toString().

• Métodos que operan sobre todos los hilos dentro del grupo: este es un conjunto de métodos que desarrollan algunas operaciones, como inicio y reinicio, sobre todos los hilos y subgrupos dentro del objeto ThreadGroup.

Estos métodos son resume(), stop() y suspend(). • Métodos de restricción de acceso: ThreadGroup y Thread permiten al administrador de seguridad restringir el acceso a los hilos en base a la relación de miembro/grupo con el grupo.

El método checkAccess() invoca al método checkAccess() del administrador de seguridad actual. El administrador de seguridad decide si debe permitir el acceso basándose en la relación de pertenencia entre el grupo y los hilos implicados. Si no se permite el acceso, el método checkAccess() lanza una excepción de tipo SecurityException.

3.4. Planificación y prioridad de hilos.

3.4.1. Planificación (Scheduling). Java tiene un planificador, una lista de procesos, que monitoriza todos los hilos que se han creado en todos los programas y decide cuáles deben ejecutarse y cuáles deben encontrarse preparados para su ejecución. Hay dos características de los hilos que el planificador tiene en cuenta en este proceso de decisión:

• La prioridad del hilo (la más importante). • El indicador de demonio.

La regla básica del planificador es que si solamente hay hilos demonio ejecutándose, la Máquina Virtual Java (JVM) concluirá. Los nuevos hilos heredan la prioridad y el indicador de demonio de los hilos que los han creado. El planificador determina qué hilos deberán ejecutarse comprobando la prioridad de todos los hilos. Aquellos con prioridad más alta dispondrán del procesador antes de los que tienen prioridad más baja. El planificador puede seguir dos patrones, preventivo y no preventivo. Los planificadores preventivos proporcionan un segmento de tiempo a todos los hilos que están corriendo en el sistema. El planificador decide cuál será el siguiente hilo a ejecutarse y llama a resume() para darle vida durante un período fijo de tiempo. Cuando finaliza ese período de tiempo, se llama a su método suspend() y el siguiente hilo en la lista de procesos será relanzado mediante su método resume(). Los planificadores no preventivos, en cambio, deciden qué hilo debe correr y lo ejecutan hasta que concluye. El hilo tiene control total sobre el sistema mientras esté en ejecución. El método yield() es un mecanismo que permite a un hilo forzar al planificador para que comience la ejecución de otro hilo que esté esperando. Dependiendo del sistema en que esté corriendo Java, el planificador será preventivo o no preventivo. 3.4.2. Prioridad. Cada hilo tiene una prioridad, que no es más que un valor entero entre 1 y 10, de modo que cuanto mayor el valor, mayor es la prioridad. El planificador determina el hilo que debe ejecutarse en función de la prioridad asignada a cada uno de ellos. Cuando se crea un hilo en Java, éste hereda la prioridad de su padre, el hilo que lo ha creado. A partir de aquí se le puede modificar su prioridad en cualquier momento utilizando el método setPriority(). Las prioridades de un hilo varían en un rango de enteros comprendido entre MIN_PRIORITY y MAX_PRIORITY (ambas constantes definidas en la clase Thread). Se ejecutará primero el hilo de prioridad superior, el llamado “Ejecutable”, y sólo cuando éste para, abandona o se convierte en “No ejecutable”, comienza la ejecución del hilo de prioridad inferior. Si dos hilos tienen la misma prioridad, el programador elige uno de ellos en alguna forma de competición. El hilo seleccionado se ejecutará hasta que:

- Un hilo con prioridad mayor pase a ser “Ejecutable”. - En sistemas que soportan tiempo compartido, termina su tiempo. - Abandone o termine su método run().

Luego un segundo hilo puede ejecutarse, y así continuamente hasta que el intérprete abandone.

3.5. Lanzador de tareas.

3.5.1. Clase «Timer». La clase java.util.Timer actúa como un lanzador de tareas que permite ejecutarlas una sólo vez o repetitivamente a intervalos de tiempo. Estas tareas deben extender la clase abstracta java.util.TimerTask.

Page 152: Fundamentos de JAVA

Fundamentos de Java /152

En el constructor de Timer podemos introducir un nombre e indicar si la tarea debe ejecutarse en primer plano o en segundo plano (tipo daemon). Mediante un método schedule() lanzaremos una tarea, pudiendo indicar el instante en que queremos que se ejecute o el intervalo de tiempo en que debe ejecutarse. Para detener el lanzador debemos ejecutar el método cancel(). Métodos de la clase Timer:

• Timer() • Timer(boolean esDaemon) • Timer(String nombre) • Timer(String nombre, boolean esDaemon)

Constructores del lanzador de tareas. • void schedule (TimerTask tarea, Date tiempo)

Lanza la tarea en un momento de tiempo dado. • void schedule (TimerTask tarea, Date inicio, long periodo)

Lanza la tarea a partir de un instante dado y relanzándola cada periodo de milisegundos. • void schedule (TimerTask tarea, long espera)

Lanza la tarea después de una espera de tiempo. • void schedule (TimerTask tarea, long espera, long periodo)

Lanza la tarea después de una espera, relanzándola en cada periodo (en milisegundos). • void scheduleAtFixedRate (TimerTask tarea, Date inicio, long periodo) • void scheduleAtFixedRate (TimerTask tarea, long espera, long periodo)

Lanzan la tarea para ejecuciones periódicas. • void cancel ()

Termina el lanzador, descargando todas las tareas. • int purge ()

Remueve y cancela todas las tareas del lanzador. 3.5.2. Clase «TimerTask». Las tareas que ejecuta un lanzadores de tareas derivan de la clase abstracta java.util.TimerTask y deben rescribir el método run() para incluir el código de la tarea. Métodos de la clase TimerTask:

• TimerTask() Constructor.

• boolean cancel () Cancela la tarea. Retorna true si la tarea se estaba ejecutando normalmente. Una tarea cancelada no puede volver a relanzarse.

• void run () Las subclases deben rescribir este método para incluir el código que queramos ejecutar.

• long scheduleExecutionTime () Retorna el tiempo de ejecución transcurrido en milisegundos.

Ejemplo que muestra la fecha completa del sistema cada segundo, sin tiempo de espera: . . .

java.util.Timer timer = new java.util.Timer();

timer.schedule( new TimerTask() {

public void run () {

System.out.println((new Date()).toString);

}

}, 0, 1000);

. . .

Page 153: Fundamentos de JAVA

Fundamentos de Java /153

VII - INTERFAZ GRÁFICA DE USUARIO

Cada programa se puede ver como un objeto que procesa algo, es decir, que coge unos datos del exterior y devuelve unos resultados. Por ejemplo, una programa que implementa una calculadora cogerá como input uno o dos números y devolverá como output un solo número que representa la operación especificada. Esto es válido en general para todas las aplicaciones. La interfaz del programa hacia el usuario que la utiliza es la manera en que el programa coge los datos del usuario y le devuelve los resultados. Por tanto, la interfaz de usuario es la parte del programa que permite a éste interactuar con el usuario. Las interfaces de usuario pueden adoptar muchas formas, que van desde la simple línea de comandos hasta las interfaces gráficas que proporcionan las aplicaciones más modernas.

1. Modelo de interfaz gráfica de usuario en Java.

Las aplicaciones basadas en ventanas son el tipo de modelo de interfaz gráfica que más a menudo utilizamos cuando trabajamos con el ordenador. Son aplicaciones que se ejecutan de forma local, que utilizan como interfaz del usuario las tecnologías de las ventanas típicas de los sistemas operativos Mac OS, Windows y XWindows (que es el servidor gráfico para Linux y Unix). Las ventanas son el componente principal de este modelo y normalmente se componen de:

- Barra de título. - Barra de estado. - Borde. - Controles de edición de datos.

Las JFC (Java Foundation Classes) agrupan, entre otras, las siguientes interfaces gráficas de programación: • AWT (paquete java.awt): incluye clases para derivar ventanas y controles gráficos, así como objetos para gestionar eventos generados por teclado y ratón. • Swing (paquete javax.swing): extiende las funcionalidades de las clases AWT y proporciona nuevas clases de controles.

Para crear una aplicación java basada en una ventana principal deberemos seguir los siguientes pasos. 1) Como siempre, se crea una clase inicial con el método main(). Sin embargo, ahora podemos hacer que esta clase extienda la clase java.awt.Frame o javax.swing.JFrame, que representan ventanas principales. Las clases Frame y JFrame tienen varios métodos y constructores que veremos posteriormente. 2) Se añaden componentes o controles visuales al contenedor, como barras de menús, cuadros de edición, botones, etc. 3) Se crean objetos de la clase java.awt.AWTEvent para gestionar cada evento y registrarlos con el componente que genere el evento.

Por ejemplo, el siguiente código crea una ventana vacía con un título, posición y tamaño determinado: import java.awt.*;

public class Ventana extends Frame {

public Ventana() {

super("Ventana de test"); // se asigna el título pasándolo al contructor de la clase Frame

setLocation(100,100); // se asigna la posición en panalla

setSize(200,100); // se asigna el tamaño inicial

setVisible(true); // se muestra la ventana

}

public static void main(String[] arg) {

new Ventana(); // se instancia la ventana y se muestra

}

}

1.1. Diferencias entre AWT y Swing.

La diferencia fundamental entre AWT y Swing es que los componentes AWT tienen asociado su propio recurso de ventana y esto origina dependencia de la plataforma. Las aplicaciones con muchos componentes, consumirán muchos recursos. Los componentes Swing, a diferencia de los AWT, están escritos en Java, lo que determina independencia respecto de la plataforma; además, al no tener su propia ventana, consumen mucho menos recursos.

Page 154: Fundamentos de JAVA

Fundamentos de Java /154

Swing extiende el AWT añadiendo un conjunto de componentes, JComponents, y sus clases de soporte. Hay un conjunto de componentes de Swing que son análogos a los de AWT, y algunos de ellos participan de la arquitectura MVC (Modelo-Vista-Controlador), aunque Swing también proporciona otros widgets nuevos como árboles, pestañas, etc. En la práctica, las aplicaciones Java con interfaces gráficas suelen combinar AWT y Swing. AWT se encarga de toda la gestión de eventos y Swing ofrece una serie de componentes más sofisticados.

1.2. Modelo de delegación de eventos.

1.2.1. Jerarquía de eventos. Cada señal que se produce en un ordenador (por ejemplo, pulsación de teclas o acciones con el ratón) es detectada por el sistema operativo, quien genera un evento de bajo nivel que es enviado a la aplicación que está activa. La aplicación activa es normalmente la que se está ejecutando mediante una ventana en primer plano del escritorio. La aplicación activa traslada el evento de bajo nivel a una de sus ventanas y ésa a su vez lo reenvía recursivamente a los controles afectados por el evento. Los controles afectados dependen del tipo de evento:

- Si es un evento de teclado lo recibe el control (y sus contenedores) que tiene el foco en ese momento. - Si es un evento de ratón lo recibe el control (y sus contenedores) situado bajo el puntero del ratón.

Cada control, según los eventos de bajo nivel que recibe, puede generar a su vez unos determinados eventos de alto nivel o semánticos. Todos estos eventos son enviados hacia el programa, el cual puede decidir gestionarlo ejecutando un código específico. Para gestionar todos los eventos generados desde los controles gráficos, Java proporciona una jerarquía de clases de eventos ubicada en el paquete java.util.

Cada clase de esta jerarquía representa una categoría de eventos: • AWTEvent, es la clase base de la cual derivan todos los eventos relacionados con controles gráficos. • ActionEvent, representa el evento por defecto que puede generar cada control. En el caso de los botones se genera cuando se pulsa el botón por cualquier medio. • AdjustmentEvent, es un evento generado por una barra de desplazamiento cuando cambia su valor de desplazamiento. • ComponentEvent, son eventos generados cuando un control se muestra u oculta, se redimensiona o cambia de posición. • ItemEvent, es generado por controles que contienen una lista de elementos y ha cambiado el elemento seleccionado. • TextEvent, es generado por controles que editan un texto y éste ha cambiado. • ContainerEvent, en generado por controles contenedores cuando se añaden o elimina controles hijos. • FocusEvent, son eventos que se producen cuando un control obtiene o pierde el foco. • InputEvent, es la clase base para los eventos de ratón y teclado. • WindowEvent, son eventos generados por ventanas para indicar cuándo se abren, cierran, minimizan, etc. • KeyEvent, es cualquier evento generado desde el teclado. • MouseEvent, es cualquier evento generado desde el ratón.

El modelo de gestión eventos de Java hace uso de orígenes de eventos (Source, normalmente los controles gráficos) y receptores de eventos (Listener, objetos que deberán registrarse para gestionar un determinado

EventObject

AWTEvent

ComponentEvent ActionEvent AdjustmentEvent ItemEvent TextEvent

InputEvent ContainerEvent FocusEvent WindowEvent

KeyEvent MouseEvent

Page 155: Fundamentos de JAVA

Fundamentos de Java /155

evento). Un origen de eventos es un objeto que tiene la capacidad de detectar eventos de bajo nivel y notificar a los receptores de eventos de que se ha producido un evento. Aunque el programador puede establecer el entorno en que se producen esas notificaciones, siempre hay un escenario por defecto para cada tipo de control gráfico. 1.2.2. Gestión de los eventos. Un objeto receptor de eventos es una clase (o una subclase de una clase) que implementa una interfaz receptora específica. Hay definidos un determinado número de interfaces receptoras, donde cada interfaz declara los métodos adecuados al tratamiento de los eventos de su clase. Por tanto, hay un emparejamiento natural entre clases de eventos y definiciones de interfaces. Por ejemplo, hay una clase de eventos de ratón que incluye muchos de los eventos asociados con las acciones del ratón, y hay una interfaz que se utiliza para definir los receptores de esos eventos. La siguiente tabla muestra los eventos, interfaces receptoras asociadas, los métodos de éstas, y la acción específica sobre el control que provoca la llamada al método:

Tipo de evento Interfaz receptora Métodos de la interfaz Acciones sobre el control origen ActionEvent ActionListener actionPerformed() Si es un botón porque se ha pulsado, si

es otro control se tratará del evento por defecto.

ComponentEvent ComponentListener componetHidden() Se oculta el control componentMove() Se mueve el control componentResized() Cambia el tamaño del control componentShow() Se muestra el control

ContainerEvent ContainerListener componentAdded() Se añade un componente al control componentRemoved() Se elimina un componente del control

KeyEvent KeyListener keyPressed() Se mantiene pulsada una tecla keyReleased() Se libera un tecla de su pulsación keyTyped() Se pulsa un tecla que genera un caracter

MouseEvent MouseListener mouseClicked() Se hace un clic con un botón del ratón mouseEntered() El puntero de ratón entra en el control mouseExited() El puntero de ratón sale del control mousePressed() Se mantiene pulsado un botón del ratón mouseReleased() Se deja de pulsar un botón del ratón

MouseMotionListener mouseDragged() Se suelta un objeto que era arrastrado por el ratón sobre el control

mouseMoved() Se arrastra un objeto con el ratón sobre el control

WindowEvent WindowListener windowActivated() El control es una ventana y se activa windowDeactivated() El control-ventana se desactiva windowClosed() Acaba de cerrarse el control-ventana windowClosing() Se va a cerrar el control-ventana windowIconifed() Se minimiza el control-ventana windowDeiconified() Se restaura el control-ventana windowOpened() Se abre el control-ventana

AdjustementEvent AdjustementListener adjustementValueChanged() Cambia el valor del control TextEvent TextListener textValueChanged() Cambia el valor de texto del control ItemEvent ItemListener itemStateChanged() Cambia el estado de un elemento del

control FocusEvent FocusListener focusGained() El control recibe el foco

focusLost() El control pierde al foco

Cada uno de los métodos anteriores de cada interfaz recibe un argumento del tipo del evento correspondiente que proporciona información adicional sobre el evento. Un objeto receptor puede estar registrado con un objeto origen para ser notificado de la ocurrencia de todos los eventos de la clase para los que el objeto receptor está diseñado. Una vez que el objeto receptor está registrado para ser notificado de esos eventos, el suceso de un evento en esta clase automáticamente invocará

Page 156: Fundamentos de JAVA

Fundamentos de Java /156

al método sobrescrito del objeto receptor. El código en el método sobrescrito debe estar diseñado por el programador para realizar las acciones específicas que desee cuando suceda el evento. Algunas clases de eventos, como los de ratón, involucran a un determinado conjunto de eventos diferentes. Una clase receptora que implemente la interfaz que recoja estos eventos debe sobrescribir todos los métodos declarados en el interfaz. Para prevenir esto, de forma que no sea tan tedioso y no haya que sobrescribir métodos que no se van a utilizar, se han definido un conjunto de clases intermedias, conocidas como Adaptadoras (Adapter): ComponentAdapter, FocusAdapter, KeyAdapter, MouseAdapter, MouseMotionAdapter, WindowAdapter. Estas clases Adaptadoras implementan las interfaces receptoras y sobrescriben todos los métodos de la interfaz con bloques vacíos. Una clase receptor puede estar definida como clase que extiende una clase Adaptadora en lugar de una clase que implemente el interfaz. Cuando se hace esto, la clase receptora solamente necesita sobrescribir aquellos métodos que sean de interés para la aplicación, porque todos los otros métodos serán resueltos por la clase Adaptadora. 1.2.3. Notificación de eventos. Todos los orígenes de eventos de AWT soportan el multi-envío a receptores. Esto significa que se pueden añadir o quitar múltiples receptores de un solo origen; en otras palabras, la notificación de que se ha producido un mismo evento se puede enviar a uno o más objetos receptores simultáneamente. Todos los orígenes de eventos disponen de métodos del tipo add...Listener() que permiten registrar a un receptor de un determinado tipo de evento y de métodos remove...Listener() que permiten desligar a un receptor de la notificación del evento correspondiente. Se puede hacer una distinción entre los eventos de bajo nivel y los eventos de tipo semántico. Las fuentes de eventos de bajo nivel serán las clases de elementos o componentes visuales del interfaz gráfico (botones, barras de desplazamiento, cajas de selección, etc.), porque cada componente de la pantalla generará sus eventos específicos. El JDK 1.2 permite registrar receptores sobre fuentes de eventos de los siguientes tipos:

Componente origen Método para registrar a un receptor java.awt.Component addComponentListener()

addFocusListener()

addKeyListener()

addMouseListener()

addMouseMotionListener()

java.awt.Container addContainerListener()

java.awt.Dialog addWindowListener()

java.awt.Frame addWindowListener()

Los receptores de eventos que se pueden registrar de tipo semántico sobre objetos fuentes, generadores de eventos, en el JDK 1.2 son:

Componente origen Método para registrar a un receptor java.awt.Button addActionListener

java.awt.Choice addItemListener

java.awt.Checkbox addItemListener

java.awt.CheckboxMenuItem addItemListener

java.awt.List addActionListener

addItemListener

java.awt.MenuItem addActionListener

java.awt.Scrollbar addAdjustmentListener

java.awt.TextArea addTextListener

java.awt.TextField addActionListener

addTextListener

El proceso para gestionar un evento generado por un control es el siguiente: 1) Creamos una subclase de la clase adaptadora correspondiente al evento. 2) Rescribimos el método que gestionará dicho evento e introducimos el código que queramos que se ejecute. 3) Creamos un objeto de la nueva subclase y lo registramos pasándolo como argumento del método add....Listener() correspondiente del control que genera el evento.

Por ejemplo, si tenemos una ventana con un botón que al ser pulsado debe cerrar la aplicación:

Page 157: Fundamentos de JAVA

Fundamentos de Java /157

// Creamos la clase que gestionará la pulsación del botón

class CerrarAplicacion extends ActionAdapter {

public void actionPerformed(ActionEvent e) {

System.exit(0); // Cierra la aplicación

};

};

. . .

// Registramos un objeto para gestionar la pulsación del botón

miBoton.addActionListener(new CerrarAplicacion());

1.2.4. Eventos de teclado y ratón. De la clase InputEvent descienden los eventos de ratón y teclado. Esta clase dispone de métodos y constantes para saber qué teclas se han pulsado y qué ha ocurrido con el ratón.

Métodos de la clase InputEvent: • boolean isShiftDown() • boolean isAltDown() • boolean isControlDown()

Indican si al producirse el evento estaba pulsada respectivamente la tecla [Shift], [Alt] o [Control]. • int getModifiers()

Retorna un valor máscara con información sobre las teclas y botones pulsados. Haciendo un “or” (operador ||) entre esta máscara y la constante SHIFT_MASK, ALT_MASK, CTRL_MASK, BUTTON1_MASK, BUTTON2_MASK o BUTTON3_MASK obtendremos un valor distinto de cero si la tecla o botón correspondiente estaba pulsada cuando se produjo el evento.

• long getWhen() Devuelve la hora en que se produjo el evento.

Métodos propios de la clase MouseEvent: • int getClickCount()

En los eventos de pulsación de ratón, retorna el número de clics en ese evento. • Point getPoint() • int getX() • int getY()

Retornan la posición del puntero de ratón.

1.3. Temporizador de eventos.

La clase javax.swing.Timer representa un componente temporizador, no visual, capaz de lanzar un evento regularmente cada cierto tiempo. El constructor de esta clase requiere dos argumentos:

• Un entero que indica el retardo, en milisegundos, entre cada evento. • Un objeto receptor que implemente la interfaz ActionListener.

Como se ha visto, la interfaz ActionListener implementa un único método llamado actionPerformed(). En la reescritura de este método podemos incluir un código que queramos que se ejecute cuando se lanza cada uno de los eventos. Por tanto, el componente Timer es una buena alternativa para ejecutar un proceso periódicamente sin usar hilos de ejecución. Para que los eventos del temporizador se empiecen a generar hay que invocar el método start(), y para detener la generación de eventos hay que invocar el método stop(). Como ejemplo ilustrativo, el siguiente código crea una clase Temporizador que implementa la interfaz ActionListener, y que declara un objeto Timer. En el constructor de esta clase se lanza el Timer para que cada segundo incremente el valor de una variable. La función personalizada getValor() permite recuperar el valor actual de esta variable. import javax.swing.Timer; // bibliotecas importantes

import java.awt.event.*; //bibliotecas importantes

public class Temporizador implements ActionListener {

private long contador = 0 ; // la variable que se incrementará

private Timer t = new Timer(1000,this); // se crea un Timer, con una actualizacion de 1 segundo

public Temporizador () { // Constructor para iniciar el Timer

t.start();

}

public long getValor() { // Función para retornar el valor actual del temporizador

Page 158: Fundamentos de JAVA

Fundamentos de Java /158

return contador;

}

public void actionPerformed(ActionEvent e) {

if (contador == Long.MAX_VALUE)

contador = 0;

else

contador++;

}

public static void main(String args[]) {

Temporizador td = new Temporizador (); // Instanciamos un temporizador y lo lanzamos

}

}

2. Librería gráfica AWT

AWT es el acrónimo del X Window Toolkit para Java. Se trata de una biblioteca de clases Java para el desarrollo de Interfaces de Usuario Gráficas. La versión del AWT que Sun proporciona con el JDK se desarrolló en sólo dos meses y es la parte más débil de todo lo que representa Java como lenguaje. El entorno que ofrece es demasiado simple, no se han tenido en cuenta las ideas de entornos gráficos novedosos. Javasoft se unió con Netscape, IBM y Lighthouse Design para crear un conjunto de clases que proporcionen una sensación visual agradable y fuesen más fáciles de utilizar por el programador. Esta colección de clases son las Java Foundation Classes (JFC), que están constituidas por cinco grupos de clases: AWT, Java 2D, Accesibilidad, Arrastrar y soltar y Swing. La estructura básica del AWT se basa en Componentes (Component) y Contenedores (Container). Estos últimos contienen componentes posicionados en su interior y son componentes a su vez, de forma que los eventos pueden tratarse tanto en Contenedores como en Componentes, corriendo por cuenta del programador el encaje de todas las piezas, así como la seguridad de tratamiento de los eventos adecuados. Con Swing se va un paso más allá, ya que todos los JComponents son subclases de Container, lo que hace posible que Swing puedan contener otros componentes que hacen más interesante.

2.1. Estructura del AWT.

La estructura de la versión actual del AWT se puede resumir en los puntos que se exponen a continuación: • Los Contenedores contienen Componentes, que son los controles básicos. • Los Componentes no se anclan en posiciones, sino que su distribución y situación está controlada por gestores de diseño (layouts manager). • El común denominador de más bajo nivel se acerca al teclado, ratón y manejo de eventos. • Alto nivel de abstracción respecto al entorno de ventanas en que se ejecute la aplicación (no hay áreas cliente, ni llamadas a X, ni hWnds, etc.). • La arquitectura de la aplicación es dependiente del entorno de ventanas, en vez de tener un tamaño fijo. • Es bastante dependiente de la máquina en que se ejecuta la aplicación (no puede asumir que un cuadro de diálogo tendrá el mismo tamaño y aspecto en cada máquina). • Carece de un formato de recursos. No se puede separar el código de lo que es propiamente interfaz.

2.1.1. Puntos, rectángulos y dimensiones. El paquete java.awt contiene clases base para definir las dimensiones de los componentes. Estas clases son:

• La clase java.awt.Point define un punto mediante sus dos variables públicas: x e y. • La clase java.awt.Dimension define una anchura y una altura mediante sus variables públicas: heigth y width. • La clase java.awt.Rectangle define un rectángulo mediante sus variables públicas (esquina superior izquierda, ancho y alto): x, y, width y height. • La clase java.awt.Insets representa el espacio que se deja libre en los bordes de un contenedor: top, left, bottom, right.

2.1.2. Componentes y contenedores. Un interfaz gráfico está construido en base a elementos gráficos básicos, los Componentes. Típicos ejemplos de estos Componentes son los botones, barras de desplazamiento, etiquetas, listas, cajas de selección o campos de texto. Los Componentes permiten al usuario interactuar con la aplicación y proporcionar información desde el programa al usuario sobre el estado del programa. En AWT, todos los Componentes de la interfaz de usuario son instancias de la clase Component o uno de sus subtipos.

Page 159: Fundamentos de JAVA

Fundamentos de Java /159

Los Componentes no se encuentran aislados, sino agrupados dentro de Contenedores. Los Contenedores contienen y organizan la situación de los Componentes; además, los Contenedores son en sí mismos Componentes y como tales pueden ser situados dentro de otros Contenedores. También contienen el código necesario para el control de eventos, cambiar la forma del cursor o modificar el icono de la aplicación. En AWT, todos los Contenedores son instancias de la clase Container o uno de sus subtipos.

2.2. Componentes.

La clase java.awt.Component es una clase abstracta que representa todo lo que tiene una posición, un tamaño, puede ser pintado en pantalla y puede recibir eventos. No tiene constructores públicos, ni puede ser instanciada. Sin embargo, desde el JDK 1.1 puede ser extendida para proporcionar una nueva característica incorporada a Java, conocida como componentes Lightweight. El siguiente diagrama muestra la jerarquía de clases de los Componentes AWT:

Sobre estos Componentes se podrían hacer más agrupaciones y quizá la más significativa fuese la que diferencie a los Componentes según el tipo de entrada. Así habrá Componentes con entrada de tipo no-textual como los botones de pulsación (Button), las listas (List), botones de marcación (Checkbox), botones de selección (Choice) y botones de comprobación (CheckboxGroup); Componentes de entrada y salida textual como los campos de texto (TextField), las áreas de texto (TextArea) y las etiquetas (Label); y, otros Componentes sin acomodo fijo en ningún lado, en donde se encontrarían Componentes como las barras de desplazamiento (Scrollbar), zonas de dibujo (Canvas) e incluso los Contenedores (Panel, Window, Dialog y Frame), que también pueden considerarse como Componentes. 2.2.1. Métodos y eventos de la clase «Component». Métodos heredados por todos los componentes:

• boolean isVisible () • void setVisible (boolean b)

Determinan la visibilidad del componente. • boolean isShowing ()

Indica si el componente se está viendo. • boolean isEnabled () • void setEnabled (boolean b)

Determinan la activación del componente. • Point getLocation ()

Component

TextComponent Checkbox Button

Canvas Choice

Container

Panel Window ScrollPane TextArea TextField

Dialog Frame

Scrollbar

List

Label

FileDialog

Page 160: Fundamentos de JAVA

Fundamentos de Java /160

• Point getLocationOnScreen () Retornan la posición de la esquina superior izquierda del componente respecto a su padre o a la pantalla.

• Dimension getSize () • void setSize (int ancho, int alto) • void setSize (Dimension d)

Determinan el tamaño del componente. • Rectangle getBounds () • void setBounds (Rectangle r) • void setBounds (int x, int y, int ancho, int alto)

Determinan la posición y tamaño. • void invalidate ()

Fuerza a volver a reordenar los componentes hijos volviendo a aplicar el layout manager asociado. • void validate ()

Se asegura de que el layout manager está bien aplicado. • void doLayout ()

Hace que se aplique el layout manager. • void requestFocus ()

Hace que el componente adquiera el foco. • void paint (Graphics g) • void repaint() • void update (Graphics g)

Métodos para dibujar en pantalla. update() rellena el componente con el color de fondo y llama a paint(). repaint() llama a update().

• void setBackground (Color c) • void setForeground (Color c)

Establecen el color de fondo y de dibujo. Eventos que pueden generar todos los componentes:

• ComponentEvent: Se genera cuando el componente se muestra, se oculta, o cambia de posición o de tamaño. • FocusEvent: Se genera cuando el componente gana o pierde el foco. • KeyEvent: Se genera cada vez que se pulsa una tecla o una combinación de teclas en el teclado. • MouseEvent: Se genera cada vez que el cursor del ratón entra o sale del componente, al clicar, o cuando se pulsa o se suelta un botón del ratón sobre el componente.

2.2.2. Botones de pulsación (clase «Button»). La clase java.awt.Button implementa un componente de tipo botón de pulsación con un título. El constructor más utilizado es el que permite pasarle como parámetro un texto, que será el que aparezca como título e identificador del botón en la interfaz de usuario. No dispone de campos o variables de instancia y pone al alcance del programador una serie de métodos entre los que destacan por su utilidad los siguientes:

• addActionListener() Añade un receptor de eventos de tipo ActionEvent producidos por el botón.

• removeActionListener() Elimina el receptor de eventos para que el botón deje de realizar acción alguna.

• getLabel() Devuelve la etiqueta o título del botón.

• setLabel() Fija el título o etiqueta visual del botón.

Eventos propios que puede generar: • ActionEvent: Se genera cuando se pulsa el botón.

Un evento de tipo ActionEvent se produce cuando el usuario pulsa sobre un objeto Button. Además, también puede generar eventos del tipo FocusListener, MouseListener o KeyListener, porque hereda los métodos de la clase Component que permiten instanciar y registrar receptores de eventos de este tipo sobre objetos de tipo Button. El siguiente código crea un botón con el texto "alguna acción", y asigna un receptor del evento de pulsación: Button btnAccion = new Button("alguna acción");

btnAccion.addActionListener( new ActionListener() {

Page 161: Fundamentos de JAVA

Fundamentos de Java /161

public void actionPerformed (ActionEvent ev) {

// cualquier código que deba ejecutarse al pulsar el botón

}

});

2.2.3. Lienzos (clase «Canvas»). La clase java.awt.Canvas representa una zona de dibujo, o lienzo. Define una zona rectangular vacía de la pantalla, sobre la cual una aplicación puede pintar, imitando el lienzo sobre el que un artista plasma su arte, o desde la cual una aplicación puede recuperar eventos producidos por acciones del usuario. La clase Canvas existe para que se obtengan subclases a partir de ella. No hace nada por sí misma, solamente proporciona una forma de implementar componentes propios. Por ejemplo, un lienzo es útil a la hora de presentar imágenes o gráficos en pantalla, independientemente de que se quiera saber si se producen eventos o no en la zona de presentación. Cuando se implementa una subclase de la clase Canvas, hay que prestar atención en implementar los métodos minimumSize() y preferredSize() para reflejar adecuadamente el tamaño del lienzo; porque, en caso contrario, dependiendo del layout que utilice el contenedor del lienzo, éste puede llegar a ser demasiado pequeño, incluso invisible. La clase Canvas es muy simple, consiste en un solo constructor sin argumentos y dos métodos, que son:

• AddNotify() Crea el observador del canvas.

• void paint (Graphics g) Rellena el control con el color de fondo. Las clases derivadas deben rescribir este método para dibujar o mostrar imágenes en el lienzo.

La forma habitual de usar un lienzo es extendiendo esta clase de la siguiente forma: class MiLienzo extends Canvas {

. . .

public void paint(Graphis g) {

// aquí se pone cualquier código de dibujo sobre el lienzo.

}

}

2.2.4. Botones de comprobación (clase «Chekbox»). La clase java.awt.CheckBox extiende la clase Component e implementa la interfaz ItemSelectable, que es la interfaz que contiene un conjunto de ítems entre los que puede haber o no alguno seleccionado. Representa una casilla de selección o verificación que puede estar marcada o desmarcada. La casilla puede tener asociada una etiqueta de texto. Cuando se crean aisladamente tiene la forma de cuadrados, pero cuando se agrupan toman la forma de círculos.

Los botones de comprobación (Checkbox) se pueden agrupar para formar un conjunto de botones de radio (véase la clase CheckboxGroup), que son agrupaciones de botones de comprobación de exclusión múltiple, es decir, en las que siempre hay un único botón activo. La programación de objetos Checkbox puede ser simple o complicada, dependiendo de lo que se intente conseguir. La forma más simple para procesar objetos Checkbox es colocarlos en un CheckboxGroup, ignorar todos los eventos que se generen cuando el usuario selecciona botones individualmente y luego, procesar sólo el evento de tipo Action cuando el usuario fije su selección y pulse un botón de confirmación. La otra forma, más compleja, para procesar información de objetos Checkbox es responder a los diferentes tipos de eventos que se generan cuando el usuario selecciona objetos Checkbox distintos. Métodos propios:

• void setLabel (String etiqueta) • String getLabel ()

Determinan la etiqueta asociada a la casilla.

Page 162: Fundamentos de JAVA

Fundamentos de Java /162

• void setState (boolean estado) • boolean getState ()

Determinan el estado de la casilla: true si está marcada y false si no lo está. • void setCheckboxGroup (CheckboxGroup padre) • CheckboxGroup getCheckboxGroup ()

Determinan el grupo al que pertenece la casilla. Eventos propios que puede generar:

• ItemEvent: Se genera cuando la casilla cambia de estado (SELECTED o DESELECTED). El siguiente código crea un botón de comprobación inicialmente marcado, y posteriormente se comprueba su estado: Checkbox casilla = new Checkbox ("casado/a", true);

. . .

if (casilla.getState() ) {

// hacer algo si está marcada la casilla

}

2.2.5. Grupo de botones (clase «CheckboxGroup»). La clase java.awt.CheckboxGroup representa un grupo de botones de radio o casillas, de forma que sólo una pueda estar marcada a la vez. Cada casilla debe llamar a su método setCheckboxGroup() para ser añadida al grupo. Métodos propios:

• void setSelectedCheckbox (Checkbox casilla)

Asigna la casilla del grupo que debe estar marcada. • Checkbox getSelectedCheckbox ()

Retorna la casilla del grupo marcada. El siguiente código muestra como agrupar dos botones de comprobación: Checkbox chb1 = new Checkbox("Opción 1");

Checkbox chb2 = new Checkbox("Opción 2");

CheckboxGroup grupo = new CheckboxGroup();

chb1.setCheckboxGroup(grupo);

chb2.setCheckboxGroup(grupo);

2.2.6. Etiquetas (clase «Label»). La clase java.awt.Label representa una etiqueta de texto que el usuario no puede editar directamente. Por defecto el texto se alinea a la izquierda. Métodos propios:

• Label (String texto) • Label (String texto, int alineacion)

Constructores. • void setAlignement (int alineacion) • int getAlignement ()

Determinan la alineación del texto (CENTER, LEFT o RIGHT). • void setText (String texto) • int getText ()

Determinan el texto de la etiqueta. El siguiente código crea una etiqueta con un texto inicial, y después recupera el contenido como un string: Label lbMensaje = new Label("<sin mensaje>");

. . .

String msgActual = lbMensaje.getLabel( );

2.2.7. Listas de opciones (clase «List»). La clase java.awt.List representa una lista de strings, cada uno de ellos representado en un línea (el primer elemento tiene índice cero). Extiende la clase Component e implementa el interfaz ItemSelectable, que es el interfaz que contiene un conjunto de ítems en los que puede haber, o no, alguno seleccionado. Además soporta el método addActionListener() que se utiliza para recoger los eventos ActionEvent que se produce cuando el usuario hace un doble clic sobre un elemento de la lista.

Page 163: Fundamentos de JAVA

Fundamentos de Java /163

La lista sólo puede mostrar en pantalla un número determinado de líneas, dependiendo de la altura de la propia lista. Por defecto no se muestran barras de desplazamiento laterales, para ello es necesario incluir la lista dentro de un control ScrollPane. Se puede hacer una selección simple (por defecto) o múltiple. Se añaden elementos a la lista con el método add(), y se eliminan con el método remove(). Métodos propios:

• List (int numeroLineas) • List (int numeroLineas, boolean multiseleccion)

Constructores. • void add (String elemento) • void add (String elemento, int indice)

Añaden un elemento a la lista al final o en el índice dado. • void replaceItem (String elemento, int indice)

Reemplaza el texto del elemento de índice dado. • void removeAll () • void remove (int indice) • void remove (String elemento)

Eliminan elementos de la lista. • int getItemCount ()

Retorna el número de elementos de la lista. • int getRows ()

Retorna el número de líneas o elementos visibles. • String getItem (int indice) • String [] getItems ()

Retornan uno y todos los elementos de la lista. • int getSelectedIndex () • String getSelectedItem () • int [] getSelectedIndexes () • String [] getSelectedItems ()

Retornan el/los elementos seleccionados. • void selec (int indice) • void deselect(String elemento)

Selecciona o elimina la selección de un elemento. • boolean isIndexSelected (int indice) • boolean isItemSelected (String elemento)

Indican si un elemento está seleccionado. • boolean isMultipleMode () • void setMultipleMode (boolean multiple)

Determinan el modo de selección múltiple. • int getVisibleIndex ()

Retorna el índice del último elemento visible. • void makeVisible (int índice)

Hace visible el elemento de índice dado, provocando si es necesario un desplazamiento de la lista. Eventos propios que puede generar:

• ActionEvent: Se genera a hacer doble clic sobre un elemento. • ItemEvent: Se genera cuando se selecciona o deselecciona un elemento de la lista.

El código siguiente crea una lista que muestra varios tipos de comida, y posteriormente se recupera el elemento seleccionado: List lstComida = new List();

lstComida.add("Carne");

lstComida.add("Fruta");

lstComida.add("Legumbres");

. . .

if (lstComida.getSelectedIndex() >= 0) { // si no hay selección el índice es negativo

String comida = lstComida.getSelectedItem();

}

Page 164: Fundamentos de JAVA

Fundamentos de Java /164

2.2.8. Lista de selección (clase «Choice»). La clase java.awt.Choice representa una lista de selección o lista combinada. La lista inicialmente se encuentra replegada mostrando sólo el elemento seleccionado. La clase Choice extiende la clase Component e implementa el interfaz ItemSelectable, que es la interfaz que mantiene un conjunto de ítems en los que puede haber, o no, alguno seleccionado. Además, esta clase proporciona el método addItemListener(), que añade un registro de eventos ítem, que es muy importante a la hora de tratar los eventos que se producen sobre los objetos de tipo Choice. Métodos propios:

• void add (String opcion) • void addItem (String opcion) • void insert (String opcion, int indice)

Añaden un elemento a la lista de opciones. • int getSelectedIndex () • String getSelectedItem () • void select (int indice) • void select (String opcion)

Determinan la opción seleccionada de la lista. • int getItemCount ()

Retorna el número de elementos de la lista. • String getItem (int indice)

Retorna la opción del índice dado. • void removeAll () • void remove (int índice) • void remove (String opción)

Eliminan opciones de la lista. Eventos propios que puede generar:

• ItemEvent: Se genera cuando se selecciona un elemento de la lista. Para procesar la selección de un ítem de la lista se instancia y registra un objeto de tipo ItemListener sobre el objeto Choice para identificar y presentar el objeto String que se elige cuando el usuario utiliza una selección. Cuando esto ocurre se captura un evento en el método sobrescrito itemStateChanged() del objeto ItemListener. El código de este método puede usar una llamada a getSelectedItem() para el ítem que está marcado y presentar la cadena que le corresponde. El siguiente ejemplo crea una lista combinada y registra un receptor que gestiona el evento de cambio de elemento seleccionado. El evento ItemListener se lanza aunque se vuelva a seleccionar el elemento actual. Choice chComida = new Choice();

chComida.add("Carne");

chComida.add("Fruta");

chComida.add("Legumbres");

chComida.addItemListener( new ItemListener() {

public void itemStateChanged (ItemEvent ev) {

int estado = evt.getStateChange(); // 1 para indicar que se ha seleccionado el elemento

String elemento = evt.getItem().toString(); // el texto del elemento seleccionado

}

}

2.2.9. Barras de desplazamiento (clase «Scrollbar»). La clase java.util.Scrollbar representa una barra de desplazamiento, las cuales se utilizan para permitir realizar ajustes de valores lineales en pantalla. Proporcionan una forma de trabajar con rangos de valores o de áreas, como en el caso de un área de texto en donde se proporcionan las barras de desplazamiento de forma automática. Pueden tener orientación HORIZONTAL o VERTICAL. Podemos desplazar el cursor de la barra mediante código modificando su valor con el método setValue(). Se debe tener en cuenta que la asignación del desplazamiento de la barra se realiza de forma asíncrona. Métodos propios:

• Scrollbar (int orientacion) • Scrollbar (int orientacion, int valor, int vis, int min, int max)

Page 165: Fundamentos de JAVA

Fundamentos de Java /165

Constructores. Donde vis indica el tamaño del área visible en caso de que la barra se utilice en un área de texto.

• int getValue () • void setValue (int valor)

Determinan el valor actual de desplazamiento. • void setMinimum (int valor) • void setMaximum (int valor)

Determinan el valor mínimo y máximo. • void setUnitIncrement (int valor) • void setBlockIncrement (int valor)

Determinan el incremento pequeño y grande. • void setOrientation (int orientacion)

Determina la orientación. • void setValues (int valor, int vis, int min, int max)

Asigna los valores de la barra. Eventos propios que puede generar:

• AdjustementEvent: Se genera cada vez que cambia el valor de la barra. 2.2.10. Campos de texto (clase «TextField»). Para la entrada directa de datos se suelen utilizar los campos de texto, que aparecen en pantalla como pequeñas cajas que permiten al usuario la entrada por teclado de una línea de caracteres. Los campos de texto (clase java.awt.TextField) son los encargados de realizar esta entrada, aunque también se pueden utilizar, activando su indicador de no-editable, para presentar texto e una sola línea con una apariencia en pantalla más llamativa, debido al borde simulando 3-D que acompaña a este tipo de elementos del interfaz gráfico. La clase TextField extiende a la clase TextComponent, que extiende a su vez, a la clase Component. Un TextField sólo permite editar en una línea, mientras que un TextArea permite varias líneas y ofrece posibilidades de edición adicionales. En un TextField se puede asignar un caracter de eco para escribir texto oculto. Se puede obtener el contenido del cuadro mediante el método getText(). Métodos heredados de TextComponent:

• String getText () • void setText (String texto)

Determinan el texto escrito en el editor. • void setEditable (boolean modo) • boolean isEditable ()

Determinan si el editor permite editar texto. • void setCaretPosition (int pos) • int getCaretPosition ()

Determinan la posición del cursor de edición. • String getSelectedText () • int getSelectionStart () • int getSelectionEnd ()

Obtiene el texto seleccionado y sus posiciones. • void selectAll () • void select (int inicio, int fin)

Selecciona todo o parte del texto. Métodos de TextField:

• TextField () • TextField (int columnas) • TextField (String texto) • TextField (String texto, int columnas)

Constructores. • int getColumns () • void setColums (int n)

Determina el número de columnas del editor. • void setEchoChar (char eco)

Page 166: Fundamentos de JAVA

Fundamentos de Java /166

• char getEchoChar () • boolean echoCharSet ()

Determinan el caracter de eco para ocultar el texto. Eventos propios que pueden generar:

• TextEvent: Se genera cada vez que se modifica el contenido del editor. • ActionEvent: Se genera cuando se pulsa [Enter] al final del texto.

El código siguiente crea un cuadro de texto y registra un receptor que recibirá el evento de cambio del contenido del cuadro de texto: TextField txtCampo = new TextField();

txtCampo.addTextListener(new TextListener() {

public void textValueChanged(java.awt.event.TextEvent evt) {

String contenido = txtCampo.getText(); // recupera el contenido del cuadro de texto

}

});

2.2.11. Áreas de texto (clase «TextArea»). Un área de texto (clase java.awt.TextArea) es una zona multilínea que permite la presentación de texto, que puede ser editable o de sólo lectura. Al igual que la clase TextField, esta clase extiende la clase TextComponent y dispone de cuatro constantes que pueden ser utilizadas para especificar la información de colocación de las barras de desplazamiento en algunos de los constructores de objetos TextArea. Estas constantes simbólicas son:

TextArea.SCROLLBARS_BOTH Para ambas barras de desplazamiento TextArea.SCROLLBARS_NONE Sin barras de desplazamiento TextArea.SCROLLBARS_HORIZONTAL_ONLY Sólo la barra horizontal TextArea.SCROLLBARS_VERTICAL_ONLY Sólo la barra vertical

Métodos de TextArea: • TextArea () • TextArea (int filas, int columnas) • TextArea (String texto) • TextArea (String texto, int filas, int columnas)

Constructores. • int getRows () • void setRows (int n) • int getColumns () • void setColumn (int n)

Determina el número de filas y columnas del editor. • void append (String texto) • void insert (String texto, int posicion) • void replaeRange (String texto, int inicio, int final)

Insertan y reemplaza texto. Eventos propios que pueden generar:

• TextEvent: Se genera cada vez que se modifica el contenido del editor. • ActionEvent: Se genera cuando se pulsa [Enter] al final del texto.

2.3. Contenedores.

La clase java.awt.Container es una clase abstracta derivada de Component, que representa a cualquier componente que pueda contener otros componentes (excepto otro contenedor). Se trata, en esencia, de añadir a la clase Component la funcionalidad de adición, sustracción, recuperación, control y organización de otros componentes. El AWT proporciona varias clases de Contenedores: Panel, Applet, ScrollPane, Window, Dialog, FileDialog, Frame. Aunque los que se pueden considerar como verdaderos contenedores son Window, Frame, Dialog y Panel, porque los demás son subtipos con algunas características determinadas y solamente útiles en circunstancias muy concretas. 2.3.1. Métodos y eventos de la clase «Container». Métodos propios:

• Component add (Component c)

Page 167: Fundamentos de JAVA

Fundamentos de Java /167

• Component add (Component c, int indice) Añaden un componente al contenedor.

• void setLayout (LayoutManager lym) • LayoutManager getLayout ()

Determinan el layout manager usado. • void validate () • void doLayout ()

Obligan a reorganizar los componentes. • void remove (int indice) • void remove (Component c) • void removeAll ()

Eliminan componentes del contenedor. Eventos propios que puede generar:

• ContainerEvent: Se genera cada vez que un componente se añade o retira del contenedor. 2.3.2. Añadir componentes a un contenedor. Para que un interfaz sea útil no debe estar compuesto solamente por contenedores, éstos deben tener componentes en su interior. Los componentes se añaden al contenedor invocando al método add() del contenedor. Este método tiene tres formas de llamada que dependen del gestor de composición o layout manager que se vaya a utilizar sobre el contenedor. En el código siguiente se incorporan dos botones a un contenedor de tipo Panel. La creación se realiza en el método init(), que es invocado en el método main(). import java.awt.*;

public class MiPanel extends Panel {

public void init() {

add( new Button( "Uno" ) ); // añade un botón al panel

add( new Button( "Dos" ) ); // añade otro botón al panel

}

public static void main( String args[] ) {

Frame f = new Frame( "Ejemplo de contenedores" ); // crea un ventana principal

MiPanel panel1 = new MiPanel ();

Panel1.init();

f.add(panel1, "Center"); // añade el panel a la ventana en su zona central

f.pack(); // valida el layout de la ventana

f.show(); // muestra la ventana

}

}

Los componentes añadidos a un objeto Container entran en una lista cuyo orden define el orden en que se van a presentar los componentes sobre el contenedor, de atrás hacia delante. Si no se especifica ningún índice de orden en el momento de incorporar un componente al contenedor ese componente se añadirá al final de la lista. Hay que tener esto muy en cuenta, sobre todo a la hora de construir interfaces de usuario complejas, en las que pueda haber componentes que solapen a otros componentes o a parte de ellos. 2.3.3. Ventanas (clase «Window»). Es una superficie de pantalla de alto nivel (una ventana). Una instancia de la clase java.awt.Window no puede estar enlazada o embebida en otro contenedor. El controlador de posicionamiento de componentes por defecto sobre un objeto Window es el BorderLayout. Una instancia de esta clase no tiene ni título ni borde, así que es un poco difícil de justificar su uso para la construcción directa de un interfaz gráfico, porque es mucho más sencillo utilizar objetos de tipo Frame o Dialog. Dispone de varios métodos para alterar el tamaño y título de la ventana, o los cursores y barras de menús. Métodos propios:

• void toFront () • void toBack ()

Desplaza la ventana hacia primer plano o al fondo. • void show ()

Muestra la ventana en primer plano. Eventos propios que puede generar:

Page 168: Fundamentos de JAVA

Fundamentos de Java /168

• WindowEvent: Se genera cada vez que se abre, cierra, iconiza, activa o desactiva la ventana. 2.3.4. Ventanas principales (clase «Frame»). Una frame es una superficie de pantalla de alto nivel (una ventana) con borde y título. Una instancia de la clase java.awt.Frame puede tener una barra de menú. Una instancia de esta clase es mucho más aparente y más semejante a lo que se entiende por ventana. La clase Frame extiende a la clase Window, y su controlador de posicionamiento de componentes por defecto es el BorderLayout. Los objetos de tipo Frame son capaces de generar varios tipos de eventos, de los cuales el más interesante es el evento de tipo WindowClosing, que se produce cuando el usuario pulsa sobre el botón de cerrar colocado en la esquina superior-derecha (normalmente) de la barra de título. Una vez instanciada una Frame se muestra llamando a su método setVisible(true). Para ocultarla se debe llamar al método setVisible(false). Métodos propios:

• Frame () • Frame (String titulo)

Constructores. • String getTitle () • void setTitle (String titulo)

Determinan el título de la ventana. • Image getIconImage () • void setIconImage (Image icono)

Determinan el icono de la barra de título. • void setResizable (boolean sw) • boolean isResizable ()

Determina y comprueba si se puede cambiar el tamaño. El siguiente código crea una nueva ventana principal que al cerrarse finaliza el programa. import java.awt.*;

public class MiVentana extends Frame {

public MiVentana () {

// centra la ventana en el escritorio

this.setLocationRelativeTo(null);

// se registra el evento de cierre de la ventana para finalizar el programa

this.addWindowListener(new java.awt.event.WindowAdapter() {

public void windowClosing(java.awt.event.WindowEvent evt) {

System.exit(0);

}

});

}

public static void main( String args[] ) {

MiVentana f = new MiVentana( ); // crea un ventana principal

f.setVisible(true); // muestra la ventana

}

}

2.3.5. Cuadros de diálogo (clase «Dialog»). Un cuadro de diálogo es una superficie de pantalla de alto nivel (una ventana) con borde y título, que permite entradas del usuario. La clase java.awt.Dialog extiende la clase Window y el controlador de posicionamiento por defecto es el BorderLayout. Permite derivar cuadros de diálogo que dependen de otras ventanas (normalmente de una Frame). El objeto Dialog puede ser movido y redimensionado, aunque no se puede ni minimizar ni maximizar. Se pueden colocar en cualquier lugar de la pantalla, su posición no está restringida al interior del padre, el objeto Frame.

Page 169: Fundamentos de JAVA

Fundamentos de Java /169

De los constructores proporcionados por esta clase destaca el que permite que el diálogo sea o no modal. Todos los constructores requieren un parámetro Frame y, algunos de ellos, permiten la especificación de un parámetro booleano que indica si la ventana que abre el diálogo será modal o no. Si es modal, todas las entradas del usuario de la aplicación serán recogidas por esta ventana, bloqueando cualquier entrada que se pudiese producir sobre otros objetos presentes de la misma aplicación. Posteriormente, si no se ha especificado que el diálogo sea modal, se puede hace que adquiera esta característica invocando al método setModal(). Una vez instanciado un Dialog, se muestra llamando a su método setVisible(true). Para ocultarlo se debe llamar al método setVisible(false). Métodos propios:

• Dialog (Frame padre) • Dialog (Frame padre, boolean modo) • Dialog (Frame padre, String titulo) • Dialog (Frame padre, String titulo, boolean modo)

Constructores. El valor del parámetro modo determina si es modal o no. • String getTitle () • void setTitle (String titulo)

Determinan el título de la ventana. • boolean isModal () • void setModal (boolean modo)

Determinan si el diálogo es modal o no. • void setResizable (boolean sw) • boolean isResizable ()

Determina y comprueba si se puede cambiar el tamaño. 2.3.6. Paneles (clase «Panel»). La clase java.awt.Panel es un contenedor genérico de componentes. Una instancia de la clase Panel simplemente proporciona un contenedor al que poder añadir componentes. El controlador de posicionamiento de componentes sobre un objeto Panel por defecto es el FlowLayout; aunque se puede especificar uno diferente en el constructor a la hora de instanciar el objeto Panel, o aceptar el controlador de posicionamiento inicialmente y después cambiarlo invocando al método setLayout(). Panel dispone de un método addNotify() que permite cambiar el aspecto del panel sin afectar a su funcionalidad. Normalmente un Panel no tiene manifestación visual alguna por sí mismo, aunque puede hacerse notar fijando su color de fondo por defecto a uno diferente del que utiliza normalmente. 2.3.7. Paneles desplazables (clase «ScrollPane»). La clase ScrollPane representa un panel con barras de desplazamiento laterales para poder visualizar un contenido mayor que el área visible del panel. Para determinar la visibilidad de las barras laterales declara las constantes SCROLLBARS_NEVER, SCROLLBARS_AS_NEEDED y SCROLLBARS_ALWAYS. Métodos propios:

• ScrollPane (int constantes) Constructor.

• Dimension getViewportSize () Retorna el tamaño del panel.

• int getHScrollbarHeight () • int getVScrollbarWidth ()

Retornan el tamaño de las barras de desplazamiento.

Page 170: Fundamentos de JAVA

Fundamentos de Java /170

• void setScrollPosition (int x, int y) • void setScrollPosition (Point p) • Point getScrollPosition ()

Determinan la posición del componente en cuanto al desplazamiento de su contenido. Se puede utilizar un panel desplazable para proporcionar barras de desplazamiento laterales a una lista. El siguiente código muestra cómo combinar estos dos componentes: ScrollPane scrollPane = new ScrollPane();

List list = new List();

scrollPane.add(list);

2.3.8. Cuadro de diálogo de archivos (clase «FileDialog»). La clase java.awt.FileDialog permite crear cuadros de diálogo para seleccionar un fichero. Opera en dos modos:

- SAVE: para buscar la ruta donde guardar un archivo. - LOAD: para buscar la ruta de un archivo o directorio.

Este cuadro de diálogo siempre se abre en modo modal. El proceso de uso es el siguiente: - Se instancia un FileDialog con el título y modo apropiado. - Se puede asignar un directorio de búsqueda inicial con setDirectoriy(). - Se muestra el cuadro de diálogo con setVisible(true). Al mostrarse en modo modal, no se retoma la ejecución del código hasta que el cuadro de diálogo se cierra. - Después que el cuadro se cierra, en las instrucciones siguientes, se puede obtener el archivo y carpeta seleccionados con getFile() y getDirectory(). Si retornan un valor null, es que el usuario canceló el cuadro de diálogo.

Métodos propios: • FileDialog (Frame padre) • FileDialog (Frame padre, String titulo) • FileDialog (Frame padre, String titulo, int modo)

Constructores. El modo puede ser SAVE o LOAD. • int getMode () • void setMode (int modo)

Determinan el modo de apertura (SAVE o LOAD). • String getDirectory () • String getFile ()

Obtienen el directorio y fichero elegido en el diálogo. • void setDirectory (String dir) • void setFile (String fichero)

Asignan el directorio y archivo al abrir el diálogo. • FilenameFilter getFilenameFilter () • voi setFilenameFilter (FilenameFilter filtro)

Determinan un filtro para buscar los ficheros. FilenameFilter es una interfaz con un único método: boolean

accept(File dir, String name). Dicho método debe indicar si un directorio y archivo determinado pertenecen o no al filtro.

El siguiente código muestra cómo abrir este cuadro de diálogo y cómo recuperar una ruta seleccionada: FileDialog fd = new FileDialog(this, "Ejemplo de uso",FileDialog.LOAD);

fd.setDirectory("."); // se selecciona por defecto la carpeta actual

fd.setVisible(true); // se abre el cuadro de diálogo en modo modal

if (fd.getFile()!=null) { // si no se canceló

String rutaCompleta = fd.getDirectory() + fd.getFile();

}

2.4. Menús.

No hay ningún método para diseñar una buena interfaz de usuario, todo depende del programador. Los menús son siempre el centro de la aplicación, porque son el medio de que el usuario interactúe con esa aplicación. La diferencia entre una aplicación útil y otra que es totalmente frustrante radica en la organización de los menús. En Java, la jerarquía de clases que intervienen en la construcción y manipulación de menús pertenece al paquete java.awt y es la que se muestra en el siguiente esquema:

Page 171: Fundamentos de JAVA

Fundamentos de Java /171

Cada una de estas clases se utiliza para lo siguiente: • MenuComponent, es la superclase de todos los componentes relacionados con menús. • MenuItem, representa una opción en un menú. • MenuShortcut, representa el acelerador de teclado, o la combinación de teclas rápidas, para acceder a un elemento de menú. • Menu, es un componente de una barra de menú. • MenuBar, encapsula el concepto de una barra de menú en un Frame. • PopupMenu, implementa un menú que puede ser presentado dinámicamente dentro de un componente. • CheckboxMenuItem, genera una caja de selección que representa una opción en un menú.

2.4.1. Menú principal (clase «Menu»). Ésta es la clase que se utiliza para construir los menús que se manejan habitualmente, conocidos como menús de persiana (o pull-down). Dispone de varios constructores para poder, entre otras cosas, crear los menús con o sin etiqueta. No tiene campos y proporciona varios métodos que se pueden utilizar para crear y mantener los menús en tiempo de ejecución. 2.4.2. Opciones de menú (clase «MenuItem»). Esta clase se emplea para instanciar los objetos que constituirán los elementos seleccionables del menú. No tiene campos y dispone de varios constructores, entre los que hay que citar a:

• MenuItem( String s, MenuShortcut ms) Crea un elemento el menú con una combinación de teclas asociada para acceder directamente a él.

Esta clase proporciona una veintena de métodos, entre los que destacan los que se citan ahora: • addActionListener( ActionListener )

Añade el receptor específico que va a recibir eventos desde esa opción del menú. • removeActionListener( ActionListener )

Es contrario al anterior, por lo que ya no se recibirán eventos desde esa opción del menú. • setEnabled( boolean )

Indica si esa opción del menú puede estar o no seleccionable. • isEnabled()

Comprobación de si la opción del menú está habilitada 2.4.3. Aceleradores de teclado (clase «MenuShortcut»). Esta clase se utiliza para instanciar un objeto que representa un acelerador de teclado, o una combinación de teclas rápidas, para un determinado MenuItem. No tiene campos y dispone de dos constructores. Aparentemente, casi todas las teclas rápidas consisten en mantener pulsada la tecla Control a la vez que se pulsa cualquier otra tecla. Uno de los constructores de esta clase:

• MenuShortcut( int tecla, boolean b) Dispone de un segundo parámetro que indica si el usuario ha de mantener también pulsada la tecla de cambio a mayúsculas (Shift). El primer parámetro es el código de la tecla, que es el mismo que se devuelve en el campo keyCode del evento KeyEvent, cuando se pulsa una tecla.

La clase KeyEvent define varias constantes simbólicas para estos códigos de teclas, como son: VK_8, VK_9, VK_A, VK_B… 2.4.4. Barras de menús (clase «MenuBar»). Esta clase no tiene campos, sólo tiene un constructor público, y es la clase que representa el concepto que todo usuario tiene de la barra de menú que está presente en la mayoría de las aplicaciones gráficas basadas en ventanas.

MenuComponent

Menubar MenuItem

PopupMenu

CheckboxMenuItem Menu

MenuShortcut

Page 172: Fundamentos de JAVA

Fundamentos de Java /172

El código siguiente ilustra algunos de los aspectos que intervienen en los menús. Es una aplicación que coloca dos menús sobre un objeto Frame. Uno de los menús tiene dos opciones y el otro, tres. La primera opción del primer menú también tiene asignada una combinación de teclas rápidas: Ctrl+Shift+K. import java.awt.*;

import java.awt.event.*;

public class ClaseConMenu {

public ClaseConMenu () {

// Se instancia un objeto de tipo Acelerador de Teclado

MenuShortcut miAcelerador = new MenuShortcut( KeyEvent.VK_K,true );

// Se instancian varios objetos de tipo Elementos de Menu

MenuItem primerElementoDeA = new MenuItem("Primer Elemento del Menu A",miAcelerador );

MenuItem segundoElementoDeA = new MenuItem("Segundo Elemento del Menu A" );

MenuItem primerElementoDeB = new MenuItem("Primer Elemento del Menu B" );

MenuItem segundoElementoDeB = new MenuItem("Segundo Elemento del Menu B" );

MenuItem tercerElementoDeB = new MenuItem("Tercer Elemento del Menu B" );

// Se instancia un objeto ActionListener y se registra sobre los objetos MenuItem

primerElementoDeA.addActionListener( new MiGestorDeMenu() );

segundoElementoDeA.addActionListener( new MiGestorDeMenu() );

primerElementoDeB.addActionListener( new MiGestorDeMenu() );

segundoElementoDeB.addActionListener( new MiGestorDeMenu() );

tercerElementoDeB.addActionListener( new MiGestorDeMenu() );

// Se instancian dos objetos de tipo Menu y se les añaden los objetos MenuItem

Menu menuA = new Menu( "Menu A" );

menuA.add( primerElementoDeA );

menuA.add( segundoElementoDeA );

Menu menuB = new Menu( "Menu B" );

menuB.add( primerElementoDeB );

menuB.add( segundoElementoDeB );

menuB.add( tercerElementoDeB );

// Se instancia una Barra de Menu y se le añaden los Menus

MenuBar menuBar = new MenuBar();

menuBar.add( menuA );

menuBar.add( menuB );

// Se instancia un objeto Frame y se le asocia el objeto MenuBar.

Frame miFrame = new Frame( "Ejemplo de ventana con menú AWT" );

miFrame.setMenuBar( menuBar );

miFrame.setSize( 250,100 );

miFrame.setVisible( true );

// Se instancia y registra un receptor de eventos de ventana para

// concluir el programa cuando se cierre el Farme

miFrame.addWindowListener( new WindowAdapter {

public void windowClosing( WindowEvent evt ) {

System.exit( 0 );

});

}

// Clase para instanciar un objeto ActionListener que se registra sobre los elementos del menu

class MiGestorDeMenu implements ActionListener {

public void actionPerformed( ActionEvent evt ) {

// Presenta en pantalla el elemento que ha generado el evento de tipo Action

System.out.println( evt.getSource() );

}

}

public static void main(String args[]){

ClaseConMenu ccm = new ClaseConMenu ();

}

}

Page 173: Fundamentos de JAVA

Fundamentos de Java /173

2.4.5. Opciones de menú señalizables (clase «CheckboxMenuItem»). Esta clase se utiliza para instanciar objetos que puedan utilizarse como opciones en un menú. Al contrario que las opciones de menú que se han visto al hablar de objetos MenuItem, estas opciones tienen mucho más parentesco con las cajas de selección. Esta clase no tiene campos y proporciona tres constructores públicos, en donde se puede especificar el texto de la opción y el estado en que se encuentra. Si no se indica nada, la opción estará deseleccionada, aunque hay un constructor que permite indicar en un parámetro de tipo booleano, que la opción se encuentra seleccionada, o marcada, indicando true en ese valor. De los métodos que proporciona la clase, quizá el más interesante sea el método que tiene la siguiente declaración: addItemListener( ItemListener ) Cuando se selecciona una opción del menú, se genera un evento de tipo ItemEvent. Para que se produzca la acción que se desea con esa selección, es necesario instanciar y registrar un objeto ItemListener que contenga el método itemStateChanged() sobrescrito con la acción que se quiere. 2.4.6. Menús contextuales (clase «PopupMenu»). Esta clase se utiliza para instanciar objetos que funcionan como menús emergentes o pop-up. Una vez que el menú aparece en pantalla, el procesado de las opciones es el mismo que en el caso de los menús de persiana. Esta clase no tiene campos y proporciona un par de constructores y un par de métodos, de los cuales el más interesante es el método show(), que permite mostrar el menú emergente en una posición relativa al componente origen. Este componente origen debe estar contenido dentro de la jerarquía de padres de la clase PopupMenu. El programa siguiente, coloca un objeto PopupMenu sobre un objeto Frame. El menú contiene tres opciones de tipo CheckboxMenuItem, y aparece cuando se pincha dentro del Frame, posicionando su esquina superior-izquierda en la posición en que se encontraba: import java.awt.*;

import java.awt.event.*;

public class VentanaConPopupMenu {

public VentanaConPopupMenu () {

// Instancia objetos CheckboxMenuItem

CheckboxMenuItem primerElementoMenu = new CheckboxMenuItem( "Primer Elemento" );

CheckboxMenuItem segundoElementoMenu = new CheckboxMenuItem( "Segundo Elemento" );

CheckboxMenuItem tercerElementoMenu = new CheckboxMenuItem( "Tercer Elemento" );

// Se instancia un objeto ItemListener y se registra sobre los elementos de menu ya instanciados

primerElementoMenu.addItemListener( new ControladorCheckBox() );

segundoElementoMenu.addItemListener( new ControladorCheckBox() );

tercerElementoMenu.addItemListener( new ControladorCheckBox() );

// Instancia un objeto Menu de tipo PopUp y le añade los objetos CheckboxMenuItem

PopupMenu miMenuPopup = new PopupMenu( "Menu Popup" );

miMenuPopup.add( primerElementoMenu );

miMenuPopup.add( segundoElementoMenu );

miMenuPopup.add( tercerElementoMenu );

// Se crea la ventana principal

Frame miFrame = new Frame( "Ejemplo de ventana con menú emergente" );

miFrame.addMouseListener( new ControladorRaton(miFrame,miMenuPopup) );

// Aquí está la diferencia con los Menus de Barra

miFrame.add( miMenuPopup );

miFrame.setSize( 250,100 );

miFrame.setVisible( true );

// Instancia y registra un receptor de eventos de ventana para terminar el programa cuando se cierra el Frame

miFrame.addWindowListener( new WindowAdapter {

public void windowClosing( WindowEvent evt ) {

System.exit( 0 );

Page 174: Fundamentos de JAVA

Fundamentos de Java /174

});

}

// Clase para atrapar los eventos de pulsacion del raton y presentar en la pantalla el objeto menu Popup,

// en la posicion en que se encontraba el cursor

class ControladorRaton extends MouseAdapter{

Frame aFrame;

PopupMenu aMenuPopup;

// Constructor parametrizado

ControladorRaton( Frame frame,PopupMenu menuPopup ) {

aFrame = frame;

aMenuPopup = menuPopup;

}

public void mousePressed( MouseEvent evt ) {

// Presenta el menu PopUp sobre el Frame que se especifique y en las coordenadas determinadas por

// el click del raton, cuidando de que las coordenadas no se encuentren situadas sobre la barra de titulo,

// porque las coordenadas Y en esta zona son negativas

if( evt.getY() > 0 )

aMenuPopup.show( aFrame,evt.getX(),evt.getY() );

}

}

// Clase para instanciar un objeto receptor de eventos de los elementos del menu

class ControladorCheckBox implements ItemListener {

public void itemStateChanged( ItemEvent evt ) {

// Presenta en pantalla el elemento que ha generado el evento

System.out.println( evt.getSource() );

}

}

public static void main( String args[] ) {

VentanaConPopupMenu vcpm = new VentanaConPopupMenu ();

}

}

2.5. Gestores de distribución (“layouts managers”).

Los layout managers o gestores de distribución, en traducción literal, ayudan a adaptar los diversos componentes que se desean incorporar a un contenedor, es decir, especifican la apariencia que tendrán los componentes a la hora de colocarlos, controlando tamaño y posición (layout) automáticamente. Java dispone de varios, que implementan la interfaz LayoutManager, tal como se muestra en la imagen:

En el tratamiento de los layouts se utiliza un método de validación, de forma que los componentes son marcados como no válidos cuando un cambio de estado afecta a la geometría o cuando el contenedor tiene un hijo incorporado o eliminado. La validación se realiza automáticamente cuando se llama a pack() o show(). Los componentes visibles marcados como no válidos no se validan automáticamente.

LayoutManager

GridLayout CardLayout

FlowLayout GridBagLayout BorderLayout

Page 175: Fundamentos de JAVA

Fundamentos de Java /175

Todos los contenedores tienen asignados un layout manager por defecto, pero podemos cambiarlo por otro que derive de las cinco clases predefinidas en el paquete java.awt. 2.5.1. Diseño de flujo (clase «FlowLayout»). Es el más simple y el que se utiliza por defecto en todos los paneles si no se fuerza el uso de alguno de los otros. Los componentes añadidos a un panel con FlowLayout se encadenan en forma de lista. La cadena es horizontal, de izquierda a derecha, y se puede seleccionar el espaciado entre cada componente.

DISEÑO DE FRAME CON 2 BOTONES

CÓDIGO EN EL CONSTRUCTOR DE LA FRAME

. . .

Button boton1 = new Button( "Botón 1" );

Button boton2 = new Button( "Botón 2" );

this.setLayout( new FlowLayout() );

this.add( boton1 );

this.add( boton2 );

pack();

. . .

Si el contenedor se cambia de tamaño en tiempo de ejecución, las posiciones de los componentes se ajustarán automáticamente, para colocar el máximo número posible de componentes en la primera línea. Los componentes se alinean según se indique en el constructor. Si no se indica nada, se considera que los componentes que pueden estar en una misma línea estarán centrados, pero también se puede indicar que se alineen a izquierda o derecha en el contenedor. 2.5.2. Diseño de borde (clase «BorderLayout»). La composición BorderLayout (de borde) proporciona un esquema más complejo de colocación de los componentes en un panel. La composición utiliza cinco zonas para colocar los componentes sobre ellas: Norte (BordeLayout.NORTH), Sur (BordeLayout.SOUTH), Este (BordeLayout.EAST), Oeste (BordeLayout.WEST) y Centro (BordeLayout.CENTER). Es el layout o composición que se utilizan por defecto Frame y Dialog. El Norte ocupa la parte superior del panel, el Este ocupa el lado derecho, Sur la zona inferior y Oeste el lado izquierdo. Centro representa el resto que queda, una vez que se hayan rellenado las otras cuatro partes. Así, este controlador de posicionamiento resuelve los problemas de cambio de plataforma de ejecución de la aplicación, pero limita el número de componentes que pueden ser colocados en contenedor a cinco; aunque, si se va a construir un interfaz gráfico complejo, algunos de estos cinco componentes pueden contenedores, con lo cual el número de componentes puede verse ampliado. En los cuatro lados, los componentes se colocan y redimensionan de acuerdo a sus tamaños preferidos y a los valores de separación que se hayan fijado al contenedor. El tamaño prefijado y el tamaño mínimo son dos informaciones muy importantes en este caso, ya que un botón puede ser redimensionado a proporciones cualesquiera; sin embargo, el diseñador puede fijar un tamaño preferido para la mejor apariencia del botón. El controlador de posicionamiento puede utilizar este tamaño cuando no haya indicaciones de separación en el contenedor, o puede ignorarlo, dependiendo del esquema que utilice. Ahora bien, si se coloca una etiqueta en el botón, se puede indicar un tamaño mínimo de ese botón para que siempre sea visible, al menos, el rótulo del botón. En este caso, el controlador de posicionamiento muestra un total respeto a este valor y garantiza que por lo menos ese espacio estará disponible para el botón.

DISEÑO DE LA FRAME

CÓDIGO EN EL CONSTRUCTOR DE LA FRAME . . .

this.setTitle( "AWTapp" );

this.setLayout( new BorderLayout(1,1) );

// En el layout se indica una separación horizontal

// y vertical de 1 pixel

this.add( new Button( "Sur" ), BordeLayout.SOUTH );

this.add( new Button( "Oeste" ), BorderLayout.WEST );

this.add( new Button( "Norte" ), BorderLayout.NORTH );

this.add( new Button( "Este" ), BorderLayout.EAST);

this.add( new Button( "Centro" ), BorderLayout.CENTER );

pack();

. . .

Page 176: Fundamentos de JAVA

Fundamentos de Java /176

2.5.3. Diseño de rejilla (clase «GridLayout»). La composición GridLayout proporciona gran flexibilidad para situar componentes. El controlador de posicionamiento se crea con un determinado número de filas y columnas y los componentes van dentro de cada una de las celdas de la tabla así definida. Si el contenedor es alterado en su tamaño en tiempo de ejecución, el sistema intentará mantener el mismo número de filas y columnas dentro de los márgenes de separación que se hayan indicado. En este caso, estos márgenes tienen prioridad sobre el tamaño mínimo que se haya indicado para los componentes, por lo que puede llegar a conseguirse que sean de un tamaño tan pequeño que sus etiquetas sean ilegibles.

DISEÑO DE REJILLA DE 2x3

CÓDIGO EN EL CONSTRUCTOR DE LA FRAME . . .

this.setLayout( new GridLayout(2,3) );

// Se colocan los seis botones

for( int i=0; i < 6; i++)

this.add( new Button( "Boton"+i ) );

pack();

. . .

2.5.4. Diseño de bolsa de rejilla (clase «GridBagLayout»). Es igual que la composición de GridLayout, con la diferencia que los componentes no necesitan tener el mismo tamaño. Es quizá el controlador de posicionamiento más sofisticado de los que actualmente soporta AWT. La forma de visualizarse el conjunto de celdas, puede determinarse a través de una serie de características recogidas en un objeto de tipo GridBagConstraints. El objeto GridBagConstraints se inicializa a unos valores de defecto, cada uno de los cuales puede ser ajustado para alterar la forma en que se presentan los componentes dentro del layout. Las propiedades que podemos modificar son:

• gridx y gridy, indican la fila y la columna, respectivamente, en donde se va a colocar el componente. La primera fila de la parte superior es gridx=0, y la columna más a la izquierda corresponde a gridy=0. • gridwidth y gridheight indican el número de celdas en la zona de presentación que va a ocupar un determinado componente. Los valores por defecto son una fila de ancho y una columna de alto, es decir, gridwidth=1 y gridheight=1. • weightx y weighty determinan la forma en que se van a redimensionar los componentes. Por defecto, los valores de estos parámetros es 0, lo que significa que cuando la ventana es redimensionada, los componentes permanecen juntos y agrupados en el centro del contenedor. Si se proporciona un valor mayor que 0 para weightx, los componentes se expandirán en la dirección x, horizontalmente. Si se proporciona un valor mayor que 0 para weighty, los componentes se expandirán en la dirección y, verticalmente. • fill determina la forma en que un componente rellena el área definida por gridx/gridy/gridwidth/gridheight; y los valores que puede tomar son: GridBagConstraints.HORIZONTAL (se expande horizontalmente para rellenar todo el área de visualización), GridBagConstraints.VERTICAL (se expande verticalmente para rellenar todo el área de visualización), GridBagConstraints.BOTH (se expande completamente para ocupar la totalidad del área de visualización) y GridBagConstraints.NONE (es reducido a su tamaño ideal, independientemente del tamaño que tenga la zona de visualización). • anchor, cuando un componente es más pequeño que la zona de visualización, se puede colocar en una determinada posición utilizando el parámetro anchor que puede tomar el valor GridBagConstrints.CENTER, que es el que toma por defecto, o cualquiera de las direcciones de los puntos cardinales: NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, NORTHWEST. • ipadx e ipady, el controlador de posicionamiento calcula el tamaño de un componente basándose en los parámetros del GridBagLayout y en los otros componentes que se encuentren sobre el layout. Se puede indicar un desplazamiento interno para incrementar el tamaño calculado para un componente y hacer que sea más ancho, ipadx, o más alto, ipady, que el tamaño real que calcularía el controlador, aunque no llegue a llenar completamente la zona de visualización en dirección horizontal o vertical, al estilo que hace el parámetro fill. • insets permite especificar la mínima cantidad de espacio que debe haber entre el componente y los bordes del área de visualización. Por defecto, el espacio determinado por insets se deja en blanco, pero también se puede rellenar con un color, o una imagen o un título.

Page 177: Fundamentos de JAVA

Fundamentos de Java /177

DISEÑO DE LA FRAME

CÓDIGO EN EL CONSTRUCTOR DE LA FRAME . . .

GridBagLayout gridbag = new GridBagLayout();

GridBagConstraints gbc = new GridBagConstraints();

this.setLayout( gridbag );

// Para este grupo fijamos la anchura y vamos

// variando alguna de las caracteristicas en los

// siguientes, de tal forma que los botones que

// aparecen en cada una de las lineas tengan

// apariencia diferente en pantalla

gbc.fill = GridBagConstraints.BOTH;

gbc.weightx = 1.0;

Button boton0 = new Button( "Botón 0" );

gridbag.setConstraints( boton0,gbc );

this.add( boton0 );

Button boton1 = new Button( "Botón 1" );

gridbag.setConstraints( boton1,gbc );

this.add( boton1 );

Button boton2 = new Button( "Botón 2" );

gridbag.setConstraints( boton2,gbc );

this.add( boton2 );

gbc.gridwidth = GridBagConstraints.REMAINDER;

Button boton3 = new Button( "Botón 3" );

gridbag.setConstraints( boton3,gbc );

this.add( boton3 );

gbc.weightx = 0.0;

Button boton4 = new Button( "Botón 4" );

gridbag.setConstraints( boton4,gbc );

this.add( boton4 );

gbc.gridwidth = GridBagConstraints.RELATIVE;

Button boton5 = new Button( "Botón 5" );

gridbag.setConstraints( boton5,gbc );

this.add( boton5 );

gbc.gridwidth = GridBagConstraints.REMAINDER;

Button boton6 = new Button( "Botón 6" );

gridbag.setConstraints( boton6,gbc );

this.add( boton6 );

gbc.gridwidth = 1;

gbc.gridheight = 2;

gbc.weighty = 1.0;

Button boton7 = new Button( "Botón 7" );

gridbag.setConstraints( boton7,gbc );

this.add( boton7 );

gbc.weighty = 0.0;

gbc.gridwidth = GridBagConstraints.REMAINDER;

gbc.gridheight = 1;

Button boton8 = new Button( "Botón 8" );

gridbag.setConstraints( boton8,gbc );

this.add( boton8 );

Button boton9 = new Button( "Botón 9" );

gridbag.setConstraints( boton9,gbc );

pack()

. . .

2.5.5. Diseño de carta (clase «CardLayout»). Éste es el tipo de composición que se utiliza cuando se necesita una zona de la ventana que permita colocar distintos componentes superpuestos en esa misma zona, como si trasparencias o cartas amontonadas. El orden de las cartas se puede establecer en el método add() del contenedor pasando como segundo argumento un índice. Para navegar por las cartas, esta clase dispone de los métodos firs(), last(), previous(), next() y show().Este layout suele ir asociado con botones de selección (Choice), de tal modo que cada selección determina la carta que se presentará.

UN PANEL CON 2 CARTAS SUPERPUESTAS

Y 2 BOTONES PARA MOSTRARLAS

CÓDIGO EN EL CONSTRUCTOR DE LA FRAME . . .

Panel panelCarta = new Panel();

panelCarta.setLayout(new CardLayout() );

panelCarta.add(new Label("Primera carta"), "1");

panelCarta.add(new Label("Segunda carta"), "2");

this.add(panelCarta);

Button boton1 = new Button("Ver carta 1");

boton1.addActionListener( . . . );

this.add(boton1);

Button boton2 = new Button("Ver carta 2");

boton2.addActionListener( . . . );

this.add(boton2);

pack();

. . .

2.5.6. Posicionamiento absoluto. Los componentes se pueden colocar en contenedores utilizando cualquiera de los controladores de posicionamiento, o utilizando posicionamiento absoluto para realizar esta función. La primera forma de colocar los componentes se considera más segura porque automáticamente serán compensadas las diferencias que se puedan encontrar entre resoluciones de pantalla de plataformas distintas.

Page 178: Fundamentos de JAVA

Fundamentos de Java /178

La clase Component proporciona métodos para especificar la posición y tamaño de un componente en coordenadas absolutas indicadas en píxeles: setBounds( int,int,int,int );

setBounds( Rectangle );

La posición y tamaño si se especifica en coordenadas absolutas, puede hacer más difícil la consecución de una apariencia uniforme, en diferentes plataformas.

2.6. La clase «Toolkit».

La clase java.awt.Toolkit es una clase abstracta que proporciona una interfaz independiente de plataforma para servicios específicos de esas plataformas, como pueden ser: fuentes de caracteres, imágenes, impresión y parámetros de pantalla. Las subclases de Toolkit se usan para subordinar varios componentes a la implementación nativa concreta del sistema gráfico subyacente. Muchas operaciones de la interfaz de usuario pueden ser realizadas asíncronamente. Esto significa que si asignamos el estado de un componente, e inmediatamente consultamos el estado, el valor retornado puede no reflejar el cambio asignado. Esto incluye, pero no sólo, a:

• Desplazarse a una posición específica. Por ejemplo, invocando ScrollPane.setScrollPosition() y entonces el método getScrollPosition() puede retornar un valor incorrecto si el cambio aún no ha sido procesado. • Mover el foco de un componente a otro. • Hacer visible un contenedor a un nivel superior. La invocación de setVisible(true) sobre un objeto Window, Frame o Dialog puede ocurrir asíncronamente. • Asignar el tamaño y posición de un contenedor a un nivel superior. Las invocaciones de setSize(), setBounds() o setLocation() sobre objetos Window, Frame o Dialog son trasladadas al sistema gestor de ventanas subyacente, el cual puede ignorar la invocación o modificarla.

Los métodos de la clase Toolkit son el pegamento que une las clases dependientes de plataforma del paquete java.awt con sus contrapartidas del paquete java.awt.peer. Algunos métodos definidos por Toolkit consultan el sistema operativo nativo directamente. Como el constructor de la clase es abstracto podemos obtener un objeto Toolkit mediante la invocación del método Toolkit.getDefaultToolkit(), que devolverá un objeto de este tipo adecuado a la plataforma nativa. Algunos de los métodos de esta clase que podemos utilizar son:

• Dimension getScreenSize(), retorna el tamaño de la pantalla primaria. • int getScreenResolution(), retorna la resolución de pantalla en puntos por pulgada. • Insets getScreenInsets(GraphicsConfiguration gc), retorna los bordes internos de la pantalla. • Image getImage(String filename), carga una imagen desde una ruta de archivo. • Image getImage(URL url), carga una imagen desde una url. • Image createImage(String filename), carga una imagen desde una ruta de archivo. Primero se verifica si hay un gestor de seguridad instalado. • Image createImage(URL url), carga una imagen desde una url. Primero se verifica si hay un gestor de seguridad instalado. • Image createImage(byte[] imagedata), crea una imagen a partir de un array de bytes. Los datos deben contener un formato de imagen soportado, como gif, jpeg o png. • PrintJob getPrintJob(Frame frame, String jobtitle, Properties props), y • PrintJob getPrintJob(Frame frame, String jobtitle, JobAttributes jobAttributes, PageAttributes pageAttributes), abren el cuadro de diálogo de configuración de impresora y retorna un objeto con las configuraciones seleccionadas. • void beep(), emite un pitido. • Clipboard getSystemClipboard(), retorna el objeto que representa el portapapeles del sistema nativo subyacente. • Cursor createCustomCursor(Image cursor, Point hotSpot, String name), crea un nuevo cursor para el ratón a partir de una imagen. • Dimension getBestCursorSize(int preferredWidth, int preferredHeight), retorna las dimensiones preferidas de los cursores del sistema nativo. • int getMaximumCursorColors(), retorna el número máximo de colores que soporta el sistema nativo para los cursores.

Page 179: Fundamentos de JAVA

Fundamentos de Java /179

2.7. Imprimir con AWT.

De entre los muchos métodos de la clase Toolkit el que representa el máximo interés en este momento es el método getPrintJob(), que devuelve un objeto de tipo PrintJob para usarlo en la impresión desde Java. En Java hay, al menos, dos formas de poder imprimir. Una es coger un objeto de tipo Graphics, que haga las veces del papel en la impresora y dibujar, o pintar, sobre ese objeto. La otra, consiste en preguntar a un componente, o a todos, si tienen algo que imprimir, y hacerlo a través del método printAll(). 2.7.1. Imprimir usando la clase «Graphics». A continuación se muestra la primera forma de imprimir para hacer aparecer el saludo ya conocido del "¡Hola Mundo!" en la impresora. import java.awt.*;

class HolaMundoPrn extends Frame {

static public void main( String args[] ) {

// Creamos un Frame para obtener un objeto PrintJob sobre él

Frame f = new Frame( "Prueba" );

f.pack();

// Se obtiene el objeto PrintJob. El sistema muestra el cuadro de diálogo de Control de Impresión.

PrintJob pjob = f.getToolkit().getPrintJob( f,"Impresion del Saludo",null );

// Si el cuadro de diálogo se cierra sin pulsar el botón imprimir retorna el valor null.

if (pjob != null) {

// Se obtiene el objeto graphics sobre el que pintar

Graphics pg = pjob.getGraphics();

// Se fija el font de caracteres con que se escribe

pg.setFont( new Font( "SansSerif",Font.PLAIN,12 ) );

// Se escribe el mensaje de saludo

pg.drawString( "¡Hola Mundo!",100,100 );

// Se finaliza la página

pg.dispose();

// Se hace que la impresora termine el trabajo y escupa la página

pjob.end();

}

// Se acabó

System.exit( 0 );

}

}

Aunque sencillo, en el código se pueden observar las acciones y precauciones que hay que tomar a la hora de mandar algo a imprimir, y que se resumen en la siguiente lista:

• El objeto PrintJob se debe crear sobre un Frame, con lo cual se debe asociar siempre a las aplicaciones visuales. • Cuando se crea una clase PrintJob el sistema presenta el cuadro de diálogo de control de la impresora, en donde se puede seleccionar el tipo de impresora, el tamaño del papel o el número de copias que se desean obtener. • Aunque el objeto PrintJob se ha de crear sobre un Frame no es necesario que éste sea visible o tenga un tamaño distinto de cero. • Antes de escribir nada en la impresora es necesario seleccionar la fuente de caracteres con que se desea hacerlo, el sistema no proporciona ninguna fuente de caracteres por defecto. • La impresión se consigue pintando sobre el objeto Graphics de la impresora. • La impresión se realiza página a página, de tal modo que cada una de ellas tiene su propio objeto Graphics. El método dispose() se utiliza para completar cada una de las páginas y que la impresora la lance.

2.7.2. Imprimir componentes. En el siguiente ejemplo, se crea un Frame con varios componentes y un botón que al ser pulsado hace que el contenido del Frame sea impreso: /* El propósito de este programa es mostrar cómo se imprimen componentes desde el AWT */

import java.awt.*;

import java.awt.event.*;

public class ClaseQueImprime {

// El contenedor miFrame y todo lo que contiene, será impresos cuando se pulse el botón "Imprimir Frame"

Page 180: Fundamentos de JAVA

Fundamentos de Java /180

Frame miFrame = new Frame( "Ejemplo de impresión de componentes AWT" );

public ClaseQueImprime () {

// Botón para imprimir el Frame

Button botonImpr = new Button( "Imprimir la ventana" );

botonImpr.addActionListener( new PrintActionListener() );

miFrame.add( botonImpr,"North" );

// Se añaden otros componentes al Frame

miFrame.add(new Button( "El botón 1" ),"West" );

miFrame.add(new Button( "El botón 2" ),"East" );

pack();

// Se muestra el Frame

miFrame.setSize( 340,200 );

miFrame.setVisible( true );

// Ésta es la clase anidada anónima que se utiliza para concluir el programa

miFrame.addWindowListener( new WindowAdapter() {

public void windowClosing( WindowEvent evt ) {

System.exit( 0 );

}

}

}

// Ésta es la clase anidada utilizada para imprimir el Frame

class PrintActionListener implements ActionListener {

public void actionPerformed( ActionEvent evt ) {

// Coge un objeto PrintJob. Esto hace que aparezca el diálogo estándar de impresión,

// que si se cierra sin imprimir devolverá un nulo

PrintJob miPrintJob = miFrame.getToolkit().

getPrintJob( miFrame,"Impresión de la ventana",null );

if( miPrintJob != null ) {

// Coge el objeto gráfico que va a imprimir

Graphics graficoImpresion = miPrintJob.getGraphics();

if( graficoImpresion != null ) {

// Invoca el método printAll() del Frame para hacer que sus componentes se se dibujen

// sobre el objeto gráfico y se pinten sobre el papel de la impresora

miFrame.printAll( graficoImpresion );

// Hacemos que se libere el papel de la impresora y los recursos del sistema

graficoImpresion.dispose();

} else {

System.out.println( "No se puede imprimir la ventana" );

}

// Se concluye la impresión y se realiza la limpieza necesaria

miPrintJob.end();

} else {

System.out.println( "Impresion cancelada" );

}

}

}

public static void main( String args[] ) {

ClaseQueImprime ihm = new ClaseQueImprime ();

}

}

2.8. Gráficos.

Toda la parte gráfica de Java se basa en la clase Graphics, por lo que esta sección se dedicará casi exclusivamente al uso de esta clase para manejar formas, fuentes de caracteres e imágenes sobre la pantalla. 2.8.1. El sistema de coordenadas. Cada uno de los componentes de Java tiene su propio sistema de coordenadas, que va desde la posición (0,0) hasta la posición determinada por su anchura total y altura total, menos una unidad; la unidad de medida son píxeles de pantalla. Como se puede apreciar en la figura siguiente, la esquina superior izquierda del

Page 181: Fundamentos de JAVA

Fundamentos de Java /181

componente es la posición que coincide con las coordenadas (0,0). La coordenada en el eje de abscisas se incrementa hacia la derecha y en las ordenadas hacia abajo.

A la hora de pintar un componente se debe tener en cuenta además del tamaño de ese componente, el tamaño del borde del componente, si lo tuviera. Por ejemplo, un borde que ocupa un píxel alrededor de un componente haría que la coordenada de la esquina superior izquierda pasase de ser (0,0) a ser ahora (1,1), y reducirá además la anchura y altura totales del componente en dos píxeles, uno por cada lado. Las dimensiones de un componente se pueden conocer a través de sus métodos getWidth() y getHeight(). El método getInsets() retorna un objeto de tipo java.awt.Insets que permite conocer el tamaño del borde. 2.8.2. Pintando en AWT. Antes de entrar en más profundidades es necesario entender exactamente cómo funciona el mecanismo de repintado que utiliza Java, para poder comprender cómo se producen y recogen los eventos que redibujan el contenido de los componentes. En AWT hay dos mecanismos por los que se producen las operaciones de repintado, dependiendo de quién sea el que ordena ese repintado, el sistema o la aplicación. En el caso de que sea el sistema el que ordena el repintado, él es quien indica a un componente que debe regenerar su contenido, y las razones más normales por las que hace esto son:

- El componente se hace visible por primera vez en la pantalla. - El componente ha cambiado de tamaño. - El componente se ha deteriorado y necesita ser regenerado; por ejemplo, estaba medio tapado por otro componente que ahora se ha movido, con lo cual hay una zona del componente que estaba oculta y ahora debe mostrarse.

En el caso de que el repintado sea ordenado por la aplicación, es el propio componente el que decide la necesidad de la actualización; normalmente debido a algún cambio en su estado interno, por ejemplo, un botón detecta que el ratón ha sido pulsado sobre él y determina que tiene que cambiar su imagen de botón normal a botón pulsado. Independientemente de quién sea el que origina la petición de repintado, el AWT utiliza un mecanismo de retrollamada para ese repintado. Esto significa que un programa debe colocar su código de repintado dentro de un método sobrecargado, y será Java quien se encargará de invocarlo en el momento del repintado. Este método se encuentra en la clase base java.awt.Component: public void paint( Graphics g )

Cuando el AWT llama a este método, el objeto Graphics que se pasa como parámetro está preconfigurado con el estado adecuado al pintado en ese componente determinado:

- El color del objeto Graphics se fija a la propiedad foreground del componente. - La fuente de caracteres se fija a la propiedad font del componente. - La traslación también se determina, teniendo en cuenta que la coordenada (0,0) representa la esquina superior-izquierda del componente - El rectángulo de recorte, o clipping, se fija al área del componente que es necesario repintar.

El programa debe utilizar este objeto Graphics, o uno derivado de él, para redibujar la salida. En general se debe evitar en todos los programas escribir código que dibuje algo fuera del ámbito del método paint(). El porqué se debe a que ese código puede ser invocado a veces cuando no es adecuado que se haga; por ejemplo, antes de hacer visible al componente o tener acceso a un objeto Graphics válido. No es nada recomendable que los programas invoquen directamente al método paint(). Para forzar el repintado desde código AWT proporciona los siguientes métodos de la clase java.awt.Component:

• public void repaint(), repinta todo el componente. • public void repaint( int x,int y,int width,int height ), repinta una zona especificada. • public void repaint( long tm ), repinta todo el componte antes de un tiempo especificado. • public void repaint( long tm,int x,int y,int width,int height ), repinta una zona en un tiempo especificado.

Los componentes que realicen operaciones complejas, deberían invocar al método repaint() con argumentos definiendo solamente la región que necesita actualización. Un error muy común es llamar a repaint() sin

Page 182: Fundamentos de JAVA

Fundamentos de Java /182

ningún parámetro, lo que hace que se repinte el componente completo; lo que hará, sin lugar a dudas, que se realicen repintados que no son necesarios. En los componentes normales los dos tipos de origen del repintado se producen de dos formas distintas, dependiendo de que la operación sea ordenada por el sistema o por la aplicación. En el caso de que sea el sistema el que ordena el repintado:

- El AWT determina si el componente necesita ser repintado completamente o solamente parte de él. - El AWT lanza el evento para invocar al método paint() sobre el componente.

Si quien ordena el repintado es la aplicación, lo que sucede es lo siguiente: - El programa determina si parte o todo el componente debe ser repintado, en respuesta a cambios en algún estado interno. - El programa invoca al método repaint() sobre el componente, el cual lanza una petición al AWT indicándole que ese componente necesita ser repintado - El AWT lanza el evento para invocar al método update() sobre el componente (si se acumulan varias llamadas a repaint() se unifican en una). La implementación por defecto de update() limpia el fondo del componente y luego hace una llamada a paint().

Por tanto, rescribiendo el método update() podemos aligerar el repintado de un componente. 2.8.3. Clase «Graphics». La clase java.awt.Graphics representa un contexto gráfico, y dispone de métodos para escribir, dibujar y mostrar imágenes sobre el área de dibujo de un componente. Un contexto gráfico define una zona de recorte, una zona a la que va a afectar; cualquier operación gráfica que se realice modificará solamente los píxeles que se encuentren dentro de los límites de la zona de recorte actual y el componente que fue utilizado para crear el objeto Graphics. Cada componente visual dispone de su propio contexto gráfico, que puede obtenerse con su método getGraphics(). Además, el método paint() de todos los componentes pasan como argumento su contexto gráfico. Primitivas gráficas:

• void drawLine (int x1, int y1, int x2, int y2) Dibuja una línea entre dos puntos.

• void drawRect (int x, int y, int ancho, int alto) Dibuja un rectángulo.

• void fillRect (int x, int y, int ancho, int alto) Dibuja un rectángulo relleno con el color actual.

• void clearRect (int x, int y, int ancho, int alto) Dibuja un rectángulo relleno con el color de fondo.

• void draw3DRect (int x, int y, int anch, int alt, boolean resalte) Dibuja un rectángulo resaltado.

• void fill3DRect (int x, int y, int anch, int alt, boolean resalte) Dibuja un rectángulo relleno resaltado.

• void drawRoundRect (int x, int y, int ancho, int alto, int arcAncho, int arcAlto) Dibuja un rectángulo de esquinas redondeadas.

• void fillRoundRect (int x, int y, int ancho, int alto, int arcAncho, int arcAlto) Rellena un rectángulo de esquinas redondeadas.

• void drawOval (int x, int y, int ancho, int alto) Dibuja una elipse.

• void fillOval (int x, int y, int ancho, int alto) Rellena una elipse.

• void drawArc (int x, int y, int ancho, int alto, int inicioAngulo, int arcoAngulo) Dibuja un arco (ángulos en grados).

• void fillArc (int x, int y, int ancho, int alto, int inicioAngulo, int arcoAngulo) Rellena un arco (ángulos en grados).

• void drawPolygon (int x[], int y[], int numeroPuntos) Dibuja y cierra un polígono.

• void drawPolyline (int x[], int y[], int numeroPuntos) Dibuja un polígono sin cerrarlo.

• void fillPolygon (int x[], int y[], int numeroPuntos)

Page 183: Fundamentos de JAVA

Fundamentos de Java /183

Rellena un polígono. Métodos para dibujar texto:

• void drawBytes (byte datos[], int offset, int len, int x, int y) • void drawChars (byte datos[], int offset, int len, int x, int y) • void drawString (Strinng texto, int x, int y)

Donde offset indica el primer elemento y len el número de bytes o caracteres a imprimir. Métodos para dibujar imágenes:

• void drawImage (Image img, ... , ImageObserver observer) Existen varios métodos y todos incluyen un objeto que implementa la interface ImageObserver, el cual controla el estado descarga y visualización de la imagen (si programamos en un applet podremos utilizar la referencia this como argumento).

La interfaz java.awt.image.ImageObserver define un método que recibe notificaciones acerca de la información de una imagen de cómo es construida. El método que declara es:

boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)

Este método debería ser invocado desde un proceso asíncrono que está cargando una imagen usando métodos como getWidth(ImageObserver) y drawImage(img, x, y, ImageObserver). El método de la interfaz debe retornar true si son necesarias actualizaciones remotas o false si la información requerida ha sido adquirida. El primer argumento es la imagen que se está generando; el segundo es una combinación mediante el operador OR de las constantes WIDTH (ya está establecido el ancho de la imagen, asignado en el argumento width), HEIGHT (ya está establecida la altura de la imagen, asignada en el argumento heigth), PROPERTIES (las propiedades de la imagen ya están disponibles), SOMEBITS (ya se han generado más píxeles para escalar la imagen, el borde de los nuevos píxeles está determinado por los últimos argumentos), FRAMEBITS (en una imagen multi-marco, el marco previo ya es dibujable), ALLBITS (la imagen está lista para dibujar), ERROR (por algún error la imagen no se puede dibujar), ABORT (se abortó la generación de la imagen).

Métodos que determinan la zona de recorte (clipping): • void clipRect( int x, int y, int width, int height )

Realiza la intersección del rectángulo de clipping actual con el formado por los parámetros que se pasan al método. Este método solamente se puede utilizar para reducir el tamaño de la zona de clipping, no para aumentarla.

• Shape getClip() Devuelve la zona de clipping actual como un objeto de tipo Shape.

• Bound getClipBounds() Devuelve el rectángulo que delimita el borde de la zona de clipping actual.

• void setClip (int x, int y, int width, int height) Fija el rectángulo de clipping actual al indicado en las cuatro coordenadas que se pasan como parámetro al método.

Otros métodos de Graphics: • FontMetrics getFontMetrics()

Retorna el objeto FontMetrics del contexto gráfico. • translate( Point p )

Se utiliza para trasladar el origen del contexto gráfico al punto de coordenada que se pasa como parámetro al método. El método modifica el contexto gráfico de forma que el origen del sistema de coordenadas que se utilice en ese contexto gráfico corresponda al punto especificado. Todas las coordenadas utilizadas en posteriores operaciones gráficas sobre ese contexto gráfico, serán relativas al nuevo origen

• setXORMode() • setPaintMode()

Afectan a las operaciones de pintado determinando cómo se combinan los píxeles pintados con los del fondo. setXORMode() aplica el operador xor lógico en la combinación de píxeles, mientras que setPaintMode() no combina los píxeles.

2.8.4. Fuentes de letras (clases «Font» y «FontMetrics»). La clase import java.awt.Font se utiliza junto con el método Graphics.drawString() para especificar un tipo especifico de fuente para escribir texto. La clase FontMetrics sirve para definir el tamaño de los caracteres que se mostrarán.

Page 184: Fundamentos de JAVA

Fundamentos de Java /184

El constructor de la clase Font recibe 3 argumentos: el nombre de la fuente. (“serif”, “sanserif”, etc.), el estilo de la fuente (Font.PLAIN, Font.BOLD, Font.ITALIC), y el tamaño de la fuente. Una vez definido el objeto tipo Font es necesario aplicarlo al con método Graphics.setFont(objetoFont). Por ejemplo: public void paint(Graphics g) {

g.setColor(Color.blue);

Font f1 = new Font("serif",Font.PLAIN,12);

g.setFont(f1);

g.drawString("informatique.com.mx",50,25);

Font f2 = new Font("sanserif",Font.BOLD+ Font.ITALIC,12);

g.setFont(f2);

g.drawString("informatique.com.mx",50,50);

}

A través de la clase FontMetrics es posible obtener las propiedades definidas en un objeto de tipo Font.

Métodos de FontMetrics. • FontMetrics (Font fuente)

Constructor. • int getAscent (), int getMaxAscent ()

Retornan el ascendente actual y máximo. • int getDescent () • int getMaxDescent ()

Retornan el descendente actual y máximo. • int getHeight ()

Retorna la distancia entre líneas. • int getLeading ()

Retorna la distancia entre el descendente de una línea y el ascendente de la siguiente. • int getMaxAdvance ()

Retorna la mayor anchura de un caracter, incluyendo el espacio hasta el siguiente caracter. • int charWidth (char c) • int stringWidth (String s) • int charsWidth (char datos[], int inicio, int len)

Retornan el ancho del argumento al ser dibujados. Para crear un objeto FontMetrics se utiliza el método Graphics.getFontMetrics(). Por ejemplo: public void paint(Graphics g) {

g.setColor(Color.blue);

Font oF = new Font("serif",Font.ITALIC + Font.BOLD,14);

g.setFont(oF);

FontMetrics oFM = getFontMetrics(oF);

String oTexto = "informatique.com.mx";

//Centrar el texto a lo ancho

int iX = (getSize().width - oFM.stringWidth(oTexto)) / 2;

//Centrar el texto a lo alto

int iY = getSize().height / 2;

//Dibujar el texto al centro

g.drawString(oTexto,iX,iY);

}

Descendente

Ascendente

Leading

LÍNEA 1

LÍNEA 2

Altura

Page 185: Fundamentos de JAVA

Fundamentos de Java /185

2.8.5. Colores (clase «Color»). Java puede utilizar el sistema de descripción de colores llamado sRGB (Red, Green, Blue) para definir el color a utilizar. Este color se conforma por 3 números, cada uno de los cuales debe estar en el rango de 0 a 255. El valor mínimo es (0,0,0) el cual equivale al negro que es la ausencia de rojo, verde y azul; el valor máximo es (255,255,255) que equivale al blanco. En Java, un sistema de descripción de colores se conoce como espacio de colores sRGB. Java 2 soporta cualquier espacio de color deseado a través del objeto java.awt.Color. Para crear un color existen 2 formas de llamar al método constructor Color: Mediante 3 enteros que representen el valor sRGB del color deseado: Color unColor = new Color(255,255,255,255);

Mediante 3 números de punto flotante que representen el valor sRGB deseado: Color ofColor = new Color(0.437F,1F,0F);

Además, la clase Color define las constantes estáticas: black, white, green, blue, red, yellow, magenta, orange, pink, gray, darkGray, lighGray. Las cuales instancian un objeto de color predefinido. 2.8.6. Imágenes (clase «Image»). La clase abstracta java.awt.Image permite crear una imagen en memoria desde un archivo o crearla a partir de un productor. Métodos de Image:

• int getWidth (ImageObserver observer) • int getHeight (ImageObserver observer)

Retornan el ancho y alto de la imagen. La interface ImageObserver permite controlar el estado de carga y visualización de la imagen.

• Graphics getGraphicst () Crea y retorna un contexto gráfico para poder dibujar sobre la imagen en memoria.

• Image getScaledInstance (int ancho, int alto, int hints) Retorna una copia de la imagen a otra escala.

Al ser Image una clase abstracta no se puede instanciar directamente. Java permite crear una imagen de otras maneras:

1) A partir de un archivo de imagen compatible (como gif, jpg o png). Para ello se pueden utilizar las siguientes técnicas:

a) Mediante el método getImage() de las clases java.awt.Toolkit y java.applet.Component y de sus subclases. Por ejemplo:

Image imagen1 = java.awt.Toolkit.getDefaultToolkit().getImage("miImagen.gif");

Image imagen2 = (new Applet()).getImage("miImagen.gif");

b) Mediante la clase javax.swing.ImageIcon: Image imagen1 = new ImageIcon("miImagen.gif").getImage();

c) Usando los métodos estáticos de la clase javax.imageio.ImageIO: Image img = ImageIO.read(new File("miImagen.gif"));

El método read() de esta clase está sobrecargado para recibir como argumento varios orígenes de la imagen, y retorna la imagen como un objeto java.awt.image.BufferedImage. d) Si el archivo de imagen está incluido como un recurso del archivo jar del proyecto podemos utilizar el método getResource() de la clase Class. Supongamos que la clase principal se denomina Main:

URL url = Main.class.getResource("miImagen.gif");

Image imagen1 = java.awt.Toolkit.getDefaultToolkit().getImage( url );

2) Directamente en memoria especificando sus dimensiones, y a continuación utilizar su contexto gráfico para dibujar en ellas y mostrarlas. En un componente, como un applet o un panel, se puede crear un nuevo objeto Image mediante el método sobrecargado createImage(). Por ejemplo, el siguiente código, dentro de un applet, crea una imagen con un círculo rojo dibujado en su contorno interior.

Image imagen = this.createImage(100, 100);

Graphics g = imagen.getGraphics(); // se obtiene el lienzo de la imagen

g.setColor(Color.red); // líneas a color rojo

g.drawOval(0, 0, 100, 100); // dibuja el círculo

Con la ayuda de un productor de imágenes como la clase java.awt.image.MemoryImageSource, se pueden construir imágenes en memoria a partir de un array de bits. El siguiente ejemplo calcula una imagen de

Page 186: Fundamentos de JAVA

Fundamentos de Java /186

100x100 representando un degradado de colores del negro al azul a lo largo del eje X y un degradado del negro al rojo a lo largo del eje Y.

int w = 100; // ancho de la imagen

int h = 100; // alto de la imagen

int[] pix = new int[w * h]; // el array de bits

int index = 0;

for (int y = 0; y < h; y++) {

int red = (y * 255) / (h - 1);

for (int x = 0; x < w; x++) {

int blue = (x * 255) / (w - 1);

pix[index++] = (255 << 24) | (red << 16) | blue;

}

}

Image img = createImage(new MemoryImageSource(w, h, pix, 0, w));

2.9. El portapapeles (la clase «Clipboard»).

El portapapeles es un recurso del que disponen algunos sistemas operativos para almacenar y compartir y datos mediante operaciones de cortar, copiar y pegar. En Java se utiliza la clase Clipboard para acceder al portapapeles del sistema subyacente, y proporciona una serie de interfaces y clases para encapsular los datos que se quieren copiar al portapapeles. 2.9.1. La clase «Clipboard». La clase java.awt.datatransfer.Clipboard implementa un mecanismo para transferir datos usando las operaciones de cortar, copiar y pegar. Estas tres operaciones se deben implementar usando el método que copia y el método que recupera datos del portapapeles. El método Toolkit.getDefaultToolkit().getSystemClipboard() retorna un objeto Clipboard que representa el portapapeles del sistema nativo. Se copian datos en el portapapeles mediante el método setContents() y se recuperan mediante el método getContents(). Para conseguir independencia de la plataforma, los datos deben ser encapsulados en un objeto de tipo Transferable. Además, los datos contenidos en el portapapeles deben ser propiedad de un objeto del tipo ClipboardOwner, siendo dicho objeto pasado como argumento del método setContents(). Se pueden crear objetos oyentes, que implementen la interfaz java.awt.datatransfer.FlavorListener, para registrarlos sobre la instancia de la clase Clipboard y que sean notificados acerca de los cambios en el portapapeles. Para añadir y quitar objetos oyentes se usan los métodos addFlavorListener() y removeFlavorListener(). Los métodos que incluye esta clase se describen a continuación:

• Clipboard(String name) Constructor. Hay que asignar un nombre al portapapeles.

• String getName(), recupera el nombre asignado al portapapeles. • void setContents(Transferable contents, ClipboardOwner owner)

Este método sincronizado introduce un contenido en el portapapeles. El contenido se encapsula en un objeto Transferable y es propiedad de un objeto ClipboardOwner. Si el propietario actual de los datos del portapapeles es otro objeto diferente de owner, entonces el propietario actual es notificado automáticamente mediante el método ClipboardOwner.lostOwnership(). Si el portapapeles no está accesible se lanza una IllegalStateException.

• Transferable getContents(Object requestor)

Este método sincronizado retorna el contenido actual del portapapeles encapsulado en un objeto Transferable, o bien null si no hay contenido. El argumento requestor no se usa y por tanto admite el valor null. Si el portapapeles no está accesible se lanza una excepción del tipo IllegalStateException.

• DataFlavor[] getAvailableDataFlavors() Retorna un array de objetos DataFlavor, los cuales representan los tipos de contenidos MIME que soporta el portapapeles. Si no hay tipos de contenidos disponibles el método retorna un array vacío.

• boolean isDataFlavorAvailable(DataFlavor flavor) Indica si un tipo de contenido MIME específico es soportado por el portapapeles.

• Object getData(DataFlavor flavor) Retorna un objeto que encapsula el contenido actual del portapapeles en un tipo de contenido MIME especificado. El objeto retornado es de la clase encapsulada en el argumento flavor.

Page 187: Fundamentos de JAVA

Fundamentos de Java /187

• void addFlavorListener(FlavorListener listener) Este método sincronizado añade un objeto oyente del portapapeles.

• void removeFlavorListener(FlavorListener listener) Este método sincronizado elimina un objeto oyente del portapapeles.

• FlavorListener[] getFlavorListeners(), Este método sincronizado retorna un array con los objetos oyentes del portapapeles.

En el siguiente ejemplo se utiliza la clase java.awt.datatransfer.StringSelection, la cual implementa Transferable, para guardar contenidos de tipo strings en el portapapeles. // Obtenemos el portapapeles del sistema

Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();

// Guardamos un contenido de tipo string sin propietario

clip.setContents(new StringSelection("Un texto copiado"), null);

. . .

// Recuperamos el contenido del portapapeles

Transferable tf = clip.getContents(null);

// y lo imprimimos

try {

System.out.println(tf.getTransferData(DataFlavor.stringFlavor));

} catch (Exception ex) {

}

2.9.2. La interfaz «ClipboardOwner». La interfaz java.awt.datatransfer.ClipboardOwner permite definir clases que pueden copiar datos a un portapapeles. Una instancia que implementa esta interfaz viene a ser el propietario del contenido que se mete en el portapapeles y es pasado como argumento al método Clipboard.setContents(). Una instancia permanece como dueño del portapapeles hasta que otra aplicación u objeto copian sus datos al portapapeles. El método que declara esta interfaz es:

• void lostOwnership(Clipboard clipboard, Transferable contents), notifica al objeto que ya no es el propietario del portapapeles. Este método será invocado cuando otra aplicación u otro objeto se apropia del portapapeles. Los argumentos son el portapapeles del cual se ha perdido la propiedad, y el contenido que el objeto puso en el portapapeles.

2.9.3. La interfaz «Transferable» y la clase «StringSelection». La interfaz java.awt.datatransfer.Transferable permite definir clases que pueden ser usadas para encapsular los datos que queremos meter en un portapapeles. Los métodos que declara esta interfaz son:

• DataFlavor[] getTransferDataFlavors() Retorna un array de objetos DataFlavor, el cual indica los tipos de contenido MIME en los que se puede suministrar el dato. El array debe ordenarse de acuerdo a las preferencias con las que es suministrado el dato (desde las más descriptivas a las menos descriptivas).

• boolean isDataFlavorSupported(DataFlavor flavor) Indica si un tipo de contenido MIME especificado en el argumento es soportado.

• Object getTransferData(DataFlavor flavor) Retorna un objeto que representa los datos que serán transferidos en un tipo de contenido MIME especificado. Lanza una excepción del tipo UnsupportedFlavorException si el formato no es soportado para los datos.

Una clase predefinida que implementa esta interfaz es java.awt.datatransfer.StringSelection. Esta clase implementa a la vez Transferable y ClipboardOwner, y permite encapsular contenido de tipo string para copiar al portapapeles. La clase StringSelection soporta DataFlavor.stringFlavor y sus sabores equivales. El soporte para DataFlavor.plainTextFlavor y sus equivales es obsoleto. Otros tipos de DataFlavor no los soporta. Para usar esta clase basta con instanciarla y pasar un string en su constructor: Transferable texto = new StringSelection("un texto");

2.9.4. La clase «DataFlavor». La clase java.awt.datatransfer.DataFlavor proporciona meta información acerca de los datos que se pueden meter en un portapapeles o que pueden ser usados para operaciones de arrastrar y soltar.

Page 188: Fundamentos de JAVA

Fundamentos de Java /188

Una instancia de DataFlavor encapsula un tipo de contenido MIME. Un tipo de contenido MIME consiste de un tipo primario, un subtipo y parámetros opcionales. Por ejemplo, en el tipo MIME "image/x-java-

image;class=java.awt.Image" el tipo primario es image (una imagen), el subtipo es x-java-image (una imagen de java), y la representación de la clase es java.awt.Image. La clase proporciona constantes estáticas que representan tipos de contenidos habituales:

• DataFlavor.stringFlavor, representa un texto de la clase "java.lang.String" y el tipo MIME "application/x-java-

serialized-object". • DataFlavor.imageFlavor, representa una imagen de la clase "java.awt.Image" y el tipo MIME "image/x-java-

image". • DataFlavor.plainTextFlavor, representa un texto plano mediante la clase "java.io.InputStream" y el tipo MIME "text/plain; charset=unicode". Está establecido como obsoleto y se recomienda usar DataFlavor.getReaderForText(Transferable) en vez de Transferable.getTransferData(DataFlavor.plainTextFlavor).

Por ejemplo, cuando el método Transferable.getTransferData(DataFlavor.imageFlavor) es invocado, retorna una instancia de la clase java.awt.Image. Para crear nuevos tipos de contenidos MIME se utilizan los siguientes métodos y constructores:

• DataFlavor(String tipoMime) Construye un DataFlavor que representa un tipo MIME especificado. El string puede especificar un parámetro "class= " para crear una instancia con una representación de clase deseada. Si no se especifica la clase se usa por defecto "class= InputStream". Por ejemplo, para crear un contenido que pueda representar un objeto de tipo java.util.Date podemos usar:

DataFlavor dateFlavor = new DataFlavor("text/plain;class=java.util.Date");

La instancia creada tendrá las siguientes características: representationClass = java.util.Date

mimeType = text/plain;class=java.util.Date

• DataFlavor(Class claseJava, String nombrePresentable) Construye un DataFlavor que representa a una clase de Java. La instancia creada tiene las siguientes características:

representationClass = claseJava

mimeType = application/x-java-serialized-object

El argumento nombrePresentable es usado para identificar el tipo de contenido, si es null se usa el valor del tipo MIME.

• DataFlavor(String tipoMime, String nombrePresentable) Construye un DataFlavor que representa un tipo MIME especificado. La instancia creada tiene las siguientes características:

representationClass = java.io.InputStream

mimeType = tipoMime

• DataFlavor(String tipoMime, String nombrePresentable, ClassLoader cargadorClase) Construye un DataFlavor que representa un tipo MIME especificado. La instancia creada tiene las siguientes características:

representationClass = java.io.InputStream

mimeType = tipoMime

El argumento cargadorClase será usado para instanciar los objetos de datos soportados por este tipo de contenido. Por ejemplo, si queremos que se instancien objetos de tipo Date se puede usar su cargador Date.class.getClassLoader().

• static DataFlavor getTextPlainUnicodeFlavor() Retorna un DataFlavor que representa un texto plano con la codificación Unicode, donde:

representationClass = java.io.InputStream

mimeType = text/plain;charset=<codificación por defecto de la plataforma>

• static DataFlavor selectBestTextFlavor(DataFlavor[] disponibles) Selecciona el mejor DataFlavor para texto desde un array pasado por argumento. Sólo DataFlavor.stringFlavor, y sus equivalentes, y tipos MIME de tipo "text" son considerados para su selección.

Page 189: Fundamentos de JAVA

Fundamentos de Java /189

2.9.5. Ejemplo para copiar clases personalizadas al portapapeles. En el siguiente ejemplo se muestra cómo poder copiar al portapapeles objetos personalizados de Java y cómo se pueden recuperar posteriormente. El ejemplo crea una clase de datos denominada Cliente, de la cual se copiará una instancia en el portapapeles. /* La clase personalizada con dos campos. */

class Cliente {

private int id;

private String nombre;

// El constructor público

public Cliente(int id, String nombre) {

this.id = id;

this.nombre = nombre;

}

// Se reescribe este método para presentar los datos de un cliente

public String toString() {

return "(" + id + ") " + nombre;

}

}

/* Se crea un DataFlavor para establecer como tipo de contenido un objeto Cliente. */

final DataFlavor clienteFlavor = new DataFlavor(Cliente.class, "Cliente");

/* Se crea una clase Transferable para encapsular un cliente.

* Esta clase será la usada para copiar los datos al portapapeles */

class ClienteTransferable implements Transferable {

private Cliente cliente;

// El constructor público.

public ClienteTransferable(Cliente cliente) {

setCliente(cliente);

}

// Método accesor para poder encapsular un cliente

public void setCliente(Cliente cliente) {

this.cliente = cliente;

}

// Retorna los tipos de DataFlavor soportados. Sólo el nuestro.

public DataFlavor[] getTransferDataFlavors() {

return new DataFlavor[] {clienteFlavor};

}

// Indica el tipo de DataFlavor soportado. Sólo el nuestro.

public boolean isDataFlavorSupported(DataFlavor flavor) {

return flavor.getHumanPresentableName().equals("Cliente");

}

// Retorna el cliente encapsulado según el tipo de contenido indicado.

public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

if ( ! isDataFlavorSupported(flavor) )

throw new UnsupportedFlavorException(flavor);

return cliente;

}

}

/* El código para copiar un cliente al portapapeles */

Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();

Transferable miCliente = new ClienteTransferable(new Cliente(1, "Pedro");

clip.setContents(miCliente, null);

/* El código para recuparar el cliente del portapapeles e imprimirlo */

try {

Cliente cliente = (Cliente) clip.getData(clienteFlavor);

System.out.println( cliente );

} catch (UnsupportedFlavorException ex) {

} catch (IOException ex) {

}

Page 190: Fundamentos de JAVA

Fundamentos de Java /190

3. Librería gráfica Swing

Además del paquete java.awt, Java pone a disposición del programador el paquete javax.swing para crear unas interfaces gráficas. La librería Swing ha sido totalmente escrita en Java utilizando el paquete awt, y pone a disposición del usuario muchas clases que están también en awt, pero mucho mejor y más potentes.

3.1. Componentes Swing.

Swing introduce nuevos componentes y algunos cambios respecto a AWT. El conjunto de componentes de esta librería por lo general extienden a los componentes de AWT ampliando sus capacidades. Las ventanas (objetos JFrame) incluyen por defecto un objeto JComponent, que actúa de contenedor de los demás componentes incluidos en la ventana. Puede obtenerse dicho objeto con el método getComponent(). Los componentes incorporan nuevas propiedades:

- El método setToolTipText(), permite mostrar automáticamente una etiqueta de ayuda contextual con fondo amarillo. - Se incorporan nuevas clases de bordes para los componentes: TitledBorder, EtchedBorder, LineBorder, MatteBorder, BevelBorder, SoftBevelBorder y CompoundBorder. - Los componentes que usan etiquetas de texto ahora pueden incorporar una imagen. - Se puede asignar a los componentes aceleradores de teclado con el método setMnemonic(KeyEvent.VK_letra).

El siguiente diagrama muestra la jerarquía de componentes ubicados en el paquete javax.swing.

3.1.1. Etiquetas (clase «JLabel»). La clase javax.swing.JLabel crea etiquetas de texto y/o imágenes y es análoga a la clase java.awt.Label. El siguiente código crea una etiqueta con un texto inicial, y después recupera el contenido como un string: JLabel lbMensaje = new JLabel("<sin mensaje>");

. . .

String msgActual = lbMensaje.getLabel( );

3.1.2. Botón de pulsación (clases «JButton»). La clase javax.swing.JButton crea botones de pulsación normales y es análoga a la clase java.awt.Button.

Es un botón que puede contener texto, gráficos, o ambos. Centra el texto siempre; en caso de contener una imagen, ha de ir a la izquierda o encima del texto. Los métodos más importantes son:

• getText() y setText(String), para obtener y asignar el texto del botón. • getTooltipText() y setTooltipText(String), para obtener y asignar la etiqueta de ayuda. • getBackground() y setBackground(Color), para obtener y asignar el color de fondo del botón. • getForeground() y setForeground(Color), para obtener y asignar el color de primer plano del botón. • getIcon() y setIcon(ImageIcon), para obtener y asignar la imagen de fondo del botón • getFont() y setFont(Font), para obtener y asignar la fuente del texto. • getBounds() y setBounds(Rectangle), para obtener y asignar los bordes del botón.

El siguiente código crea un botón con un tamaño especificado, le asigna una etiqueta de ayuda y especifica que el botón se activará cuando se pulsen las teclas [Alt]+[B]:

Component

JLabel JPanel AbstractButton JList

JComponent

JToogleButton

JMenu JCheckBox JRadioButton

JButton

JScrollbar JTextComponent JSlider

JMenuItem

JDialog JFrame

JTextField JTextArea

JPasswordField

Page 191: Fundamentos de JAVA

Fundamentos de Java /191

JButton boton1 = new JButton("Botón de prueba");

boton1.setBounds(new Rectangle(107, 50, 102, 41));

boton1.setToolTipText("Ayuda del botón");

boton1.setMnemonic(KeyEvent.VK_B);

3.1.3. Botón de conmutación (clases «JToogleButton»). La clase javax.swing.JToggleButton crea botones con dos estados: pulsado y no pulsado. (Suelen usarse en conjunción con otros botones de forma que sólo uno de ellos permanezca pulsado.)

Tiene los mismos métodos que JButton, y además añade algunos nuevos: • isSelected(), para obtener el estado del botón: true pulsado, y false no pulsado. • setSelected(boolean); para establecer el estado del botón.

Es posible añadir botones a grupos para garantizar opciones mutuamente exclusivas. 3.1.4. Casilla de verificación (clase «JCheckBox»). La clase javax.swing.JCheckBox crea casillas de verificación y es asimilable a la clase java.awt.Checkbox.

Tiene las mismas características y métodos que JtoogleButton. 3.1.5. Botón de radio (clase «JRadioButton»). La clase javax.swing.JRadioButton crea botones de radio y es asimilable a la clase java.awt.Checkbox cuando se agrupan varios botones.

Permiten seleccionar una única opción dentro de un conjunto de opciones relacionadas, de forma que sólo puede haber una opción seleccionada a la vez. Aunque tiene la misma función que los botones de conmutación agrupados, conviene usar los botones de radio en cuadros de diálogos, y los de conmutación en barras de herramientas. Tiene métodos similares a los de JCheckBox. 3.1.6. Grupo de botones (clase «ButtonGroup»). La clase javax.swing.ButtonGroup permite crear grupos de cualquiera de las cuatro clases de botones. Se usa su método add() para añadir un botón al grupo. El siguiente código muestra como agrupar dos botones de radio: . . .

JRadioButton r1 = new JRadioButton ("Opción 1");

JRadioButton r2 = new JRadioButton ("Opción 2");

ButtonGroup grupo = new ButtonGroup();

grupo.add(r1);

grupo.add(r2);

. . .

3.1.7. Lista (clase «JList»). La clase javax.swing.JList permite crear una lista y es asimilable a la clase java.awt.List. Sin embargo, ahora los elementos pueden ser de cualquier clase y no sólo strings. Permite tres tipos de selección:

• Un único elemento. Se establece mediante el método: setSelectionMode(ListSelectionModel.SINGLE_SELECTION).

• Un rango simple, consiste en seleccionar varios elementos consecutivos. Se establece mediante el método: setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION).

• Un rango múltiple, consiste en seleccionar varios elementos no consecutivos. Se establece mediante el método:

setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION).

Se pueden proporcionar elementos a la lista pasando un array de strings en su constructor. Por ejemplo: String[] contenidos={"elemento1", "elemento2","elemento3","elemento4","elemento5"};

Page 192: Fundamentos de JAVA

Fundamentos de Java /192

JList=new JList(contenidos);

Con esta forma no es posible añadir ni eliminar elementos de la lista. Otra forma de proporcionar elementos a la lista es mediante un objeto que implemente la interfaz javax.swing.ListModel, que incluye los siguientes métodos:

• int getSize() debe retornar el número de elementos de la lista. • Object getElementAt(int index) debe retornar el elemento de índice dado. • void addListDataListener(ListDataListener l) debe añadir a un oyente de la lista. • void removeListDataListener(ListDataListener l) debe eliminar a un oyente de la lista.

Java define la clase javax.swing.DefaultListModel, que implementa esta interfaz a través de su clase base javax.swing.bstractDefaultListModel, y permite crear listas de objetos con capacidad para añadir y eliminar elementos. La lista mostrará el string que retorne el método toString() de cada elemento. El siguiente trozo de código muestra cómo crear una lista de strings: JList lista = new JList(); // se crea el componente visual

DefaultListModel lm = new DefaultListModel(); // se crea el modelo origen de los elementos

lm.addElement("uno");

lm.addElement("dos");

lista.setModel(lm); // se asigna el modelo al componente visual

lm.removeIndex(0); // elimina el primer elemento. La lista se renderiza

. . .

String elemento = (String) lista.getSelectedValue(); // recupera el texto del elemento seleccionado

3.1.8. Caja combinada (clase «JComboBox»). La clase javax.swing.JComboBox permite crear cajas combinadas y es asimilable a la clase java.awt.Choice.

Este componente nos permite, al hacer clic sobre él, seleccionar una opción de entre un conjunto, todas ellas mutuamente exclusivas. Se pueden generalizar de dos formas: editables y no editables. Los editables permiten escribir en el cuadro de texto, y los no editables sólo permiten seleccionar un elemento de su lista. De forma análoga a la clase JList, el origen de los elementos seleccionable se estable a través de un objeto que implementa la interfaz javax.swing.ComboBoxModel, y también existe una clase javax.swing.DefaultComboBoxModel que la implementa. 3.1.9. Campo de texto (clase «JTextField»). La clase javax.swing.JTextField crea un editor de una línea de texto y es análoga a la clase java.awt.TextField.

El código siguiente crea un cuadro de texto y posteriormente recupera su contenido: JTextField txtCampo = new JTextField();

. . .

String contenido = txtCampo.getText();

3.1.10. Área de texto (clase «JTextArea»). La clase javax.swing.JTextArea crea un editor de varias líneas de texto y es análoga a la clase java.awt.TextArea.

Su funcionalidad es idéntica a la de JTextField.

Page 193: Fundamentos de JAVA

Fundamentos de Java /193

3.1.11. Campo de contraseña (clase «JPasswordField»). La clase javax.swing.JPasswordField crea un editor para escribir claves ocultas. Es similar a JTextField, excepto en que muestra su contenido con un caracter de máscara. Los métodos getEchoChar() y setEchoChar() permite establecer el caracter de máscara. 3.1.12. Panel de texto (clase «JTextPane»). La clase javax.swing.JTexPane crea un editor para texto con tipos de fuentes y estilos diversos. Incorpora métodos para recuperar el contenido de un documento y aplicar diversos estilos al texto.

Un texto con varios formatos.

Algunos de sus métodos son: • void setStyledDocument(StyledDocument doc), asocia el editor con un documento de texto con estilos. La interfaz javax.swing.text.StyledDocument define un documento de texto con formato enriquecido. La clase javax.swing.text.DefaultStyledDocument implementa esta interfaz. • StyledDocument getStyledDocument(), recupera el modelo asociado con el editor. • void replaceSelection(String content), reemplaza la selección actual en el editor con el argumento, o bien si no hay selección, en el punto de inserción. El nuevo texto heredará los estilos actuales. • void insertIcon(Icon g), inserta un icono en el punto de inserción actual. • Style addStyle(String nm, Style parent), añade un nuevo estilo al editor. La clase javax.swing.text.Style es una colección de atributos para asociar con un elemento del documento. • void removeStyle(String nm), elimina un estilo por nombre. • Style getStyle(String nm), recupera un estilo añadido por nombre.

3.1.13. Campo de texto con formato (clase «JFormattedText»). La clase javax.swing.JFormattedTextField crea un editor con máscara, de forma que el texto introducido debe corresponderse con el formato de la máscara. Si no es así, el editor recupera el último texto válido que se introdujo o un valor vacío. El formato de máscara se establece a través de objetos JFormattedTextField.AbstractFormatterFactory, que es una clase abstracta. Se puede usar su clase derivada javax.swing.text.DefaultFormatterFactory, en cuyo constructor se puede pasar un objeto JFormattedTextField.AbstractFormatter, que determina el formato de máscara. Existen clases predefinidas que extienden a JFormattedTextField.AbstractFormatter para diversos tipos de máscaras. Algunas de estas clases están en el paquete javax.swing.text y son:

DateFormatter Aplica máscaras sobre formatos de fecha NumberFormatter Aplica máscaras sobre formatos numéricos MaskFormatter Se usa para dar formato y editar strings. Usa los siguientes caracteres para especificar el

formato de un string: # cualquier número válido ' caracter de escape U cualquier letra, haciendo una conversión siempre a mayúsculas L cualquier letra, haciendo una conversión siempre a minúsculas A cualquier letra o número ? cualquier letra * cualquier caracter H cualquier caracter hexadecimal (0-9, a-f o A-F)

Permite restringir los caracteres no válidos o válidos mediante los métodos setInvalidCharacters() y setValidCharacters(). Por ejemplo, las siguientes instrucciones serían equivalentes: MaskFormatter formatter = new MaskFormatter("0x***");

formatter.setValidCharacters("0123456789abcdefABCDEF");

En el siguiente código de ejemplo se aplican diversas máscaras sobre un control JFormattedTextField: JFormattedTextField ffText = new JFormattedTextField();

// Se aplica un formato de fecha corta del estilo "12-12-2009"

ffText.setFormatterFactory(

Page 194: Fundamentos de JAVA

Fundamentos de Java /194

new DefaultFormatterFactory(

new DateFormatter(java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT))));

// Se aplica un formato de moneda con dos decimales

ffText.setFormatterFactory(new DefaultFormatterFactory(

new NumberFormatter(new java.text.DecimalFormat("#.00"))) );

// Se aplica un formato de nif del estilo "11111111-A", de forma que la letra siempre se escriba en mayúscula

try {

ffText.setFormatterFactory(new DefaultFormatterFactory(new MaskFormatter("########'-U")));

} catch (ParseException ex) {

// excepción generada si el formato de máscara es incorrecto

}

Una vez establecida la máscara, podemos utilizar el método void setValue(Object) para pasar un valor al cuadro de texto. El valor pasado debe ser de un tipo compatible para el formateador correspondiente. Para recuperar el valor actual de cuadro podemos utilizar el método Object getValue(). Este método retorna un objeto del tipo especificado por el formateador con el contenido actual válido del editor, el cual no tiene porqué corresponderse con lo escrito en el editor. Esto es así porque el editor no valida por defecto lo escrito en el editor hasta que éste pierde el foco. Podemos forzar esta validación con el método void commitEdit(). La clase del objeto retornado por getValue() depende del formateador. Si la máscara es para números se puede moldear a la clase java.lang.Number y después recuperar el valor como un tipo primitivo mediante los métodos de esta clase. Si la máscara es para fechas se puede moldear al tipo java.util.Date. También podemos establecer cómo se realiza la validación mediante el método void setFocusLostBehavior(int). Este método admite las siguientes constantes como argumento: JFormattedTextField.COMMIT_OR_REVERT (valida o recupera el último valor válido), JFormattedTextField.REVERT (recupera el último valor válido), JFormattedTextField.COMMIT (acepta cualquier valor escrito y puede provocar una excepción) y JFormattedTextField.PERSIST (no aplica formateo). 3.1.14. Escala (clase «JSlider») y barras de progreso (clase «JProgressBar»). La clase javax.swing.JProgressBar crea una barra de progreso. La barra se rellena con un color que avanza desde un valor mínimo a un valor máximo.

Los métodos significativos de esta clase son: • int getValue() y void setValue(int), establecen el valor actual de la barra. • int getMinimum() y void setMinimum(int), establecen el valor mínimo de la barra. • int getMaximum() y void setMaximum(int), establecen el valor máximo de la barra. • int getOrientation() y void setOrientation(int), establecen la orientación. Los valores admitidos son SwingConstants.VERTICAL o SwingConstants.HORIZONTAL. • boolean isStringPainted() y void setStringPainted(boolean), establecen si debe mostrarse una etiqueta con el valor de progreso de la barra. • String getString() y void setString(String s), establecen el símbolo de la etiqueta de la barra de progreso. Por defecto, si el string es nulo, se usa el símbolo de porcentaje. • double getPercentComplete(), retorna el porcentaje que se ha completa en al barra. El valor retornado es un número entre 0.0 y 1.0.

3.1.15. Escala (clase «JSlider»). La clase javax.swing.JSlider crea una escala, una barra con un marcador que se desplaza a su largo. La posición del marcador indica el valor actual de la escala, que debe restringirse a un valor mínimo y máximo.

Los métodos significativos de esta clase son: • int getValue() y void setValue(int), establecen el valor actual de la escala. • int getMinimum() y void setMinimum(int), establecen el valor mínimo de la escala. • int getMaximum() y void setMaximum(int), establecen el valor máximo de la escala. • int getOrientation() y void setOrientation(int), establecen la orientación. Los valores admitidos son SwingConstants.VERTICAL o SwingConstants.HORIZONTAL.

Page 195: Fundamentos de JAVA

Fundamentos de Java /195

• Dictionary getLabelTable() y void setLabelTable(Dictionary labels), establecen un mapa de etiquetas asociadas a los valores de la escala. Una manera sencilla de crear un argumento para el método setter es usar createStandardLabels(). • Hashtable createStandardLabels(int increment), crea un mapa de etiquetas de texto numéricas comenzando por el valor mínimo y usando un incremento dado.

3.1.16. Menús. Se han mejorado, introduciendo más posibilidades en las opciones de menú.

La clase javax.swing.JMenu es la implementación de un menú, el cual está formado por elementos de menú (objetos JmenuItem) y separadores (objetos Jseparator). En esencia, un menú es un botón que tiene asociado un menú emergente (objeto JPopupMenu). Cuando se pulsa el "botón" el menú emergente aparece. Al igual que con los botones de pulsación, el menú puede registrar un receptor del evento de pulsación (un ActionEvent). 3.1.17. Panel (clase «JPanel»). La clase javax.swing.JPanel es un contenedor que agrupa componentes dentro de una ventana. Los layouts permiten un correcto posicionamiento de los componentes hijos. En el siguiente código se crea un panel con el layout de borde y se añaden una etiqueta y un cuadro de texto al mismo: JPanel panel = new JPanel();

panel.setLayout(new java.awt.BorderLayout());

panel.add(new JLabel("dni"), java.awt.BorderLayout.WEST);

panel.add(new JTextField(), java.awt.BorderLayout.CENTER);

3.1.18. Árboles (clase «JTree»). La clase javax.swing.JTree crea un árbol, un componente que permite organizar una jerarquía de elementos subordinados.

Debido a su complejidad de diseño, se proporcionan modelos de árboles que deben implementar la interfaz javax.swing.tree.TreeModel. La clase javax.swing.tree.DefaultTreeModel permite crear un modelo por defecto. El siguiente código crea el árbol de la imagen precedente: import javax.swing.*;

import javax.swing.tree.*;

. . .

arbol = new JTree();

DefaultMutableTreeNode treeNode1 = new DefaultMutableTreeNode("Departamentos");

DefaultMutableTreeNode treeNode2 = new DefaultMutableTreeNode("Finanzas");

DefaultMutableTreeNode treeNode3 = new DefaultMutableTreeNode("Madrid");

treeNode2.add(treeNode3);

treeNode1.add(treeNode2);

treeNode2 = DefaultMutableTreeNode("Ventas");

treeNode3 = new DefaultMutableTreeNode("Madrid");

treeNode2.add(treeNode3);

treeNode3 = new DefaultMutableTreeNode("Barcelona");

treeNode2.add(treeNode3);

treeNode1.add(treeNode2);

arbol.setModel(new DefaultTreeModel(treeNode1));

Page 196: Fundamentos de JAVA

Fundamentos de Java /196

Si deseamos conocer cuándo ha cambiado la selección debemos implementar la interfaz TreeSelectionListener y añadir una instancia usando el método addTreeSelectionListener(). El método de esta interfaz, valueChanged(), será invocado cuando la selección cambie. Por ejemplo: arbol.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {

public void valueChanged(javax.swing.event.TreeSelectionEvent evt) {

TreePath t1 = evt.getNewLeadSelectionPath(); // obtiene la rama seleccionada

TreePath t2 = evt.getOldLeadSelectionPath(); // obtiene la anterior rama seleccionada

});

Si estamos interesados en detectar cuando se hace un doble clic o cuando el usuario hace clic sobre un nodo, independientemente de la selección, se recomienda el siguiente código: class ArbolMouseListener extends MouseAdapter {

private JTree arb;

public ArbolMouseListener(JTree arb) {

this.arb = arb;

}

public void mousePressed(MouseEvent e) {

int selRow = arb.getRowForLocation(e.getX(), e.getY());

TreePath selPath = arb.getPathForLocation(e.getX(), e.getY());

if(selRow != -1) {

if(e.getClickCount() == 1) {

// se ha realizado un clic

} else if(e.getClickCount() == 2) {

// se ha realizado un doble clic

}

}

}

arbol.addMouseListener(new ArbolMouseListener(arbol));

3.1.19. Tablas (clase «JTable»). La clase javax.swing.JTable crea una tabla o rejilla. Normalmente están ligadas a bases de datos, pero se pueden usar independientemente.

Para crear una tabla hay que pasar en su constructor los datos como un array de dos dimensiones. Por ejemplo: Jtable tblNumeros = new JTable(new Object [][] {{1, "Uno"}, {2, "Dos"}}, new String [] {"Cardinal", "Ordinal"});

Como primer argumento del constructor se pasa una matriz con los valores de cada fila, y como segundo argumento una array con los nombres de las columnas. Con esta forma no es posible añadir ni eliminar elementos de la tabla. Otra forma de proporcionar el contenido es mediante un objeto que implemente javax.swing.TableModel, el cual va a controlar los datos que se introducen en la tabla. También se puede derivar el objeto de la clase javax.swing.AbstractTableModel. Los métodos de esta clase abstracta son:

public int getColumnCount(), debe retornar el número de columnas. puclic int getRowCount(), debe retornar el número de filas. public Object getValueAt(int fila, int col), debe retornar el contenido de la celda dada. public void setValueAt(Object valor, int fila, int col), debe asignar el valor de la celda dada. public boolean isCellEditable(int fila, int col), debe indicar si una celda dada es editable.

La clase javax.swing.table.DefaultTableModel extiende a AbstractTableModel para implementar un modelo basado en una matriz de objetos presentados en las celdas como strings. 3.1.20. Control de incremento (clase «JSpinner»). La clase javax.swing.JSpinner crea un control para incrementar o decrementar un valor.

Page 197: Fundamentos de JAVA

Fundamentos de Java /197

El comportamiento del control viene determinado por un objeto que implemente javax.swing.SpinnerModel. Por defecto, el control usa un objeto javax.swing.SpinnerNumberModel para valores numéricos. Otras clases que se pueden usar como modelo son javax.swing.SpinnerDateModel para fechas y javax.swing.SpinnerListModel para tomar los valores de una lista.

Los métodos más significativos de esta clase son: • Object getValue() y void setValue(Object), establecen el valor actual. • Object getNextValue(), retorna el siguiente valor al actual en la secuencia. • Object getPreviousValue(), retorna el anterior valor al actual en la secuencia. • void setModel(SpinnerModel model), cambia el modelo que proporciona los valores del control.

3.1.21. Separadores (clase «JSeparator»). La clase javax.swing.JSparator crea una línea separadora HORIZONTAL o VERTICAL. (Normalmente se usan para separar opciones de menús.) 3.1.22. Divisores (clase «JSplitPane»). La clase javax.swing.JSplitPane se usa para dividir dos componentes. Ambos componentes son divididos gráficamente y pueden ser redimensionados interactivamente. 3.1.23. Fichas con pestañas (clase «TabbedPane»). La clase javax.swing.JTabbedPane crea un grupo de fichas superpuestas con pestañas asociadas (con AWT, para hacer esto, había que usar el layout manager CardLayout).

Una ficha puede incluir cualquier componente. El método addTab() permite añadir una etiqueta para la pestaña y un componente: JtabbedPane fichas = new JTabbedPane();

fichas.addTab("tab1", new JPanel());

fichas.addTab("tab2", new JPanel());

Se usa setSelectedIndex() para mostrar la ficha de índice dado: Fichas.setSelectedIndex(0); // muestra la primera ficha

3.1.24. Selección de archivos (clase «JFileChooser»). La clase javax.swing.JFileChooser crea un cuadro de diálogo modal que permite seleccionar un archivo. Es similar a la clase java.awt.FileDialog. Si queremos abrirlo para leer el fichero, podemos llamarlo así: JFileChooser fileChooser = new JFileChooser();

int seleccion = fileChooser.showOpenDialog(null);

La ejecución del código se para en el método showOpenDialog() hasta que se cierra el cuadro de diálogo. A la vuelta, en seleccion tendremos asignado:

JFileChooser.CANCEL_OPTION Si el usuario le ha dado al botón cancelar. JFileChooser.APPROVE_OPTION Si el usuario le ha dado al botón aceptar JFileCHooser.ERROR_OPTION Si ha ocurrido algún error.

Comprobando que se ha dado al botón aceptar, podemos obtener el fichero seleccionado por el usuario así: if (seleccion == JFileChooser.APROVE_OPTION) {

File fichero = fileChooser.getSelectedFile();

// Aquí debemos abrir y leer el fichero.

...

}

Para seleccionar un fichero para guardar datos, el mecanismo es análogo, pero se llama al método showSaveDialog(): JFileChooser fileChooser = new JFileChooser();

int seleccion = fileChooser.showSaveDialog(null);

if (seleccion == JFileChooser.APPROVE_OPTION) {

File fichero = fileChooser.getSelectedFile();

// Aquí debemos abrir el fichero para escritura y salvar nuestros datos.

...

}

Page 198: Fundamentos de JAVA

Fundamentos de Java /198

Si no queremos que el JFileChooser muestre todos los ficheros del directorio, podemos añadirle un filtro. Básicamente hay que hacer una clase que herede de FileFilter e implementar el método accept(). Este método recibe un parámetro File y nosotros debemos decidir si pasa o no el filtro, devolviendo true o false. Por ejemplo, si sólo queremos ver fichero .jpg, podemos hacer este filtro: import javax.swing.filechooser.FileFilter;

public class FiltroDeJPG extends FileFilter {

public boolean accept (File fichero) {

return (tieneExtensionJPG (fichero))? true : false;

}

public String getDescription() {

return ("Filtro JPGs");

}

}

Debemos definir ambos métodos. La descripción puede ser cualquier cadena de texto que nos sirva como descripción del filtro. Finalmente, debemos pasar este filtro al JFileChooser: fileChooser.setFilter(new FiltroDeJPG());

Sin embargo, una de las bondades ofrecidas por el JDK 1.6, es el ahorro en la codificación de una clase filtro, pues éste ya viene con una incluida a la cual solo necesitamos invocarla, la sintaxis es la siguiente: JFileChooser jf = new JFileChooser();

FileNameExtensionFilter filter = new FileNameExtensionFilter("JPG & GIF", "jpg", "gif");

jf.setFileFilter(filter);

Donde el primer término es la descripción y los siguientes serían los tipos de archivos aceptados. 3.1.25. Selección de color (clase «JColorChooser»). La clase javax.swing.JColorChooser crea un cuadro de diálogo modal que permite seleccionar un color.

Provee el método estático Color showDialog (Component padre, String titulo, Color c)

que establece la ventana padres, un título para el cuadro de diálogo y un color inicial seleccionado. Se abre el cuadro de diálogo y cuando se cierra, el método retorna el color seleccionado o null. 3.1.26. Paneles desplazables (clase «JScrollPane»). La clase javax.swing.JScrollPane crea paneles con barras de desplazamiento laterales. Es similar a la clase java.awt.ScrollPane.

3.2. Los gestores de distribución de Swing.

La librería Swing incorpora dos nuevos tipos de layouts: BoxLayout y OverLayout. 3.2.1. Diseño de caja (clase «BoxLayout»). El controlador de posicionamiento javax.swing.BoxLayout permite colocar los componentes a lo largo del eje X o del eje Y, y también posibilita que los componentes ocupen diferente espacio a lo largo del eje principal. En un controlador BoxLayout sobre el eje Y, los componentes se posicionan de arriba hacia abajo en el orden en que se han añadido. Al contrario que en el caso del GridLayout, aquí se permite que los componentes sean de diferente tamaño a lo largo del eje Y, que es el eje principal del controlador de posicionamiento, en este caso. En el eje que no es principal, BoxLayout intenta que todos los componentes sean tan anchos como el más ancho, o tan alto como el más alto, dependiendo de cuál sea el eje principal. Si un componente no puede

Page 199: Fundamentos de JAVA

Fundamentos de Java /199

incrementar su tamaño, el BoxLayout mira las propiedades de alineamiento en X e Y para determinar dónde colocarlo. 3.2.2. Diseño nulo (clase «OverlayLayout»). El controlador de posicionamiento javax.swing.OverlayLayout se dimensiona para contener el más grande de los componentes y superpone cada componente sobre los otros. La clase OverlayLayout no tiene un constructor por defecto, así que hay que crearlo dinámicamente en tiempo de ejecución.

3.3. Cuadros de diálogo predefinidos.

La clase javax.swing.JOptionPane ofrece métodos estáticos para abrir ventanas de mensaje y confirmación. • void showMessageDialog (Component padre, Objet mensaje); • void showMessageDialog (Component padre, Objet mensaje, String titulo, int tipo, Icon icono);

Abren una ventana con el mensaje dado. En tipo pueden usarse las constantes INFORMATION_MESSAGE, ERROR_MESSAGE, PLAIN_MESSAGE, QUESTION_MESSAGE y WARNING_MESSAGE.

• int showConfirmDialog (Component padre, Object mensaje); • int showConfirmDialog (Component padre, Object mensaje, String titulo, int opciones, int tipo, Icon icono);

Abre una ventana de confirmación con botones Sí/No/Cancel. En opciones podemos usar las constantes YES_NO_OPTION, YES_NO_CANCEL_OPTION y OK_CANCEL_OPTION. El valor de retorno indica el botón pulsado (se usan las constantes CANCEL_OPTION, YES_OPTION, NO_OPTION, o OK_OPTION).

• String showInputDialog (Component padre, Object mensaje); • String showInputDialog (Component padre, Object mensaje, String titulo, int tipo, Icon icono, Object[]

valoresPosibles, Object valorInicial); Abre una ventana de mensaje con un cuadro de edición o una combobox con valores dados. Retorna el texto del editor o null si se canceló.

3.4. Enlace de propiedades de componentes.

Java proporciona varias clases (entre ellas Binding) para establecer un enlace entre dos propiedades de un componente, de forma que cuando la propiedad origen se modifique la propiedad destino tome automáticamente el mismo valor. 3.4.1. Clases para enlazar. La clase org.jdesktop.beansbinding.Binding es una clase abstracta que representa el concepto de enlace entre dos propiedades, normalmente de dos objetos, y contiene métodos para sincronizar explícitamente los valores de ambas propiedades. La clase org.jdesktop.beansbinding.AutoBinding es una implementación concreta de Binding que sincroniza automáticamente el origen y destino refrescándolos y guardándolos de acuerdo a una de las tres siguientes estrategias (la cual se especifica en el constructor de la clase):

• AutoBinding.UpdateStrategy.READ_ONCE Realiza la sincronización del destino desde el origen sólo en el momento del enlace. No aplica sincronizaciones automáticas.

• AutoBinding.UpdateStrategy.READ Realiza sincronizaciones automáticas del destino desde el origen cada vez que éste es modificado.

• AutoBinding.UpdateStrategy.READ_WRITE Realiza la sincronización automática del destino desde el origen y viceversa.

La clase org.jdesktop.beansbinding.Bindings es una clase fabricadora que crea instancias de implementaciones concretas de Binding. Aporta el siguiente método estático sobrecargado:

• AutoBinding<SS,SS,TS,TV> createAutoBinding(AutoBinding.UpdateStrategy estrategia, SS objOrigen, TS

ojbDestino, Property<TS,TV> porpiedadDestino) throws java.lang.IllegalArgumentException Crea y retorna una instancia de AutoBinding que enlaza un objeto origen a una propiedad de un objeto destino.

La clase org. jdesktop.beansbinding.BindingGroup permite crear grupos de enlaces (objetos Binding) y operar y/o establecer los cambios de estado de los enlaces como un grupo. 3.4.2. Enlace de propiedades del mismo tipo. Supongamos que añadimos un componente JSlider y un componente JProgressBar a una ventana, y deseamos que la barra de progreso se desplace automáticamente para tomar el mismo valor de desplazamiento de la barra deslizante. El desplazamiento de ambas barras queda establecido por sus respectivas propiedades value.

Page 200: Fundamentos de JAVA

Fundamentos de Java /200

Si enlazamos la propiedad value de ambas barras, tomando la barra deslizadora como origen y la barra de progreso como destino conseguiremos el objetivo deseado.

El código debe ser parecido al siguiente: import org.jdesktop.beansbinding.*;

import javax.swing.*;

import org.jdesktop.beansbinding.ELProperty;

import org.jdesktop.beansbinding.BeanProperty;

. . .

// Se instancian las dos barras

JSlider jSlider1 = new JSlider();

JProgressBar jProgressBar1 = new JProgressBar();

// Se crea un grupo de enlaces

BindingGroup bindingGroup = new BindingGroup();

// Se enlaza los valores de las dos barras. Para ello se instancia un AutoBinding

Binding binding = Bindings.createAutoBinding(

AutoBinding.UpdateStrategy.READ,

jSlider1, ELProperty.create("${value}"),

jProgressBar1, BeanProperty.create("value"));

// Se añade el enlace al grupo

bindingGroup.addBinding(binding);

// Se invocan todos los enlazadores

bindingGroup.bind();

En el código anterior: - Para establecer el origen se crea un objeto del tipo ELProperty, el cual permite referenciar (mediante una expresión EL) una propiedad de algún objeto Java bean. - Para establecer el destino se crea un objeto del tipo BeanProperty, el cual representa una propiedad de algún objeto Java bean.

3.4.3. Enlace de propiedades de tipos distintos. Supongamos ahora que añadimos un componente JSlider y un componente JTextBox a una ventana, y deseamos que el cuadro de texto refleje automáticamente el valor de desplazamiento de la barra deslizante. Si enlazamos la propiedad value de la barra (como origen) con la propiedad text del cuadro de texto (como destino) conseguiremos el objetivo deseado.

El problema se plantea al considerar que la propiedad value es de tipo int mientras que la propiedad text es de tipo String. Estos dos tipos de datos no son moldeables implícitamente y por tanto hay que realizar una conversión explícita. Para ello debemos pasar al enlazador un objeto de tipo org.jdesktop.beansbinding.Converter que especifique cómo realizar la conversión entre ambos tipos de datos. Converter es una clase abstracta y por tanto deberemos extenderla. Por ejemplo: class ConversorIntString extends Converter<Integer,String> {

public String convertForward(Integer value) {

return value.toString();

}

public Integer convertReverse(String value) {

try {

Page 201: Fundamentos de JAVA

Fundamentos de Java /201

return Integer.parseInt(value);

} catch (Exception ex) {

return 0;

}

}

}

Mediante el método setConverter() de la clase Binding podemos establecer la conversión de los datos en el enlazador: . . .

Binding binding = Bindings.createAutoBinding(

AutoBinding.UpdateStrategy.READ,

jSlider1, ELProperty.create("${value}"),

jTextField1, BeanProperty.create("text"));

binding.setConverter(new ConversorIntString());

. . .

3.5. Apariencia de las aplicaciones.

Cada ejecutable Java posee un objeto javax.swing.UIManager que determina la apariencia en pantalla y funcionamiento que van a tener sus componente Swing. Por defecto, Swing utiliza una apariencia denominada Metal. Podemos cambiar la apariencia de los componente con el método setLookAndFeel() del objeto UIManager.

3.6. Pintando en Swing.

Swing sigue el modelo de pintado de AWT y lo extiende con el uso de la técnica de doble-buffer y nuevas propiedades. La técnica del doble-buffer consiste en no dibujar directamente en la pantalla, sino sobre una imagen en memoria, que se vuelca sobre pantalla cada vez que haya que repintar los componentes. Los componentes swing disponen de los métodos: boolean isDoubleBuffered()

void setDoubleBuffered(boolena o)

para determinar el uso de esta técnica. Como Swing usa transparencias al repintar componentes, para mejorar el rendimiento se añade una propiedad de opacidad, que se controla con los métodos: boolean isOpaque()

void setOpaque(boolen o)

En Swing se invoca el método repaint() o paint() cuando es necesario repintar un componente, el cual a su vez invoca a los métodos paintComponent(), paintBorder() y paintChildren(). (Al contrario que en AWT, el método update() no se invoca jamás.) En Swing, se debe rescribir el método paintComponent() en vez de paint() para cambiar el contenido de los componentes. Como los repintados no son instantáneos, debemos invocar el método paintInmediately() para que el repintado se realice en tiempo real.

4. JavaBeans

Como se ha visto, se puede crear una interfaz de usuario en un programa Java en base a componentes: paneles, botones, etiquetas, caja de listas, barras de desplazamiento, diálogos, menús, etc. Existen componentes que van desde los más simples como un botón hasta otros mucho más complejos como un calendario, una hoja de Los primeros componentes que tuvieron gran éxito fueron los VBX (Visual Basic Extension), seguidos a continuación por los componentes OCX (OLE Custom Controls). En la tecnología Java se incorporan los JavaBeans, que son independientes de la plataforma.

4.1. ¿Qué es un bean?

4.1.1. Introducción. Un JavaBean o bean es un componente de software que se puede reutilizar y que puede ser manipulado visualmente por una herramienta de programación en lenguaje Java.

Page 202: Fundamentos de JAVA

Fundamentos de Java /202

Para crear un bean se debe definir un interfaz para el momento del diseño (design time) que permita a las herramientas de programación o IDE’s interrogar al componente y conocer las propiedades que define y los tipos de eventos que puede generar en respuesta a diversas acciones. Aunque los beans individuales pueden variar ampliamente en funcionalidad desde los más simples a los más complejos, todos ellos comparten las siguientes características:

• Introspección: Permite que las herramientas de programación o IDE analicen cómo funciona el bean. • Personalización: El programador puede alterar la apariencia y la conducta del bean. • Eventos: Informan al IDE de los sucesos que puede generar el bean en respuesta a las acciones del usuario o del sistema, y también los sucesos que puede manejar. • Propiedades: Permiten cambiar los valores de las propiedades del bean para personalizarlo. • Persistencia: Se puede guardar el estado de los beans que han sido personalizados por el programador, cambiando los valores de sus propiedades.

4.1.2. Patrones de diseño de beans. En general, un bean es una clase que obedece ciertas reglas:

• Un bean tiene que tener un constructor por defecto (sin argumentos). • Un bean tiene que tener persistencia, es decir, implementar la interfaz Serializable. • Un bean tiene que tener introspección. Los IDE’s reconocen ciertas pautas de diseño, nombres de las funciones miembros o métodos y definiciones de las clases, que permiten a las herramientas de programación mirar dentro del bean y conocer sus propiedades y su conducta.

Normalmente el proceso de creación de un bean será el siguiente: 1) Crear una clase que implemente la interfaz java.io.Serializable (por ejemplo, MiClase.class), permitir que tenga un constructor sin argumentos y compilar el archivo. 2) Crear un archivo de manifiesto con el nombre “manifest.tmp” que contenga las siguientes líneas:

Name: MiClase.class

Java-Bean: True

3) Crear un fichero JAR con el mismo nombre de la clase que contenga a “MiClase.class” y “manifest.temp”. 4.1.3. Ejemplo de bean. En este apartado desarrollaremos un componente bean que permita introducir el nombre de un cliente a través de una interfaz gráfica de usuario. Para ello, previamente, se debe diseñar una clase de entidad para encapsular los datos de un cliente. package cliente;

public class Cliente implements Serializable, Cloneable {

private String nif;

// Constructores

public Cliente () {

this(null);

}

public Cliente (String nombre) {

this.nombre = nombre;

}

// Métodos accesores

public String getNombre() {

return nombre;

}

public void setNombre(String nombre) {

this.nombre = nombre;

}

// Método clonador

public Cliente clone() { // retorna una copia de la instancia

try {

return (Cliente) super.clone();

} catch (CloneNotSupportedException ex) {

return null;

}

}

}

Page 203: Fundamentos de JAVA

Fundamentos de Java /203

La clase Cliente cumple con las reglas de diseño de los beans y por tanto podría utilizarse como un bean. Pero el interés de este ejemplo es crear un nuevo componente visual que nos permita gestionar un objeto Cliente a través de la interfaz de usuario. Para ello crearemos la clase ClienteBean, que tendrá el siguiente aspecto visual:

La clase ClienteBean encapsulará toda la lógica para gestionar visualmente la introducción y visualización de los datos de un cliente. Para crear este bean extenderemos la clase Panel, en cuya superficie añadiremos un editor de texto (para el nombre) con su correspondiente etiqueta. Si aplicamos la encapsulación al máximo, el bean se comunicará con el exterior a través de los métodos accesores getCliente() y setCliente(), que permitirán respectivamente retornar un objeto Cliente con los datos introducidos en el componente de edición, y mostrar los datos de un cliente pasado por argumento. package cliente;

import java.awt.*;

public class ClienteBean extends Panel implements java.io.Serializable {

private TextField tfNombre; // editor de texto para introducir y mostrar el nombre

/* Constructor sin argumentos *******************************************************/

public ClienteBean() {

this.setLayout(new GridLayout(2, 2, 1, 1)); // se aplica un diseño de rejilla de 1x2

this.add(new Label("Nombre ")); // se añade una etiqueta

tfNombre = new TextField(); // se instancia el editor para el nombre

this.add(tfNombre); // ... y se añade al panel

}

/* Método interno para rellenar los controles con los datos de un objeto Cliente. Si el argumento

* es nulo los controles se dejarán en blanco. *******************************************/

private void asignaDatos(Cliente cliente) {

String tNombre = null;

if (cliente != null) {

tNombre = cliente.getNombre();

}

tfNombre.setText(tNombre==null? "" : tNombre);

}

/* Se reescribe este método de la clase Panel para activar o desactivar los controles internos ***/

public void setEnabled(boolean b) {

super.setEnabled(b); // se invoca el método de la superclase para activar o desactivar el panel

tfNombre.setEnabled(b);

}

/* Este método deja en blanco todos los controles de edición ********************************/

public void resetea() {

asignaDatos(null);

}

/* Retorna un objeto Cliente a partir del contenido de los controles internos ******************/

public Cliente getCliente() {

return new Cliente(tfNombre.getText() );

}

/* Asigna los controles de edición a partir de los datos de un objeto Cliente *******************/

public void setCliente(Cliente cliente) {

asignaDatos(cliente);

}

}

En este código de ejemplo no se realizan validaciones sobre los datos. Una vez compiladas las clases, añadido el archivo de manifiesto, y generado un archivo ClienteBean.jar, podemos utilizar el nuevo componente en cualquier ventana. Para ello lo único que necesitamos es instanciarlo y añadirlo dentro de un contenedor de la ventana o de la propia ventana. class GestionClientes extends java.awt.Frame {

private cliente.ClienteBean clienteBean;

public GestionClientes() {

this.add( clienteBean = new cliente.ClienteBean() );

Page 204: Fundamentos de JAVA

Fundamentos de Java /204

. . .

this.pack();

this.setVisible(true);

}

. . .

}

Podemos hacer algunas consideraciones sobre el uso del componente ClienteBean: • Aunque un usuario pueda editar los controles de edición del bean, desde el código de la ventana no se puede acceder directamente a los controles internos del bean. Para asignar u obtener por código el nombre debemos usar los método accesores getCliente() y setCliente(). • Si queremos utilizar el bean sólo para mostrar datos, debemos desactivar el bean invocando su método setEnabled() con valor false, y después debemos pasarle los datos mediante el método accesor setCliente(). • Si permitimos editar el bean, podemos dejar en blanco todos sus controles con el método resetea(), y posteriormente recuperar los datos introducidos por el usuario mediante el método accesor getCliente().

4.2. Gestión de las propiedades de un bean.

4.2.1. Propiedades de un bean. Una propiedad es un atributo del JavaBean que afecta a su apariencia o a su conducta. Por ejemplo, un botón puede tener las siguientes propiedades: el tamaño, la posición, el título, el color de fondo, el color del texto, si está o no habilitado, etc. Las propiedades de un bean deben poder examinarse y modificarse mediante métodos públicos conocidos como accesores. Los métodos accesores pueden ser de dos tipos:

• Métodos getter: leen el valor de las propiedades. El nombre de estos métodos comienza por get o is (si el tipo de la propiedad es boolean). • Métodos setter: cambian el valor de las propiedades. El nombre de estos métodos comienza por set.

Un IDE que cumpla con las especificaciones de los JavaBeans, como NetBeans, sabe cómo analizar un bean y conocer sus propiedades. Además, crea una representación visual para cada uno de los tipos de propiedades, denominada editor de propiedades, para que el programador pueda modificarlas fácilmente en el momento del diseño. Cuando un programador arrastra un bean de la paleta de componentes y lo deposita en un contenedor, el IDE muestra el bean visualmente. Cuando seleccionamos el bean aparece una hoja de propiedades, que es una lista de las propiedades del bean, con sus editores asociados para cada una de ellas. El IDE llama a los métodos accesores getter para mostrar en los editores los valores de las propiedades. Si el programador cambia el valor de una propiedad se llama a un método accesor setter para actualizar el valor de dicha propiedad y que puede o no afectar al aspecto visual del bean en el momento del diseño. Las especificaciones JavaBeans definen un conjunto de convenciones (patrones de diseño) que el IDE usa para inferir qué métodos corresponden a propiedades. public void setNombrePropiedad ( TipoPropiedad valor )

public TipoPropiedad getNombrePropiedad ( )

public boolean isNombrePropiedad ( )

Cuando el IDE carga un bean usa el mecanismo denominado reflexión para examinar todos los métodos, fijándose en aquellos que empiezan por set, get e is. El IDE añade las propiedades que encuentra a la hoja de propiedades para que el programador personalice el bean. 4.2.2. Propiedades simples. Una propiedad simple representa un único valor. Por ejemplo: class Cliente {

//miembro de la clase que se usa para guardar el valor de la propiedad

private String nombre;

//métodos setter y getter de la propiedad denominada Nombre

public void setNombre(String nombre) {

this.nombre = nombre;

}

public String getNombre() {

return nombre;

}

. . .

Page 205: Fundamentos de JAVA

Fundamentos de Java /205

}

En el caso de que dicha propiedad sea booleana se escribe class Cliente {

. . .

//miembro de la clase que se usa para guardar el valor de la propiedad

private boolean preferente;

//métodos set y get de la propiedad denominada Preferente

public void setPreferente(boolean preferente) {

this. preferente = preferente;

}

public boolean isPreferente() {

return nombre;

}

}

Para que una propiedad en un bean sea de solo lectura únicamente deberá ser público un método accesor getter con el nombre de la propiedad. Para que la propiedad sea de solo escritura únicamente deberá ser público un método accesor setter con el nombre de la propiedad. 4.2.3. Propiedades indexadas. Las propiedades indexadas representan colecciones de valores a los que se accede por índice como en los arrays. Si en un bean tenemos el atributo String [] telefonos = { "11111111", "2222222", "3333333" };

Los patrones de diseño para esta propiedad son: // Para obtener el array

public String [] getTelefonos () {

return telefonos;

}

// Para cambiar el array

public void setTelefonos (String [] nuevosTelefonos) {

telefonos = muevosTelefonos;

}

// Para obtener un teléfono

public String getTelefono (int indice) {

if (telefonos == null || indice < 0 || indice >= telefonos.length)

return null;

return telefonos[indice];

}

// Para cambiar un teléfono

public void setTelefono (int indice, String telefono) {

if (telefonos == null || indice < 0 || indice >= telefonos.length)

return;

telefonos[indice] = telefono;

}

4.2.4. Propiedades compartidas. Hablaremos de que una propiedad es compartida o ligada si al ser modificado su estado, un objeto receptor (listener) es notificado de dicho cambio. Para notificar un cambio en dicha propiedad necesitamos llevar a cabo las siguientes tareas:

• Crear una clase que defina un suceso (evento) personalizado, denominada XXXEvent. • Crear una interfaz receptora, denominada XXXListener, que declare los métodos que los objetos (listeners) a los que se le notifican el cambio en dicha propiedad precisen implementar. • Crear una lista que contenga los objetos receptores interesados en el cambio del valor de dicha propiedad. • Definir dos funciones denominadas addXXXListener y removeXXXListener, que añadan o eliminen objetos receptores de dicha lista.

Para explicar la implementación de estas tareas tomaremos como referencia la clase Cliente declarada en el apartado anterior. public class Cliente {

// propiedad que queremos ligar

Page 206: Fundamentos de JAVA

Fundamentos de Java /206

private String nombre;

public void setNombre(String nombre) {

this.nombre = nombre;

}

public String getNombre() {

return nombre;

}

}

La clase que define un suceso: Las clases que definen los eventos deben extender a la clase java.util.EventObject. Denominaremos NombreEvent a la clase que define el evento de que ha cambiado el nombre del cliente. Dicha clase debe tener dos miembros, el nombre anterior y el nuevo nombre. Las clases que definen los eventos necesitan conocer al objeto que es el origen del evento, el cual se pasará como primer argumento a través de su constructor. public class NombreEvent extends EventObject {

protected String oldNombre, newNombre;

public NombreEvent(Object fuente, String oldNombre, String newNombre) {

super(fuente);

this.oldNombre = oldNombre;

this.newNombre = newNombre;

}

public String getNewNombre() {

return newNombre;

}

public int getOldNombre() {

return oldNombre;

}

}

La clase base EventObject debe recibir el objeto origen de los sucesos, y luego se inicializan los atributos con el antiguo y el nuevo nombre. La interfaz receptora: La clase cuyos objetos (listeners) están interesados en el cambio en el valor de la propiedad ligada ha de implementar una interface que denominaremos NombreListener. Dicha interface declarará una única función lanzaCambioNombre que ha de ser reescrita por la clase que implemente la interfaz. public interface NombreListener extends EventListener {

public void lanzaCambioNombre (NombreEvent e);

}

Para uniformizar el mecanismo hacemos que nuestra interfaz extienda a la interfaz java.util.EventLister, provista por Java para derivar objetos receptores de eventos. La fuente de los eventos: Un objeto que está interesado en recibir eventos se denomina receptor del evento. El objeto que produce los eventos se denomina origen del evento. El origen del evento debe mantener una lista de receptores del evento y proporcionar dos métodos para añadir (addNombreListener) o eliminar (removeNombreListener) dichos objetos de la lista. public class Cliente {

. . .

private Vector nombreListeners = new Vector();

// Método para añadir receptores del evento

public synchronized void addNombreListener(NombreListener listener) {

nombreListeners.addElement(listener);

}

// Método para eliminar receptores del evento

public synchronized void removeNombreListener(NombreListener listener) {

nombreListeners.removeElement(listener);

}

}

Cada vez que se produzca un cambio en el valor de la propiedad Nombre, se ha de notificar de dicho cambio a los objetos interesados que se guardan en el vector nombreListeners.

Page 207: Fundamentos de JAVA

Fundamentos de Java /207

Notificación de los cambios: El método que cambia la propiedad se denomina setNombre. Es por tanto en este método donde notificaremos del cambio a los receptores del evento. Para nuestro ejemplo, crearemos un objeto de la clase NombreEvent y notificaremos a los receptores del evento invocando el método lanzaCambioNombre de la interfaz NombreListener. public void setNombre (String nuevoNombre) {

if (this.nombre != nuevoNombre) {

NombreEvent event = new NombreEvent(this, this.nombre, nuevoNombre);

this.nombre = nuevoNombre;

notificarCambio(event);

}

}

Se define un método notificarCambio() para notificar del cambio a todos los receptores: private synchronized void notificarCambio(NombreEvent event) {

for (NombreListerner listener : nombreListeners)

listerner.lanzaCambioNombre(event);

}

Recepción del cambio de propiedad: Una clase receptora que reciba notificaciones del cambio en la propiedad Nombre debe implementar la interfaz NombreListener y reescribir el método lanzaCambioNombre(). Como ejemplo se crea una clase Notificacion que recibe la notificación del cambio de nombre de un cliente e imprime el antiguo y el nuevo nombre. public class Notificacion implements NombreListener {

Cliente cliente;

public Notificacion (Cliente cliente) {

this.cliente = cliente;

if (cliente != null)

cliente.addNombreListener(this); // se registra el receptor con el origen del evento

}

public void lanzaCambioNombre(NombreEvent ev) {

System.out.println("Nuevo nombre: "+ev.getNewNombre());

System.out.println("Antiguo nombre: "+ev.getOldNombre());

}

}

La clase Notificacion encapsula un objeto Cliente que es pasado a través de su constructor. Tras asignar el cliente se registra la instancia de Notificacion como receptor del evento mediante el método addNombreListener() de la clase Cliente. Cada vez que el objeto Cliente modifique su nombre desde otro código, la instancia de la clase Notificacion recibirá el aviso y tomará las medidas apropiadas. 4.2.5. Clases e interfaces predefinidas para gestionar cambios en propiedades. Para facilitar la implementación del mecanismo de aviso en el cambio de propiedades, Java proporciona clases e interfaces predefinidas que podemos utilizar en vez de tener que crear nuestras propias clases de evento e interfaces receptoras. La clase java.beans.PropertyChangeSupport incorpora todo el código necesario para registrar objetos receptores (que deben implementar la interfaz PropertyChangeListener) y notificar de los cambios. La clase PropertyChangeSupport dispone de los siguientes métodos:

• PropertyChangeSupport(Object bean) Único constructor, al cual hay que pasarle la instancia del bean que genera el evento.

• void addPropertyChangeListener(PropertyChangeListener oyente)

Añade un objeto receptor para ser notificado sobre todas las propiedades compartidas. • void removePropertyChangeListener(PropertyChangeListener oyente)

Elimina al objeto receptor dado de la lista de receptores de cualquier propiedad compartida. • void addPropertyChangeListener(String propiedad, PropertyChangeListener oyente)

Añade un objeto receptor para una propiedad compartida dada. • void removePropertyChangeListener(String propiedad, PropertyChangeListener oyente)

Hace que un objeto receptor deje de recibir notificaciones para una determinada propiedad compartida.

Page 208: Fundamentos de JAVA

Fundamentos de Java /208

• void firePropertyChange(String propiedad, Object anteriorValor, Object nuevoValor) Notifica a todos los objetos receptores registrados de que ha cambiado el valor de una propiedad dada. Este método debe recibir el nombre de la propiedad, el antiguo valor y el nuevo valor de la propiedad. El método está sobrecargado para pasar valores de tipo int y boolean. Si el antiguo y el nuevo valor son iguales y no nulos, no se lanzan notificaciones.

• void firePropertyChange(PropetyChangeEvent evt)

Notifica a todos los objetos receptores registrados de que ha cambiado el valor de una propiedad dada. Los datos necesarios (propiedad, antiguo valor y nuevo valor) deben estar encapsulados en el argumento.

• boolean hasListeners(String propiedad) Indica si una propiedad dada tiene registrados receptores o no.

Para que un bean use esta funcionalidad normalmente instanciará un objeto de la clase PropertyChangeSupport y encapsulará sus métodos. Por ejemplo, la clase Cliente se escribiría ahora así: public class Cliente {

private String nombre; // la propiedad compartida

private PropertyChangeSupport avisador; // el objeto notificador del evento

// Constructor

public Cliente () {

avisador = new PropertyChangeSupport(this);

}

// Método para añadir receptores de eventos

public void addPropertyChangeListener(PropertyChangeListener listener) {

avisador.addPropertyChangeListener(listener);

}

// Método para eliminar receptores del evento

public void removePropertyChangeListener (PropertyChangeListener listener) {

avisador.removePropertyChangeListener (listener);

}

// Método para notificar de los cambios

public void notificarCambio(PropertyChangeEvent evt) {

avisador.firePropertyChange(evt);

}

// Métodos accesores

public String getNombre() {

return nombre;

}

public void setNombre(String nuevoNombre) {

PropertyChangeEvent event = new PropertyChangeEvent("Nombre", this.nombre, nuevoNombre);

this.nombre = nuevoNombre;

notificarCambio(event);

}

}

Para crear un objeto receptor de los eventos, que sea notificado de los cambios, debemos usar una clase que implemente la interfaz java.beans.PropertyChangeListener. Esta interfaz define el siguiente método: public void propertyChange(PropertyChangeEvent evt)

Donde el parámetro evt es un objeto del cual se pueden utilizar los siguientes métodos: • String getPropertyName(), para obtener el nombre de la propiedad sobre la que se notifica. • Object getNewValue(), para obtener el nuevo valor de la propiedad. • Object getOldValue(), para obtener el antiguo valor de la propiedad.

De esta forma, la clase receptora Notificacion se definiría ahora de la siguiente manera: public class Notificacion implements PropertyChangeListener {

Cliente cliente;

public Notificacion (Cliente cliente) {

this.cliente = cliente;

if (cliente != null)

cliente.addPropertyChangeListener(this, "Nombre"); // se registra el receptor con el origen del evento

}

public void propertyChange(PropertyChangeEvent ev) {

Page 209: Fundamentos de JAVA

Fundamentos de Java /209

System.out.println("Nuevo nombre: "+ev.getNewValue());

System.out.println("Antiguo nombre: "+ev.getOldValue());

}

}

4.2.6. Ejemplo de gestión de propiedades compartidas. En los componentes beans gráficos podemos capturar los cambios que produce el usuario en los controles internos capturando el evento de pérdida de foco de cada control, y comprobando si se ha cambiado el valor.

NOTA: Los controles definidos en las librerías gráficas awt y swing están implementados como beans que incorporan la funcionalidad de avisar de cambios en algunas de sus propiedades. Internamente ya encapsulan un objeto privado de tipo PropertyChangeSupport y definen métodos públicos que a su vez invocan a los métodos addPropertyChangeListener(), removePropertyChangeListener() y firePropertyChange() del objeto PropertyChangeSupport.

Para aplicar este mecanismo en la clase ClienteBean registraremos los eventos de perdida de foco del cuadro de edición del nombre en el constructor del bean. Al extender a la clase java.awt.Panel hereda los métodos para registrar, eliminar y avisar a objetos oyentes. Previamente se crea una variable privada de tipo Cliente para contener los valores actuales, y entonces modificaremos los métodos accesores. public class ClienteBean extends Panel implements java.io.Serializable {

. . .

private Cliente cliente; // contendrá los valores actuales del cliente

/* Constructor sin argumentos *******************************************************/

public ClienteBean(){

cliente = new Cliente("");

. . .

// Se gestiona el evento de perdida de foco del cuadro del texto del nombre

tfNombre.addFocusListener(new java.awt.event.FocusAdapter() {

public void focusLost(FocusEvent e) {

String newNombre = tfNombre.getText(); // se obtiene el contenido del cuadro de texto

String oldNombre = cliente.getNombre(); // se obtiene el nombre actual

if ( ! newNombre.equals(oldNombre) ) { // si cambió el nombre

firePropertyChange("Nombre", oldNombre(), newNombre); // se lanza el aviso

cliente.setNombre(newNombre); // y se actualiza

}

}

});

}

. . .

/* Retorna un objeto Cliente a partir del contenido de los controles internos ******************/

public Cliente getCliente() {

return cliente;

}

/* Asigna los controles de edición a partir de los datos de un objeto Cliente *******************/

public void setCliente(Cliente cliente) {

if (cliente == null)

this.cliente = new Cliente(""); // se crea un nuevo cliente con los datos en blanco

else

this.cliente = cliente.clone(); // se asigna una copia del objeto original

asignaDatos(this.cliente);

}

/* Métodos envolventes para registrar oyentes ********************************************/

public void addOyenteNombre (PropertyChangeListener pcl) {

super.addPropertyChangeListener ("Nombre", pcl );

}

public void removeOyenteNombre( PropertyChangeListener pcl) {

super.removePropertyChangeListener ("Nombre", pcl );

}

}

Si ahora instanciamos un objeto de nuestro componente vean:

Page 210: Fundamentos de JAVA

Fundamentos de Java /210

ClienteBean bean1 = new ClienteBean();

Podemos crear un objeto oyente y registrarlo con el componente: bean1.addOyenteNombre( objetoOyente );

4.2.7. Propiedades restringidas. Una propiedad restringida es similar a una propiedad ligada salvo que los objetos receptores (listeners) a los que se les notifica el cambio del valor de la propiedad tienen la opción de vetar cualquier cambio en el valor de dicha propiedad. Al igual que con las propiedades compartidas, Java proporciona clases e interfaces predefinidas que podemos utilizar en vez de tener que crear nuestras propias clases de evento e interfaces receptoras. El mecanismo de notificación de los cambios se encuentra implementado por la clase java.beans.VetoableChangeSupport. Esta clase dispone de métodos para registrar objetos receptores (que deben implementar la interfaz VetoableChangeListener), para notificar de los cambios, y para detectar el veto de algún objeto receptor. La clase VetoableChangeSupport dispone de los siguientes métodos:

• VetoableChangeSupport(Object bean) Único constructor, al cual hay que pasarle la instancia del bean que genera el evento.

• void addVetoableChangeListener(VetoableChangeListener oyente)

Añade un objeto receptor para ser notificado sobre todas las propiedades restringidas. • void removeVetoableChangeListener(VetoableChangeListener oyente)

Elimina al objeto receptor dado de la lista de oyentes de cualquier propiedad restringida. • void addVetoableChangeListener(String propiedad, VetoableChangeListener oyente)

Añade un objeto receptor para una propiedad restringida dada. • void removeVetoableChangeListener(String propiedad, VetoableChangeListener oyente)

Hace que un objeto receptor deje de recibir avisos para una determinada propiedad restringida. • void fireVetoableChange(String propiedad, Object anteriorValor, Object nuevoValor) throws PropertyVetoException

Avisa a los objetos receptores registrados de que ha cambiado el valor de una propiedad dada. Este método debe pasar el antiguo valor y el nuevo valor de la propiedad. El método está sobrecargado para pasar valores de tipo int y boolean. Si el antiguo y el nuevo valor son iguales y no nulos, no se lanzan notificaciones. Si un receptor veta el cambio se relanza una excepción del tipo PropertyVetoException.

• void fireVetoableChange(PropetyChangeEvent evt) throws PropertyVetoException

Notifica a todos los objetos receptores registrados de que ha cambiado el valor de una propiedad dada. Los datos necesarios (propiedad, antiguo valor y nuevo valor) deben estar encapsulados en el argumento.

• boolean hasListeners(String propiedad) Indica si una propiedad tiene registrados oyentes o no.

Para que el bean use esta funcionalidad debe instanciar un objeto de la clase java.beans.VetoableChangeSupport y encapsular sus métodos. Si queremos que la propiedad Nombre de la clase Cliente sea restringida, la clase se escribiría ahora así: public class Cliente {

private String nombre; // la propiedad compartida y restringida

private VetoableChangeSupport avisadorVeto; // el objeto notificador del evento de veto

private PropertyChangeSupport avisador; // el objeto notificador del evento de cambio

// Constructor

public Cliente () {

avisador = new PropertyChangeSupport(this);

avisadorVeto = new VetoableChangeSupport(this);

}

. . .

// Método para añadir receptores de eventos

public void addPropertyVetoableListener(VetoableChangeListener listener) {

avisador.addVetoableChangeListener( "Nombre", listener);

}

// Método para eliminar receptores del evento

public void removeVetoableChangeListener (PropertyChangeListener listener) {

avisador.removePropertyChangeListener ( "Nombre", listener);

}

// Método para notificar de los cambios de veto

Page 211: Fundamentos de JAVA

Fundamentos de Java /211

public void notificarVetoCambio(PropertyChangeEvent evt) {

avisador.fireVetoableChange(evt);

}

// Métodos accesores

public String getNombre() {

return nombre;

}

public void setNombre(String nuevoNombre) throws PropertyVetoException {

PropertyChangeEvent event = new PropertyChangeEvent("Nombre", this.nombre, nuevoNombre);

notificarVetoCambio(event); // se pide permiso de cambio

this.nombre = (String) event.getNewValue(); // si no se produjo excepción se produce el cambio

notificarCambio(event); // y se avisa del cambio

}

}

El cambio fundamental es que ahora el método accesor setNombre() relanza una excepción del tipo java.beans.PropertyVetoException si se veta el cambio de la propiedad. Se podría haber optado por captura la excepción internamente mediante un bloque try/catch sin relanzarla. Para crear un objeto receptor que reciba avisos de veto de propiedades restringidas debemos usar una clase que implemente la interfaz VetoableChangeListener. Esta interfaz define el siguiente método: public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException

Donde el parámetro evt es un objeto del cual se pueden utilizar los siguientes métodos: • String getPropertyName(), para obtener el nombre de la propiedad sobre la que se avisa. • Object getNewValue(), para obtener el nuevo valor de la propiedad. • Object getOldValue(), para obtener el antiguo valor de la propiedad.

Siguiendo el ejemplo precedente, reescribiremos la clase Notificacion para poder vetar cambios si el antiguo nombre es igual a "Pedro": public class Notificacion implements PropertyChangeListener, VetoableChangeListener {

Cliente cliente;

public Notificacion (Cliente cliente) {

this.cliente = cliente;

if (cliente != null) {

cliente.addPropertyChangeListener(this, "Nombre"); // se registra el receptor con el origen del evento

cliente.addVetoableChangeListener(this, "Nombre"); // se registra el receptor con el origen del evento

}

}

. . .

public void vetoableChange(PropertyChangeEvent ev) throws PropertyVetoException {

if (evt.getOldValue.equals("Pedro"))

throw new PropertyVetoException("", ev);

}

}

El tipo de excepción PropertyVetoException recibe en su constructor dos argumentos: un mensaje opcional y un objeto de tipo PropertyChangeEvent. Estos dos argumentos pueden ser recuperados posteriormente con los métodos getMessage() y getPropertyChangeEvent() respectivamente.

4.2.8. Ejemplo de gestión de propiedades restringidas.

NOTA: Los controles definidos en las librerías gráficas awt y swing están implementados como beans que incorporan la funcionalidad de avisar de vetos en algunas de sus propiedades. Internamente ya encapsulan un objeto privado de tipo VetoableChangeSupport y definen métodos públicos que a su vez invocan a los métodos addVetoableChangeListener(), removeVetoableChangeListener() y fireVetoableChange() del objeto VetoableChangeSupport.

Siguiendo con el ejemplo del componente ClienteBean, reescribiremos el código del evento de perdida de foco del control de edición. Si un objeto oyente veta el cambio del valor introducido por el usuario se recupera el antiguo valor. public class ClienteBean extends Panel implements java.io.Serializable {

. . .

/* Constructor sin argumentos *******************************************************/

Page 212: Fundamentos de JAVA

Fundamentos de Java /212

public ClienteBean() {

. . .

tfNombre.addFocusListener(new java.awt.event.FocusAdapter() {

public void focusLost(FocusEvent e) {

try {

cliente.setNombre(tfNombre.getText());

} catch (PropertyVetoException ex) {

// si se produce el veto se recupera el valor anterior en el editor

tfNombre.setText( (String) (ex. getPropertyChangeEvent().getOldValue()) );

}

}

});

}

. . .

/* Métodos envolventes para registrar receptores de veto *********************************/

public void addVetadorNombre (VetoableChangeListener vcl) {

cliente.addVetoableChangeListener ("nombre", vcl );

}

public void removeVetadorNombre(VetoableChangeListener vcl) {

soporte.removeVetoableChangeListener ("nombre", vcl );

}

}

4.3. Persistencia de un bean.

Un bean persiste cuando tiene sus propiedades, campos e información de estado, almacenadas en un almacén de datos persistente, de forma que posteriormente se puedan recuperar. El mecanismo que hace posible la persistencia se llama serialización. Cuando un ejemplar de bean es serializado se convierte en una canal de datos para ser escritos. Cualquier applet, aplicación o herramienta que utilice el Bean puede "reconstruirlo" mediante la deserialización. Todos los bean deben persistir, para ello deben soportar la serialización implementando la interfaz java.io.Serializable o java.io.Externalizable. Se puede controlar el nivel de serialización de nuestro Bean:

- Automático: implementando Serializable. Todo es serializado. - Excluyendo los campos que no se quieran serializar marcándolos con el modificador transient (o static). - Escribir los beans a un fichero de formato específico: implementando Externalizable, y sus dos métodos.

Como ejemplo, añadiremos a la clase Cliente la capacidad de ser persistente creando un método para serializar a un archivo y otro para deserializar desde un archivo. public class Cliente {

private String nombre;

. . .

// Método para serializar la instancia a un archivo

public boolean serializa (File archivo) {

boolean seSerializo = true;

try {

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(archivo));

oos.writeObject( this );

oos.close();

} catch (Exception ex)

seSerializo = false;

}

return seSerializo;

}

// Método para deserializar la instancia desde un archivo

public boolean deserializa (File archivo) {

boolean seDeserializo = true;

try {

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(archivo));

Cliente cliente = (Cliente) ois.readObject( );

Page 213: Fundamentos de JAVA

Fundamentos de Java /213

ois.close();

this.setNombre(cliente.getNombre());

} catch (Exception ex)

seDeserializo = false;

}

return seDeserializo;

}

}

4.4. La clase «BeanInfo».

Como se ha explicado, un IDE descubre las propiedades y los eventos de un bean a través del mecanismo denominado introspección. Otra forma de hacerlo es a través de una clase que implemente la interfaz java.beans.BeanInfo. Habitualmente las clases extenderán java.beans.SimpleBeanInfo, que es una implementación básica de BeanInfo. Para que un IDE encuentre la correspondiente clase BeanInfo, el nombre de la clase debe ser el mismo que el nombre del bean seguido por el string "BeanInfo", por ejemplo ClienteBeanInfo. La clase BeanInfo especifica la siguiente información:

• El icono que representa al bean. • Un objeto BeanDescriptor que contiene una referencia a una clase Customizer (que proporcionará una interfaz de usuario personalizable). • Una lista de propiedades del bean con una breve descripción de estas propiedades. Esta descripción será utilizada por la hoja de propiedades del componente en el IDE en el momento de diseño. Se incluye el nombre de la propiedad, su valor, y una descripción de ayuda. • Referencias al editor de propiedades. • Una lista de métodos que define el bean, con una descripción de cada uno de ellos.

La clase que implementa el interface BeanInfo se usa antes que el mecanismo de la introspección. Añadiendo una clase BeanInfo a nuestro bean podemos incluso ocultar propiedades que no queremos que se muestren en la hoja de propiedades del componente en el IDE, que de otro modo sí serían mostradas.

Page 214: Fundamentos de JAVA

Fundamentos de Java /214

VIII - APPLETS

Los applets son programas Java que deben ser cargados y ejecutados desde un navegador web (browser), como «FireFox» o «Microsoft Internet Explorer».

1. Introducción.

Un applet debe extender la clase java.applet.Applet o javax.swing.JApplet, las cuales a su vez extienden respectivamente a las clases java.awt.Panel y javax.swing.JPanel. No se ejecuta como una aplicación normal (no tiene un constructor público), sino que es incrustado en un documento web, quien se encarga de instanciarlo y ejecutarlo.

1.1. ¿Cómo crear un applet Java?

Como la clase subyacente de un applet es un panel gráfico, podemos diseñar el contenido de un applet de la misma forma que se diseñaría un panel. Bien añadiendo componentes awt o swing al panel, o bien dibujando directamente sobre la superficie del panel usando la clase Graphics. En este primer ejemplo de un applet, se muestra un mensaje de saludo que es dibujado usando la clase Graphics. Para que se mantenga el mensaje es necesario que el código de dibujo se establezca en el método paint(). Esto garantiza que cada vez que se tenga que redibujar el applet por cualquier motivo, se redibujará nuestro mensaje. import java.awt.Graphics;

public class Hola extends java.applet.Applet {

// Se reescribe este método para mostrar el mensaje

public void paint (Graphics g) {

g.drawString ("¡Hola Mundo!", 5, 25);

}

}

Tras compilar el archivo fuente se obtiene el archivo «Hola.class» (o bien un archivo «Hola.jar»). Dicho archivo se puede referenciar dentro de una página web para que cuando sea mostrada en un navegador se muestre el applet. Para hacer esto se utiliza la instrucción del lenguaje HTML <applet>. El siguiente código incluye el applet «Hola.class» en una página web. <html>

<head>

<title>Hola mundo</title>

</head>

<body>

<applet code="Hola.class" width="150" height="25">

Su navegador no soporta applets de Java

</applet>

</body>

</html>

La etiqueta <applet> admite los siguientes atributos: • code: especifica el nombre del archivo ".class". • width, heigth: especifican el ancho y alto del applet. • codebase: especifica la ruta del archivo class o jar. • archive: especifica el archivo jar. Si se usa este atributo, en el atributo code deberemos indicar la ruta de paquetes (p.e. “unPaquete.Hola”). • name: especifica un nombre para identificar al applet.

1.2. Métodos de la clase «Applet».

Los applets son instanciados por el navegador que ejecuta las páginas que los incluyen. Un programador no debe ni crear ni reescribir los constructores de esta clase. La clase Applet define unos métodos específicos que serán invocados automáticamente por el navegador en situaciones determinadas. El código que insertemos en estos métodos determinarán el comportamiento del applet. Los métodos que definen el comportamiento de un applet son:

• void init () - Inicializar

Page 215: Fundamentos de JAVA

Fundamentos de Java /215

Es invocado una sola vez cuando se carga el applet desde la página web. Se usa para establecer valores iniciales, cargar imágenes, fuentes, etc.

• void start () - Iniciar Es invocado después de la inicialización y cada vez que la página web debe retomar le ejecución del applet. Se usa para iniciar los procesos del applet (como hilos).

• vois stop () - Parar Es invocado cuando el usuario abandona la página que contiene el applet. Cualquier thread que el applet haya iniciado continuará su ejecución, pero es posible detenerlos sobrescribiendo este método.

• void destroy () - Limpiar Es invocado justo antes de que la página web que contiene al applet se cierre. Se usa para eliminar cualquier hilo en ejecución o liberar objetos.

• void paint (Graphics g) - Pintar Es invocado cada vez que el navegador necesite repintar el applet. Se usa para dibujar algo en pantalla, ya sea texto, una línea, color de fondo o imágenes y que se mantenga.

Otros métodos interesantes de un applet son: • URL getDocumentBase ()

Retorna la ruta url del documento web en el cual está incrustado el applet. • void showStatus (String mensaje)

Muestra en la barra de estado del navegado un mensaje. • AudioClip getAudioClip (URL url, String nombre)

Retorna un clip de audio especificado. • void play (URL url, String nombre)

Reproduce un clip de audio especificado. • void repaint ()

Provoca un repintado del applet llamando a update(), quien a su vez llama a paint().

1.3. Paso de parámetros a los applets.

Desde la página web que incluye a un applet podemos pasar parámetros al applet. El siguiente ejemplo muestra cómo pasar parámetros a un applet desde la página web, y cómo el applet los puede obtener:

Código de la página HTML <applet align="middle" code="MiApplet.class" width="225"

height="100">

<param name="mensaje" value="Hola">

<param name="fuente" value="verdana">

Su navegador no soporta Java!.

</applet>

Los parámetros se pasan al applet cuando es cargado y se obtienen utilizando el método getParameter(), al cual se pasa como argumento un string con el nombre del parámetro y retorna un string con el valor.

Código de la clase Java para obtener los parámetros import java.awt.*;

public class MiApplet extends java.applet.Applet {

Font unaFuente;

// Defino una variable para guardar cada parámetro

String sMensaje; // guardará el mensaje

String sFuente; // guardará la fuente

// Se leen los parámetros y se asigna un tipo de letra

public void init (){

sMensaje = getParameter ("mensaje");

sFuente = getParameter ("fuente");

// Asigno un tipo de letra a partir del parámetro

unaFuente = new Font(sFuente, Font.BOLD, 12);

}

// Se muestra el mensaje del parámetro con el tipo de letra

public void paint (Graphics g) {

g.setFont(unaFuente);

Page 216: Fundamentos de JAVA

Fundamentos de Java /216

g.setColor(Color.green);

g.drawString(sMensaje, 5, 50);

}

}

2. Conceptos avanzados.

2.1. Hilos de ejecución y applets.

Para que la ejecución de los applets no cause problemas en el navegador web, es aconsejable que los procesos del applet se lancen mediante hilos de ejecución (threads). El lugar idóneo para crear un objeto thread es en el método start() del applet y ejecutarlo a continuación. Por ejemplo: public class MiApplet extends Applet {

Thread mihilo;

// método start() del applet . . .

public void start() {

if(mihilo == null) { // si no se creó todavía

mihilo = new Thread(this); // se crea ahora

mihilo.start(); // y se lanza

}

}

. . .

}

2.2. Comunicación entre applets (interfaz «AppletContext»).

Los navegadores mantienen una colección de los applets cargados en cada página. Desde el código de un applet es posible acceder a esta colección utilizando la interfaz AppletContext. Los métodos de la interfaz java.applet.AppletContext pueden ser usados por un applet para obtener información acerca de su entorno. Para obtener la información del contexto de la página web que contiene a los applets se utiliza el método getAppletContext() de la clase Applet, que retorna un objeto de esta interfaz. Los métodos de AppletContext para acceder a las instancias de los applets creados en una página web son:

• Enumeration getApplets () Retorna una enumeración de todos los applets cargados en el documento web.

• Applet getApplet (String nombre) Retorna la instancia del applet de nombre dado. Este nombre se corresponde con el definido en la etiqueta HTML <applet name="nombreDelApplet" >.

• void showDocument (URL url) Provoca que el navegador muestre la página web indicada por la ruta url.

2.3. Ejemplo de applet animado.

Crearemos un applet que muestre la animación de un monigote moviendo sus brazos. Para ello usaremos dos fotogramas con este aspecto:

Existen varios métodos para crear estas imágenes. En nuestro applet crearemos dos objetos Image y dibujaremos sobre ellos las líneas mediante código. (Otra opción es crear las imágenes mediante un programa de dibujo y guardarlas en archivo; se pueden crear objetos Image a partir de un archivo) La clase Image es abstracta, por ello usaremos el método createImage() del applet para crear los fotogramas. Para dibujar sobre un componente gráfico primero debemos obtener su contexto gráfico mediante el método getGraphics(). (Cada objeto gráfico posee su contexto gráfico, por tanto no hay que crear uno nuevo.) El applet debe lanzar un proceso para ir alternando los dos fotogramas aproximadamente cada 200 milisegundos. Para ello crearemos un hilo de ejecución que se mantendrá en ejecución durante toda la vida

Page 217: Fundamentos de JAVA

Fundamentos de Java /217

del applet. Usaremos una variable booleana para parar la animación cuando la página web que contiene el applet se oculte. El código para mostrar los fotogramas debe ubicarse en el método paint(), ya que nos garantiza la permanencia de los dibujos en la pantalla. Desde el hilo haremos sucesivas llamadas a paint() para alternar los fotogramas. import java.awt.Color;

import java.awt.Graphics;

import java.awt.Image;

import java.util.Date;

import javax.swing.JApplet;

public class AppletAnimado extends JApplet implements Runnable {

static int ESPERA = 200; //milisegundos entre fotos

Image [] img; // array para guardar los fotogramas

int contador; // se usará de índice para el array img

long cronoInicio; // origen para contar la espera de tiempo

Thread hilo; // el hilo de ejecución

boolean bParar = false; // usado para parar el hilo

// MÉTODO DE INICIALIZACIÓN

public void init() {

// Si existe, se lee como parámetro el tiempo de espera

String sEspera = getParameter(“espera”);

if (sEspera != null)

ESPERA = Integer.parseInt(sEspera);

// Se crean los fotogramas

img = new Image [2]; // array para dos elementos

// Crea el primer fotograma y dibuja el monigote

img[0] = this.createImage(101, 176);

Graphics g = img[0].getGraphics();

g.setColor(Color.red); // líneas a color rojo

g.drawOval(25, 0, 50, 50); // cabeza

g.drawLine(50, 50, 50, 125); // cuerpo vertical

g.drawLine(0, 75, 100, 75); // brazos horizontales

g.drawLine(50, 125, 25, 175); // pierna izquierda

g.drawLine(50, 125, 75, 175); // pierna derecha

// Crea el segundo fotograma y dibuja el monigote

img[1] = this.createImage(100, 175);

g = img[1].getGraphics();

g.setColor(Color.red); // líneas a color rojo

g.drawOval(25, 0, 50, 50); // cabeza

g.drawLine(50, 50, 50, 125); // cuerpo vertical

g.drawLine(0, 50, 50, 75); // brazo izquierdo

g.drawLine(50, 75, 100, 50); // brazo derecho

g.drawLine(50, 125, 25, 175); // pierna izquierda

g.drawLine(50, 125, 75, 175); // pierna derecha

}

// SE LLAMA CADA VEZ QUE SE RETOMA EL APPLET

public void start() {

if (hilo==null) { // si todabía no se creó el hilo

hilo = new Thread (this); // lo crea

hilo.start(); // y lo lanza

}

bParar = false; // se inicia o retoma la animación

}

// SE LLAMA CADA VEZ QUE SE ABANDONA EL APPLET

public void stop() {

bParar = true; // se para la animación

}

Page 218: Fundamentos de JAVA

Fundamentos de Java /218

// MÉTODO DE DESTRUCCIÓN

public void destroy() {

hilo = null; // para el hilo negando la condición del bucle

}

// SE LLAMA CADA VEZ QUE EL SISTEMA NECESITA REPINTAR EL APPLET.

// Y ADEMÁS LO LLAMAREMOS DESDE EL PROCESO DEL HILO.

public void paint(Graphics g) {

// Muestra el fotograma correspondiente

g.drawImage(img[contador], 0, 0, this);

}

// MÉTODO HEREDADO DE RUNNABLE PARA EJECUTAR EL PROCESO DEL HILO

public void run() {

cronoInicio= (new Date()).getTime();

// El bucle acaba en el momento que hilo sea null

while (hilo == Thread.currentThread())

if (! bParar) { // Si el proceso no está parado

long crono = (new Date()).getTime();

if (crono-cronoInicio >= ESPERA) {

cronoInicio = crono;

contador = (++contador) % 2; // alterna entre 0 y 1

repaint(); // llama a paint()

}

}

}

}

Una página web que contenga el applet anterior podría ser la siguiente. Esta página pasa como parámetro el tiempo de espera de la animación. <html>

<head>

<title>Un applet animado</title>

</head>

<body>

<applet code="AppletAnimado.class"

width="101" height="176">

<param name=”espera” value=250>

</applet>

</body>

</html>

Page 219: Fundamentos de JAVA

Fundamentos de Java /219

IX - COMUNICACIONES EN RED

Para los propósitos que persigue este manual, una red de ordenadores es un conjunto de máquinas o dispositivos que están interconectados a través de algún medio que les permite intercambiar datos. Cada uno de los dispositivos de la red puede considerarse como un nodo, y cada uno de estos nodos está identificado mediante una dirección única. Para el protocolo TCP/IP, que es el que se utiliza en comunicaciones por Internet, esta direcciones se suelen expresar con la forma 127.20.432.51.

1. Introducción.

Las redes actuales utilizan para la transferencia de datos un concepto conocido como «packet switching». Los datos son encapsulados en paquetes que son transferidos desde el origen hasta el destino, en donde los datos se van extrayendo de uno o más paquetes para reconstruir el mensaje original. Tanto como es posible, Java abstrae todos los detalles de manejo a bajo nivel de la red, dejándole ese trabajo a la Máquina Virtual Java. El modelo de programación que se utiliza es el de ficheros; de hecho, se puede comparar una conexión de red (un socket) con un canal. Además, la capacidad de manejo de múltiples hilos de ejecución que proporciona Java, es muy cómoda a la hora de poder manejar múltiples conexiones a la vez. Java proporciona dos formas diferentes de atacar la programación de comunicaciones a través de red, al menos en lo que a la comunicación web concierne. Por un lado están las clases Socket, DatagramSocket y ServerSocket, y por otro lado están las clases URL, URLEncoder y URLConnection.

1.1. La clase «InetAddress».

La clase java.net.InetAddress proporciona objetos que se pueden utilizar para manipular tanto direcciones IP como nombres de dominio; sin embargo, no se pueden instanciar estos objetos directamente. La clase proporciona varios métodos estáticos que devuelven un objeto de tipo InetAddress:

• El método estático InetAddress.getByName(String host) devuelve un objeto InetAddress representando el host que se le pasa como parámetro. Este método se puede utilizar para determinar la dirección IP de un host, conociendo su nombre; entendiendo por nombre del host el nombre de la máquina, como "java.sun.com", o la representación como cadena de su dirección IP, como "206.26.48.100". • El método InetAddress.getAllByName(String host) devuelve un array de objetos InetAddress, y se puede utilizar para determinar todas las direcciones IP asignadas a un host. • El método InetAddress.getLocalHost() devuelve un objeto InetAddress representando el ordenador local sobre el que se ejecuta la aplicación. • Los métodos InetAddress.getByAddress(String host, byte[] addr) e InetAddress.getByAddress(String host) retornan un objeto InetAddress basado en el host y la dirección IP indicados respectivamente en host y addr.

Una vez que se ha obtenido el objeto InetAddress, hay otra serie de métodos de objeto que se pueden utilizar: • El método String getHostName() retorna el nombre de la máquina. • El método byte [] getAddress() retorna un array de bytes conteniendo la dirección IP de la máquina. Las direcciones IP, según la versión de protocolo, están formadas por 4 o 16 números enteros en el rango 0 y 255. Debemos tener en cuenta que el tipo byte está comprendido entre el rango -127 y 128, y por tanto los valores negativos deberán ser interpretados como positivos sumándoles simplemente el valor 256. Por ejemplo, el siguiente código obtiene en el string srtIP la dirección IP, separada por puntos, de un objeto address:

byte[] bytes = address.getAddress();

String strIP = "";

for( int i=0; i < bytes.length; i++ ) {

int uByte = bytes[i] < 0 ? bytes[i]+256 : bytes[cnt];

strIP += uByte + (i < bytes.length - 1 ? "." : "");

}

1.2. Trabajando con URL’s.

1.2.1. Direcciones de red. Una URL, o dirección de red, es en realidad un puntero a un determinado recurso de un determinado sitio de Internet. Al especificar una URL, se está indicando:

- El protocolo utilizado para acceder al servidor (http, por ejemplo) - El nombre del servidor

Page 220: Fundamentos de JAVA

Fundamentos de Java /220

- El puerto de conexión (opcional) - El camino, y - El nombre de un fichero determinado en el servidor (opcional a veces)

A veces el nombre del fichero se puede omitir, ya que el navegador incorporará automáticamente el nombre de fichero index.html cuando no se indique ninguno, e intentará descargar ese fichero. Por ejemplo, si queremos descargar la página principal de un sitio web, se podrá usar cualquiera de las dos URL siguientes: http://www.misitioweb.es:8080/index.html

http:// www.misitioweb.es:8080/

La sintaxis general, resumiendo pues, para una dirección URL, sería: protocolo://nombre_servidor[:puerto]/directorios/fichero

El puerto es opcional y normalmente no es necesario especificarlo si se está accediendo a un servidor que proporcione sus servicios a través de los puertos estándar; tanto el navegador como cualquier otra herramienta que se utilice en la conexión conocen perfectamente los puertos por los cuales se proporciona cada uno de los servicios e intentan conectarse directamente a ellos por defecto. 1.2.2. La clase «URL». La clase java.net.URL contiene constructores y métodos para la manipulación de URL’s que reverencien objetos o servicios en Internet. El protocolo TCP necesita dos tipos de información: la dirección IP (o dominio) y el número de puerto. Como se ha visto al tratar la clase InetAddress, si se quiere obtener la dirección IP real de la red en que se está ejecutando una aplicación, se pueden realizar llamadas a los métodos getLocalHost() y getAddress(). El puerto habitual de los servicios Web es el 80, aunque servidores como Tomcat utilizan el 8080. Con todo esto, Java permite los siguientes cuatro constructores para la clase URL:

• public URL( String spec ) • public URL( String protocol, String host, int port, String file ) • public URL( String protocol, String host, String file ) throws MalformedURLException • public URL( URL context, String spec )

Así que se podrían especificar todos los componentes de una dirección URL como en: URL url = new URL( "http","www.yahoo.com","80","index.html" );

o dejar que los sistemas utilicen todos los valores por defecto que tienen definidos, como en: URL url = new URL( "www.yahoo.com" );

y en los dos casos se obtendría la visualización de la página principal de Yahoo en el navegador desde el cual se ha invocado. La clase URL incluye métodos para obtener cada una de las partes que componen una URL:

• String getProtocol() retorna el protocolo (por ejemplo "http"). • String getHost() retorna el nombre de host (por ejemplo "www.micuenta.com"). Las direcciones IPv6 son encapsuladas entre corchetes. • int getPort() retorna el número de puerto (por ejemplo 80). • String getFile() retorna la ruta del archivo referenciado (por ejemplo "ventas/index.html"). • String getRef() retorna la referencia completa (por ejemplo "http://www.micuenta.com:80/ventas/index.html").

El ejemplo que se presenta a continuación, muestra cómo se utiliza un objeto URL para conectarse a una dirección URL y leer un fichero desde esa dirección a través de un canal de entrada de tipo stream. import java.net.*;

import java.io.*;

class EjemploURL {

public static void main( String[] args ) {

String cadena;

try {

// Creamos un objeto de tipo URL

URL url = new URL("http://www.micuenta.com/ventas/index.html" );

// Abrimos una conexión hacia esa URL, que devolverá un canal de entrada

// por el cual se podrá leer todo lo que llegue.

BufferedReader paginaHtml = new BufferedReader( new InputStreamReader( url.openStream() ) );

// Leemos y presentamos el contenido del fichero en pantalla línea a línea

while( (cadena = paginaHtml.readLine()) != null ) {

System.out.println( cadena );

}

Page 221: Fundamentos de JAVA

Fundamentos de Java /221

} catch( UnknownHostException e ) {

e.printStackTrace();

} catch( MalformedURLException e ) {

e.printStackTrace();

} catch( IOException e ) {

e.printStackTrace();

}

}

}

Como se ve en este ejemplo, una vez que se dispone del objeto URL, se puede obtener un canal de entrada (para leer sus datos) mediante el método InputStream openStream(). 1.2.3. La clase «URLEncoder». La clase jave.net.URLEnconder proporciona el método estático encode(String url), que convierte la cadena que se le pasa a un formato llamado x-www-from-urlencoded y la devuelve como un objeto String. Para realizar la conversión, todos los caracteres se van examinando uno a uno, de izquierda a derecha. Los caracteres ASCII de la „a‟ a la „z‟, de la „A‟ a la „Z‟ y del „0‟ al „9‟, permanecen igual. Los espacios en blanco „ „ se convierten a un signo más, „+‟. El resto de los caracteres se convierten a una cadena de 3 caracteres de la forma "%xy", donde xy son los dos dígitos en hexadecimal que representan los 8 bits más significativos del carácter. El siguiente fragmento de código es el que muestra cómo se realiza esta conversión: System.out.println( URLEncoder.encode( "http://espacio .tilde~.mas+.com" ) );

En la pantalla, la representación de la cadena en el formato que se ha descrito aparecerá como: http%3A%2F%2Fespacio+.tilde%7E.mas%2B.com

1.2.4. La clase «URLConnection». La clase java.net.URLConnection es una clase abstracta que puede ser extendida, con un constructor protegido que admite un objeto URL como parámetro. Tiene unas ocho variables que contienen información muy útil sobre la conexión que se haya establecido y cerca de cuarenta métodos que se pueden utilizar para examinar y manipular el objeto que se crea con la instanciación de la clase. El siguiente ejemplo muestra cómo se realiza la conexión a una dirección URL y se crea un objeto de tipo URLConnection. El programa usa entonces ese objeto para obtener y presentar en pantalla algunos de los aspectos de alto nivel de la dirección URL, como son el tipo de contenido, la fecha de la última modificación o la propia dirección URL. import java.net.*;

import java.io.*;

import java.util.*;

class EjemploConexion {

public static void main( String[] args ) {

String cadena;

try {

// Creamos un objeto de tipo URL

URL url = new URL("http://www.micuenta.com/ventas/index.html" );

// Se abre una conexión hacia la dirección recogiendola en un objeto de tipo URLConnection

URLConnection conexion = url.openConnection();

// Se utiliza la conexión para leer y presentar la dirección

System.out.println( conexion.getURL() );

// Se utiliza la conexión para leer y presentar la fecha de última modificación

Date fecha = new Date( conexion.getLastModified() );

System.out.println( fecha );

// Se utiliza la conexión para leer y presentar el tipo de contenido

System.out.println( conexion.getContentType() );

// Se abre un canal con la conexión, obteniendo primero un objeto InputStream

BufferedReader paginaHtml = new BufferedReader( new InputStreamReader(url.openStream()) );

// Se utiliza el objeto DataInputStream para leer y presentar el fichero línea a línea

while( (cadena = paginaHtml.readLine()) != null ) {

System.out.println( cadena );

}

} catch( UnknownHostException e ) {

e.printStackTrace();

Page 222: Fundamentos de JAVA

Fundamentos de Java /222

} catch( MalformedURLException e ) {

e.printStackTrace();

} catch( IOException e ) {

e.printStackTrace();

}

}

}

Como se ve en el ejemplo, la forma habitual de conseguir un objeto de tipo URLConnection es invocar el método openConnection() de un objeto URL.

2. Trabajando con Sockets.

2.1. Fundamentos sobre sockets.

Los sockets son un sistema de comunicación entre procesos de diferentes máquinas de una red. Más exactamente, un socket es un punto de comunicación por el cual un proceso puede emitir o recibir información. Utilizan una serie de primitivas para establecer el punto de comunicación, para conectarse a una máquina remota en un determinado puerto que esté disponible, para escuchar en él, para leer o escribir y publicar información en él, y finalmente para desconectarse. Con todas las primitivas se puede crear un sistema de diálogo muy completo. 2.1.1. Tipos de sokets. El tipo de sockets describe la forma en la que se transfiere información a través de ese socket:

• Sockets Stream (TCP). Son un servicio orientado a conexión, donde los datos se transfieren sin encuadrarlos en registros o bloques. Si se rompe la conexión entre los procesos, éstos serán informados de tal suceso para que tomen las medidas oportunas. El protocolo de comunicaciones con streams es un protocolo orientado a conexión, ya que para establecer una comunicación utilizando el protocolo TCP, hay que establecer en primer lugar una conexión entre un par de sockets. Mientras uno de los sockets atiende peticiones de conexión (servidor), el otro solicita una conexión (cliente). Una vez que los dos sockets estén conectados, se pueden utilizar para transmitir datos en ambas direcciones. • Sockets Datagrama (UDP). Son un servicio de transporte sin conexión. Son más eficientes que TCP, pero en su utilización no está garantizada la fiabilidad. Los datos se envían y reciben en paquetes, cuya entrega no está garantizada. Los paquetes pueden ser duplicados, perdidos o llegar en un orden diferente al que se envió. El protocolo de comunicaciones con datagramas es un protocolo sin conexión, es decir, cada vez que se envíen datagramas es necesario enviar el descriptor del socket local y la dirección del socket que debe recibir el datagrama. Como se puede ver, hay que enviar datos adicionales cada vez que se realice una comunicación, aunque tiene la ventaja de que se pueden indicar direcciones globales y el mismo mensaje llegará a muchas máquinas a la vez. • Sockets Raw. Son sockets que dan acceso directo a la capa de software de red subyacente o a protocolos de más bajo nivel. Se utilizan sobre todo para la depuración del código de los protocolos.

2.1.2. Funcionamiento genérico de los sokets. Normalmente, un servidor se ejecuta sobre una computadora específica y tiene un socket que responde en un puerto específico. El servidor únicamente espera, escuchando a través del socket a que un cliente haga una petición. En el lado del cliente, el cliente conoce el nombre de host de la máquina en la cual el servidor se encuentra ejecutando y el número de puerto en el cual el servidor está conectado. Para realizar una petición de conexión, el cliente intenta encontrar al servidor en la máquina servidora en el puerto especificado.

Si todo va bien, el servidor acepta la conexión. Además de aceptar, el servidor obtiene un nuevo socket sobre un puerto diferente. Esto se debe a que necesita un nuevo socket (y, en consecuencia, un numero de puerto diferente) para seguir atendiendo al socket original para peticiones de conexión mientras atiende las necesidades del cliente que se conectó.

Page 223: Fundamentos de JAVA

Fundamentos de Java /223

Por la parte del cliente, si la conexión es aceptada, un socket se crea de forma satisfactoria y puede usarlo para comunicarse con el servidor. Es importante darse cuenta que el socket en el cliente no está utilizando el número de puerto usado para realizar la petición al servidor. En lugar de éste, el cliente asigna un número de puerto local a la máquina en la cual está siendo ejecutado. Ahora el cliente y el servidor pueden comunicarse escribiendo o leyendo en o desde sus respectivos sockets.

2.2. Java Sockets.

El paquete java.net Java proporciona una clase Socket, la cual implementa una de las partes de la comunicación bidireccional entre un programa Java y otro programa en la red. La clase Socket se sitúa en la parte más alta de una implementación dependiente de la plataforma, ocultando los detalles de cualquier sistema particular al programa Java. Usando la clase java.net.Socket en lugar de utilizar código nativo de la plataforma, los programas Java pueden comunicarse a través de la red de una forma totalmente independiente de la plataforma. De forma adicional, java.net incluye la clase ServerSocket, la cual implementa un socket el cual los servidores pueden utilizar para escuchar y aceptar peticiones de conexión de clientes. Nuestro objetivo será conocer cómo utilizar las clases Socket y ServerSocket. Por otra parte, si intentamos conectar a través de la Web, la clase URL y clases relacionadas (URLConnection, URLEncoder) son probablemente más apropiadas que las clases de sockets. Pero de hecho, las clases URL no son más que una conexión a un nivel más alto a la Web y utilizan como parte de su implementación interna los sockets. 2.2.1. Modelo de comunicaciones con Java. El modelo de sockets más simple es:

- El servidor establece un puerto y espera durante un cierto tiempo (timeout en segundos), a que el cliente establezca la conexión. Cuando el cliente solicite una conexión, el servidor abrirá la conexión socket con el método accept(). - El cliente establece una conexión con la máquina host a través del puerto que se designe en port#. - El cliente y el servidor se comunican mediante streams InputStream y OutputStream.

2.2.2. Apertura de Sockets. Si estamos programando un CLIENTE, el socket se abre de la forma: Socket miCliente;

try {

miCliente = new Socket( "maquina",numeroPuerto );

} catch( IOException e ) {

}

Donde maquina es el nombre de la máquina en donde estamos intentando abrir la conexión y numeroPuerto es el puerto (un número) del servidor que está corriendo sobre el cual nos queremos conectar. Cuando se

Page 224: Fundamentos de JAVA

Fundamentos de Java /224

selecciona un número de puerto, se debe tener en cuenta que los puertos en el rango 0-1023 están reservados. Para las aplicaciones que se desarrollen, debemos usar valores por encima del 1023. Si estamos programando un SERVIDOR, la forma de apertura del socket es la que muestra el siguiente ejemplo: ServerSocket miServicio;

try {

miServicio = new ServerSocket( numeroPuerto );

} catch( IOException e ) {

}

A la hora de la implementación de un servidor también necesitamos crear un objeto socket desde el servidor para que esté atento a las conexiones que le puedan realizar clientes potenciales y poder aceptar esas conexiones: Socket socketServicio = null;

try {

socketServicio = miServicio.accept();

} catch( IOException e ) {

}

2.2.3. Creación de Streams. En la parte CLIENTE de la aplicación, se puede utilizar la clase DataInputStream para crear un stream de entrada que esté listo a recibir todas las respuestas que el servidor le envíe. DataInputStream entrada;

try {

entrada = new DataInputStream( miCliente.getInputStream() );

} catch( IOException e ) {

}

La clase DataInputStream permite la lectura de líneas de texto y tipos de datos primitivos de Java de un modo altamente portable; dispone de métodos para leer todos esos tipos como: read(), readChar(), readInt(), readDouble() y readLine(). Deberemos utilizar la función que creamos necesaria dependiendo del tipo de dato que esperemos recibir del servidor. En el lado del SERVIDOR, también usaremos DataInputStream, pero en este caso para recibir las entradas que se produzcan de los clientes que se hayan conectado: DataInputStream entrada;

try {

entrada = new DataInputStream( socketServicio.getInputStream() );

} catch( IOException e ) {

}

En el lado del CLIENTE, podemos crear un stream de salida para enviar información al socket del servidor utilizando las clases PrintStream o DataOutputStream: PrintStream salida;

try {

salida = new PrintStream( miCliente.getOutputStream() );

} catch( IOException e ) {

}

La clase PrintStream tiene métodos para la representación textual de todos los datos primitivos de Java. Sus métodos write y println() tienen una especial importancia en este aspecto. No obstante, para el envío de información al servidor también podemos utilizar DataOutputStream: DataOutputStream salida;

try {

salida = new DataOutputStream( miCliente.getOutputStream() );

} catch( IOException e ) {

}

La clase DataOutputStream permite escribir cualquiera de los tipos primitivos de Java, muchos de sus métodos escriben un tipo de dato primitivo en el stream de salida. De todos esos métodos, el más útil quizás sea writeBytes(). En el lado del SERVIDOR, podemos utilizar la clase PrintStream para enviar información al cliente: PrintStream salida;

try {

Page 225: Fundamentos de JAVA

Fundamentos de Java /225

salida = new PrintStream( socketServicio.getOutputStream() );

} catch( IOException e ) {

}

Pero también podemos utilizar la clase DataOutputStream como en el caso de envío de información desde el cliente. 2.2.4. Cierre de Sockets. Siempre deberemos cerrar los canales de entrada y salida que se hayan abierto durante la ejecución de la aplicación. En la parte del cliente: try {

salida.close();

entrada.close();

miCliente.close();

} catch( IOException e ) {

}

Y en la parte del servidor: try {

salida.close();

entrada.close();

socketServicio.close();

miServicio.close();

} catch( IOException e ) {

}

Es importante destacar que el orden de cierre es relevante. Es decir, se deben cerrar primero los streams relacionados con un socket antes que el propio socket, ya que de esta forma evitamos posibles errores de escrituras o lecturas sobre descriptores ya cerrados. 2.2.5. Ejemplo de aplicación Cliente/Servidor mediante sockets. En este ejemplo, implementaremos dos procesos. Uno, que actuará como servidor, recibirá mensajes desde aplicaciones clientes y los mostrará por consola. El otro proceso, que actuará como cliente, enviará mensajes al servidor. Cuando el servidor haya mostrado más de 100 mensajes finalizará su ejecución. La aplicación cliente se basa en una clase que en su constructor establece una conexión con la aplicación servidora. Si la conexión se establece, inicia un proceso para leer líneas de texto por teclado y enviarlas al servidor. Si se introduce una línea en blanco o falla la conexión con el servidor, finaliza la aplicación. El código de la clase cliente es el siguiente: import java.io.DataOutputStream;

import java.io.IOException;

import java.io.OutputStream;

import java.net.Socket;

public class Cliente {

final int PUERTO = 54321; // el puerto que usa la aplicación servidora

final String URLSERVIDOR = "localhost"; // la URL de la aplicación servidora

Socket socket; // el socket cliente que va a enviar los mensajes al servidor

// Constructor

public Cliente() {

try {

// Al instanciar un socket, se establece la conexión con el cliente o se lanza una excepcion si no se pudo.

socket = new Socket(URLSERVIDOR, PUERTO);

// Este método procesa los mensajes del usuario y los envía al proceso servidor

leerEnviarMensajes();

// Al cerrar un socket se da aviso al socket servidor, y ya no es reutilizable

socket.close();

} catch (IOException ex) {

System.out.println ("error al conectar con el servidor");

}

}

private void leerEnviarMensajes() {

// Se procesan mensajes de un usuario hasta que no introduce nada y salta de línea.

String mensaje = “”; // string con el texto del mensaje

Page 226: Fundamentos de JAVA

Fundamentos de Java /226

OutputStream aux = null; // canal para enviar mensajes al servidor

try {

aux = socket.getOutputStream(); // obtenemos un canal con el socket servidor

DataOutputStream writer = new DataOutputStream(aux);

do {

mensaje = System.out.console().readLine(); // lee una línea por teclado

if ( ! mensaje.equals("") ) { // lo enviamos al servidor si contiene algo

writer.writeUTF(mensaje); // se envía el mensaje

writer.flush(); // y se fuerza el volcado del canal

}

} while ( ! mensaje.equals(“”) );

} catch (IOException ex) {

System.out.println ( "Se perdió conexión con el servidor. Se finaliza el programa");

}

}

}

De este código destacaremos lo siguiente: - En el constructor de la clase Socket se pasa el host y puerto de la aplicación servidora. Si no se puede establecer la conexión se lanza una IOException. - Para enviar datos al socket servidor, debemos obtener un stream de la clase OutputStream con el método getOutputStream(). - Para finalizar la conexión debemos invocar el método close() del socket cliente, el cual puede lanzar una IOException.

La aplicación servidora estará a la escucha de cualquier petición de un cliente. Por cada cliente se lanza un hilo que lee sus mensajes y los procesa; a cada mensaje se le adjunta el host del socket cliente. Una vez leídos 100 mensajes, la aplicación cierra todas las conexiones y finaliza su ejecución. El código de la clase servidora es el siguiente: import java.io.BufferedReader;

import java.io.DataInputStream;

import java.io.FileReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.logging.Level;

import java.util.logging.Logger;

public class Servidor implements Runnable {

final int NUMMENSAJES = 100; // número máximo de mensajes

final int PUERTO = 54321; // el puerto que usa la aplicación servidora

ServerSocket oyente; // éste es el socket oyente

int contadorMensajes = 0; // contador de mensajes

// Clase para lanzar un hilo que procese los mensajes de un determinado socket cliente

class HiloParaCliente extends Thread {

Socket scliente; // el socket cliente

public HiloParaCliente(Socket s) {

scliente = s;

}

public void run() { // mantiene la conexión con un cliente y procesa lo que nos envíe.

String host = scliente.getInetAddress().getHostName(); // el nombre del host cliente

try {

InputStream aux = scliente.getInputStream();

DataInputStream reader = new DataInputStream(aux);

while ( ! oyente.isClosed() ) {

if ( (++contadorMensajes) > NUMMENSAJES) {

// se cierra el socket servidor

try {oyente.close();} catch (IOException ex) {}

Page 227: Fundamentos de JAVA

Fundamentos de Java /227

} else {

// Se muestra el host cliente y el mensaje recibido

System.out.println(host + “: “+ reader.readUTF() ;

}

}

} catch (IOException ex) {

System.out.println ("Error en hilo del cliente " + host);

} finally {

try {scliente.close();} catch (IOException ex) {}

}

}

}

// Constructor de la clase

public Servidor () {

initComponents();

try {

oyente = new ServerSocket(PUERTO);

System.out.println("A la escucha");

new Thread(this).start(); // Se lanza el hilo de escuchar clientes

} catch (IOException ex) {

System.out.println ("Falló la escucha");

}

}

public void run() { // proceso de hilo para escuchar clientes

while ( ! oyente.isClosed() ) {

try {

Socket cliente = oyente.accept(); // el método espera hasta recibir la petición de un cliente

new HiloParaCliente(cliente).start(); // se lanza un hilo para atender al cliente

} catch (IOException ex) {

System.out.println ("Se rechazó un cliente");

} catch (SocketException ex) { // ocurre cuando el socket oyente se cierra

}

}

}

}

De este código destacaremos lo siguiente: - En el constructor de la clase ServerSocket se pasa sólo el puerto de la aplicación servidora. Si el puerto ya está ocupado o no se puede establecer se lanza una IOException. - Para aceptar una conexión cliente se invoca el método accept() del socket servidor. Este método permanece bloqueado hasta recibir una petición, y en ese caso retorna un objeto Socket con la configuración del socket cliente. - Para leer los datos enviados por el cliente se obtiene primero un stream de la clase InputStream mediante el método getInputStream() del socket cliente. - Al cerrar el socket servidor, se cierran todas sus conexiones abiertas, y el método accept(), si estaba bloqueado a la espera de una conexión, lanzará una SocketException.

3. Trabajando con datagramas.

3.1. Clientes Datagrama.

La clase DatagramPacket, junto con la clase DatagramSocket, son las que se utilizan para la implementación del protocolo UDP (User Datagram Protocol). La programación para uso del protocolo UDP se diferencia de la programación para utilizar el protocolo TCP en que no existe el concepto de ServerSocket para los datagramas y que es el programador el que debe construirse sus propios paquetes a enviar por UDP. Para enviar datos a través de UDP, hay que construir un objeto de tipo DatagramPacket y enviarlo a través de un objeto DatagramSocket, y al revés para recibirlos, es decir, a través de un objeto DatagramSocket se recoge el objeto DatagramPacket. Toda la información respecto a la dirección, puerto y datos, está contenida en el paquete.

Page 228: Fundamentos de JAVA

Fundamentos de Java /228

Para enviar un paquete, primero se construye ese paquete con la información que se desea transmitir, luego se almacena en un objeto DatagramSocket y, finalmente se invoca el método send() sobre ese objeto. Para recibir un paquete, primero se construye un paquete vacío, luego se le presenta a un objeto DatagramSocket para que almacene allí el resultado de la ejecución del método receive() sobre ese objeto. Hay que tener en cuenta que el hilo de ejecución encargado de todo esto estará bloqueado en el método receive() hasta que un paquete físico de datos se reciba a través de la red; este paquete físico será el que se utilice para rellenar el paquete vacío que se había creado. También hay que tener cuidado cuando se pone a escuchar a un objeto DatagramSocket en un puerto determinado, ya que va a recibir los datagramas enviados por cualquier cliente. Es decir, que si los mensajes enviados por los clientes están formados por múltiples paquetes; en la recepción pueden llegar paquetes entremezclados de varios clientes y es responsabilidad de la aplicación el ordenarlos.

3.2. La clase «DatagramPacket».

Para la clase DatagramPacket se dispone de dos constructores, uno utilizado cuando se quieren enviar paquetes y el otro se usa cuando se quieren recibir paquetes. Ambos requieren que se les proporcione un array de bytes y la longitud que tiene. En el caso de la recepción de datos, no es necesario nada más, los datos que se reciban se depositarán en el array; aunque, en el caso de que se reciban más datos físicos de los que soporta el array, el exceso de información se perderá y se lanzará una excepción de tipo IllegalArgumentException, que a pesar de que no sea necesaria su captura, siempre es bueno recogerla. Cuando se construye el paquete a enviar, es necesario colocar los datos en el array antes de llamar al método send(); además de eso, hay que incluir la longitud de ese array y, también se debe proporcionar un objeto de tipo InetAddress indicando la dirección de destino del paquete y el número del puerto de ese destino en el cual estará escuchando el receptor del mensaje. Es decir, que la dirección de destino y el puerto de escucha deben ir en el paquete, al contrario de lo que pasaba en el caso de TCP que se indicaban en el momento de construir el objeto Socket. El tamaño físico máximo de un datagrama es 65535 bytes, y teniendo en cuenta que hay que incluir datos de cabecera, esa longitud nunca está disponible para datos de usuario, sino que siempre es algo menor. La clase DatagramPacket proporciona varios métodos para poder extraer los datos que llegan en el paquete recibido. La información que se obtiene con cada método coincide con el propio nombre del método, aunque hay algunos casos en que es necesario saber interpretar la información que proporciona ese mismo método. El método getAddress() devuelve un objeto de tipo InetAddress que contiene la dirección del host remoto. El saber cuál es el ordenador de origen del envío depende de la forma en que se haya obtenido el datagrama. Si ese datagrama ha sido recibido a través de Internet, la dirección representará al ordenador que ha enviado el datagrama (el origen del datagrama); pero si el datagrama se ha construido localmente, la dirección representará al ordenador al cual se intenta enviar el datagrama (el destino del datagrama). De igual modo, el método getPort() devuelve el puerto desde el cual ha sido enviado el datagrama, o el puerto a través del cual se enviará, dependiendo de la forma en que se haya obtenido el datagrama. El método getData() devuelve un array de bytes que contiene la parte de datos del datagrama, ya eliminada la cabecera con la información de encaminamiento de ese datagrama. La forma de interpretar ese array depende del tipo de datos que contenga. El método getLength() devuelve el número de bytes que contiene la parte de datos del datagrama, y el método getOffset() devuelve la posición en la cual empieza el array de bytes dentro del datagrama completo.

3.3. La clase «DatagramSocket».

Un objeto de la clase DatagramSocket puede utilizarse tanto para enviar como para recibir un datagrama. La clase tiene tres constructores. Uno de ellos se conecta al primer puerto libre de la máquina local; el otro permite especificar el puerto a través del cual operará el socket; y el tercero permite especificar un puerto y una dirección para identificar a una máquina concreta. Independientemente del constructor que se utilice, el puerto desde el cual se envíe el datagrama, siempre se incluirá en la cabecera del paquete. Normalmente, la parte del servidor utilizará el constructor que permite indicar el puerto concreto a usar, ya que si no, la parte cliente no tendría forma de conocer el puerto por el cual le van a llegar los datagramas. La parte cliente puede utilizar cualquier constructor, pero por flexibilidad, lo mejor es utilizar el constructor que deja que el sistema seleccione uno de los puertos disponibles. El servidor debería entonces comprobar cuál es el puerto que se está utilizando para el envío de datagramas y enviar la respuesta por ese puerto.

Page 229: Fundamentos de JAVA

Fundamentos de Java /229

A diferencia de esta posibilidad de especificar el puerto, o no hacerlo, no hay ninguna otra diferencia entre los sockets datagrama utilizados por cliente y servidor. Para enviar un datagrama hay que invocar al método send() sobre un socket datagrama existente, pasándole el objeto paquete como parámetro. Cuando el paquete es enviado, la dirección y número de puerto del ordenador origen se coloca automáticamente en la porción de cabecera del paquete, de forma que esa información pueda ser recuperada en el ordenador destino del paquete. Para recibir datagramas, hay que instanciar un objeto de tipo DatagramSocket, conectarse a un puerto determinado e invocar al método receive() sobre ese socket. Este método bloquea el hilo de ejecución hasta que se recibe un datagrama, por lo que si es necesario hacer alguna cosa durante la espera, hay que invocar al método receive() en su propio hilo de ejecución. Si se trata de un servidor, hay que conectarse con un puerto específico. Si se trata de un cliente que está esperando respuestas de un servidor, hay que escuchar en el mismo puerto que fue utilizado para enviar el datagrama inicial. Si se envía un datagrama a un puerto anónimo, se puede mantener el socket abierto que fue utilizado en el envío del primer datagrama y utilizar ese mismo socket para esperar la respuesta. También se puede invocar al método getLocalPort() sobre el socket antes de cerrarlo, de forma que se pueda saber y guardar el número del puerto que se ha empleado; de este modo se puede cerrar el socket original y abrir otro socket en el mismo puerto en el momento en que se necesite. Si el datagrama inicial es enviado a un puerto determinado, se puede utilizar el mismo socket para recibir la respuesta, o cerrar el socket original y abrir uno nuevo sobre el mismo puerto. Para responder a un datagrama, hay que obtener la dirección del origen y el número de puerto a través del cual fue enviado el datagrama, de la cabecera del paquete y luego, colocar esta información en el nuevo paquete que se construya con la información a enviar como respuesta. Una vez pasada esta información a la parte de datos del paquete, se invoca al método send() sobre el objeto DatagramSocket existente, pasándole el objeto paquete como parámetro. La dirección y número de puerto de la máquina que está enviando será incorporada automáticamente a la cabecera del paquete en el momento de ser enviado. Es importante tener en cuenta que los números de puerto TCP y UDP no están relacionados. Se puede utilizar el mismo número de puerto en dos procesos si uno se comunica a través de protocolo TCP y el otro lo hace a través de protocolo UDP. Es muy común que los servidores utilicen el mismo puerto para proporcionar servicios similares a través de los dos protocolos en algunos servicios estándar, como puede ser el eco.

3.4. Ejemplo de programa de eco con datagramas.

Este ejemplo realiza una prueba del servicio Echo contra un servidor, enviando una línea de texto al puerto estándar de este servicio, el puerto 7, empleando un datagrama UDP. El código de la clase cliente es como sigue: import java.net.*;

import java.io.*;

import java.util.*;

class ClienteEco {

public static void main( String[] args ) {

String servidor = "www.midominio.com"; // servidor

int puerto = 7; // puerto eco

String mensajeUdp = “Mensaje UDP” // los datos a enviar

try {

// Convertimos el mensaje en un array de bytes

byte[] mensajeUdp = mensajeUdp.getBytes();

// Obtenemos la dirección IP del servdor

InetAddress dirIp = InetAddress.getByName( servidor );

// Creamos el paquete que se va a enviar al puerto

DatagramPacket paquete = new DatagramPacket( mensajeUdp, mensajeUdp.length, dirIp, puerto );

// Abrimos un socket datagrama para enviarle el mensaje y lo enviamos

DatagramSocket socketDgrama = new DatagramSocket();

socketDgrama.send( paquete );

// Sobreescrimos el mensaje en el paquete para confirmar que el eco es realmente lo que llega

byte[] arrayDatos = paquete.getData();

for( int cnt=0; cnt < paquete.getLength(); cnt++ )

arrayDatos[cnt] = (byte) 'x';

Page 230: Fundamentos de JAVA

Fundamentos de Java /230

// Escribimos esta versión del mensaje

System.out.println( new String( paquete.getData() ) );

// Ahora recibimos el eco en ese mismo paquete, de forma que sobreescriba las "x"

socketDgrama.receive( paquete );

// Presentamos en pantalla el eco

System.out.println( new String( paquete.getData() ) );

// Se cierra el socket

socketDgrama.close();

} catch( UnknownHostException e ) {

e.printStackTrace();

System.out.println( "Debes estar conectado para que esto funcione bien." );

} catch( SocketException e ) {

e.printStackTrace();

} catch( IOException e ) {

e.printStackTrace();

}

}

}

El código de la clase servidora es como sigue: import java.net.*;

import java.io.*;

import java.util.*;

class ServidorEcoUdp extends Thread {

public static void main( String[] args ) {

// Se instancia y lanza un objeto servidor UDP para escuchar el puerto 7

new ServidorEcoUdp().start();

}

public void run() {

try {

// Se instancia un objeto sobre el puerto 7

DatagramSocket socketDgrama = new DatagramSocket( 7 );

System.out.println( "Servidor Udp escuchando el 7" );

// Entramos en bucle infinito ecuchando el puerto. Cuando se produce una llamada,

// se lanza un hilo de ejecución de ConexionEchoUdp para atenderla

while( true )

// Limitamos la cadena de eco a 1024 bytes

DatagramPacket paquete = new DatagramPacket( new byte[1024],1024 );

// Nos quedamos parados en el receive(), y devolvemos un socket cuando se recibe la llamada.

// Este socket es el que se pasa como parámetro al nuevo hilo de ejecución que se crea

socketDgrama.receive( paquete );

new ConexionEcoUdp( paquete );

}

} catch( IOException e ) {

e.printStackTrace();

}

}

}

// Esta clase se utiliza para lanzar un hilo de ejecución que atienda la llamada recibida a través del puerto 7.

class ConexionEchoUdp extends Thread {

DatagramPacket paquete;

// Constructor

public ConexionEchoUdp( DatagramPacket paquete ) {

System.out.println( "Recibida una llamada en el puerto 7" );

this.paquete = paquete;

// Trabajamos por debajo de la prioridad de los otros puertos

setPriority( NORM_PRIORITY-1 );

// Se arranca el hilo y se pone a correr

start();

}

Page 231: Fundamentos de JAVA

Fundamentos de Java /231

public void run() {

System.out.println( "Lanzado el hilo UDP de atencion del puerto 7" );

// Se crea el paquete de eco basándonos en los datos del paquete que se ha recibido como parámetro

DatagramPacket paqueteEnvio = new DatagramPacket( paquete.getData(), paquete.getLength(),

paquete.getAddress(), paquete.getPort() );

// Declaramos el socket datagrama

DatagramSocket socketDgrama = null;

try {

// Abrimos un socket datagrama

socketDgrama = new DatagramSocket();

// Se utiliza el nuevo socket datagrama para enviar el mensaje y cerrar el socket

socketDgrama.send( paqueteEnvio );

socketDgrama.close();

System.out.println("Socket UDP cerrado." );

e.printStackTrace();

}catch( UnknownHostException e ) {

datagramSocket.close();

System.out.println("Socket UDP cerrado." );

e.printStackTrace();

}catch( SocketException e ) {

datagramSocket.close();

System.out.println("Socket UDP cerrado." );

e.printStackTrace();

}catch( IOException e ) {

datagramSocket.close();

System.out.println("Socket UDP cerrado." );

e.printStackTrace();

}

}

}