Vous êtes sur la page 1sur 210

Escuela Politécnica de Ingenierı́a, Gijón

Grado en Ingenierı́a Informática


en Tecnologı́as de la Información

Introducción a la Programación
Jorge Dı́ez, Oscar Luaces, Juan José del Coz
{jdiez,oluaces,juanjo}@uniovi.es

Departamento de Informática
Universidad de Oviedo en Gijón

Curso 2018-2019
Contenidos

1. Conceptos básicos de Programación 1


1.1. La Programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1. ¿Qué es la Programación? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.2. ¿Qué es un programa de ordenador? . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Paradigmas de programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3. Lenguajes de programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3.1. Definición, sintaxis y semántica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3.2. Tipo de lenguajes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3.3. El lenguaje Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4. Hola, Mundo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4.1. Una clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4.2. El método main() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4.3. Imprimir en pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4.4. Comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5. Propiedades fundamentales de los programas . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6. Ciclo de vida de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.6.1. Análisis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.6.2. Diseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.7. Pruebas de software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.7.1. Tipos de pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.7.2. Requisitos funcionales y casos de prueba . . . . . . . . . . . . . . . . . . . . . . . . 14
1.7.3. Especificación formal de un programa . . . . . . . . . . . . . . . . . . . . . . . . . 15

2. Tipos de datos básicos 17


2.1. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2. Tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.1. Tipos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3. Tipos enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3.1. Operadores matemáticos con enteros . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4. Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5. Identificadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.6. E/S formateada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.6.1. Clase Scanner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.6.2. Impresión con formato en consola: printf() . . . . . . . . . . . . . . . . . . . . . . 25
2.7. Tipos reales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.7.1. Operadores matemáticos con reales . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.8. Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.8.1. Constantes sin nombre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.9. Asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.10. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.10.1. Precedencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.10.2. Asociatividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.11. Conversiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.11.1. Operador de conversión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

iii
2.11.2. Conversiones automáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

3. Clases y Objetos 39
3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2. Clases y objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3. Atributos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.4. Métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.4.1. Métodos que devuelven un resultado . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.4.2. Métodos set() y get() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.5. Creación y uso de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.5.1. Crear un objeto: el operador new . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.5.2. Usar un objeto: el operador punto . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.5.3. Llamadas a métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.6. Encapsulación de la información: acceso público y privado . . . . . . . . . . . . . . . . . . 49
3.7. Documentación Javadoc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.7.1. Documentación de una clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.7.2. Documentación de atributos y métodos . . . . . . . . . . . . . . . . . . . . . . . . 51
3.8. Reutilización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.8.1. Clase Fecha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.9. Representación en memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.9.1. Variables de los tipos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.9.2. Objetos y tipos referenciados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.9.3. Paso de parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.9.4. Métodos que reciben objetos de la propia clase . . . . . . . . . . . . . . . . . . . . 59

4. Programación estructurada 61
4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.2. Sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.3. Condiciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.3.1. El tipo boolean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.3.2. Operadores lógicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.3.3. Operadores relacionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.3.4. Operadores de comparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.4. Sentencia if-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.4.1. El operador ( ? : ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.4.2. Sentencias if-else anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.5. Sentencia switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.6. Bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.7. Bucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.7.1. Operadores de asignación aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.7.2. Las sentencias break y continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.8. Bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.8.1. Operadores de pre- y pos- incremento y decremento . . . . . . . . . . . . . . . . . 88
4.8.2. Otras consideraciones sobre for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
4.9. Bucle do-while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.10. Esquemas iterativos tı́picos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.10.1. Secuencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
4.10.2. Tratamientos de secuencias de elementos . . . . . . . . . . . . . . . . . . . . . . . . 94
4.10.3. Búsquedas asociativas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.10.4. Bucles anidados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
4.11. Pruebas Funcionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.11.1. Pruebas de caja blanca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4.11.2. Pruebas de caja negra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
4.11.3. Ejemplos de casos de pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
4.12. Ámbito y tiempo de vida de una variable . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

iv
5. Vectores 111
5.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
5.1.1. Estructuras de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
5.2. Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.2.1. ¿Qué es un vector? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.2.2. Declaración, creación e inicialización de un vector . . . . . . . . . . . . . . . . . . . 115
5.3. Representación de un vector en memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
5.4. Uso de un vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
5.4.1. Operador corchete [ ] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
5.4.2. El atributo length . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
5.4.3. Clase Temperaturas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
5.5. Propiedades de los vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
5.6. Algoritmos simples con vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
5.6.1. Tratamientos secuenciales de vectores . . . . . . . . . . . . . . . . . . . . . . . . . 126
5.6.2. Búsqueda lineal en vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
5.7. Vectores de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
5.8. Cadenas de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
5.8.1. Clase String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
5.8.2. Modificar cadenas de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5.8.3. Los argumentos de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.9. Matrices y vectores multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
5.9.1. Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
5.9.2. Representación en memoria de una matriz . . . . . . . . . . . . . . . . . . . . . . . 141
5.9.3. Uso de una matriz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.9.4. Programas simples con matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
5.9.5. Vectores multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

6. Introducción a la Programación Orientada a Objetos 149


6.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
6.2. Construcción e inicialización de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
6.2.1. Constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
6.2.2. Selección del constructor al usar new . . . . . . . . . . . . . . . . . . . . . . . . . . 152
6.2.3. Constructor por defecto y por copia . . . . . . . . . . . . . . . . . . . . . . . . . . 152
6.2.4. Otros constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
6.2.5. ¿Cómo funciona la creación de objetos en clases sin constructores? . . . . . . . . . 154
6.2.6. Valores iniciales de los atributos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
6.2.7. Tareas de los constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
6.3. Destrucción de objetos: el Recolector de basura . . . . . . . . . . . . . . . . . . . . . . . . 158
6.3.1. El método finalize() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
6.4. Representación en memoria de clases y objetos . . . . . . . . . . . . . . . . . . . . . . . . 160
6.4.1. Dos tipos de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
6.4.2. ¿Cómo se mantiene en memoria una clase y sus atributos, métodos y objetos? . . . 161
6.5. Elementos estáticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
6.5.1. Acceso a elementos estáticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
6.6. El objeto this . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
6.6.1. Usos del objeto this . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
6.7. Métodos sobrecargados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
6.7.1. Resolución de sobrecarga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
6.8. Relaciones entre clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
6.8.1. Composición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
6.9. La clase Object: concepto de Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
6.9.1. Método equals() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
6.9.2. Método toString() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
6.10. Ejemplos de clases completas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
6.10.1. La clase Diccionario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
6.10.2. La clase Fecha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184

v
6.10.3. Otra implementación de la clase Fecha . . . . . . . . . . . . . . . . . . . . . . . . . 190

A. Palabras Reservadas 193

B. Constantes 195

C. Operadores en Java 197


C.1. Operadores Aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
C.2. Operadores Relacionales y de Comparación . . . . . . . . . . . . . . . . . . . . . . . . . . 198
C.3. Operadores Lógicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
C.4. Operadores de Bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
C.5. Operadores de Asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
C.6. Otros Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

D. Precedencia y Asociatividad 201

E. Imprimir en consola: printf() 203


E.1. Especificadores de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
E.2. Secuencias de escape . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204

vi
Tema 1

Conceptos básicos de Programación


Objetivos

Comprender los objetivos y principios de la Programación.

Entender las caracterı́sticas principales de los paradigmas y los lenguajes de programación.

Conocer las fases en la construcción de un programa.

Diferenciar los conceptos de algoritmo y programa.

Comprender las propiedades fundamentales que un programa debe cumplir.

Conocer el objetivo de las pruebas de software, qué son los requisitos funcionales y cómo se especifica
formalmente un programa.

Ser capaz de escribir programas en Java que impriman en pantalla.

1.1. La Programación
1.1.1. ¿Qué es la Programación?
Como esta asignatura se llama Introducción a la Programación, lo primero que debemos tener claro
es qué es la Programación.

definición Programación: Disciplina que se ocupa de estudiar y definir las técnicas y metodologı́as más
apropiadas para la elaboración de programas informáticos.

Dado que esta es la primera asignatura de programación del grado, la base para todas las demás
asignaturas del bloque de Programación del Módulo de Software de Aplicaciones, se verán durante el
curso una introducción a algunas de las técnicas más elementales para la producción de programas.
Estudiaremos muchos aspectos de los programas:

1. las caracterı́sticas deseables que deben tener,

2. la forma de abordar su realización,

3. las fases en las que se suele dividir su elaboración,

4. los lenguajes en los que se pueden escribir, y los tipos de lenguajes que existen,

5. los diferentes paradigmas de programación que se pueden utilizar, y

6. estudiaremos las bases de uno de ellos, la Programación Orientada a Objetos, en un lenguaje


concreto, Java.

1
1.1.2. ¿Qué es un programa de ordenador?
Es decir, vamos a estudiar cómo se hace un programa. Casi todo el mundo hoy en dı́a usa programas
de ordenador, sin embargo la mayorı́a de la gente no ha hecho nunca ninguno. Hay notable diferencia
entre la percepción de lo que los programas desde el punto de vista de los usuarios, que se fijan en la
funcionalidad, a la visión de los programadores, que saben cómo son internamente. Eso es lo que vamos
a estudiar en esta asignatura, qué es un programa, cómo es, cómo se elabora y qué caracterı́sticas debe
cumplir. Vamos a pensar en los programas desde el punto de vista del programador, no del usuario.

definición Programa: Texto con instrucciones que deben poder ser entendidas y ejecutadas por un ordenador.

Analizando esa definición, podemos resaltar varios aspectos:

Es un texto legible por un programador que conozca el lenguaje en el que está escrito (lenguaje
de programación). A este texto, también se le denomina código fuente.
Consta de un conjunto de instrucciones precisas que sirven para realizar la tarea que hace el
programa.
Puede ser ejecutado por un ordenador (gracias a otros programas que lo traducen o interpretan
llamados compiladores e intérpretes).

Se puede observar un ejemplo en la Sección 1.4, en concreto el programa “Hola, Mundo” escrito en
Java. De momento es imposible que se entienda por qué tiene exactamente esas palabras, pero se puede
apreciar que es un texto legible.

1.2. Paradigmas de programación

definición Paradigma de Programación: Representa un enfoque particular o filosofı́a para la construcción


de un programa.

Es importante resaltar que:

no existe un paradigma mejor que los demás,


cada uno tiene sus caracterı́sticas,
todos tienen ventajas y desventajas, y
sin embargo, hay situaciones donde un paradigma resulta más apropiado.

En 1o se estudian dos de ellos, y en 2o hay un asignatura dedicada al estudio de otros (Tecnologı́as


y Paradigmas de la Programación). Pero citemos cuáles son los paradigmas más importantes:

1. Imperativo: concibe la programación como una secuencia instrucciones que se ejecutan paso a
paso (algoritmo) y van cambiando el estado de los datos del programa hasta alcanzar la solución
(ejemplo, lenguaje C).
Procedimental, los programas se resuelven descomponiéndolos en procedimientos o módulos
(C, Pascal).
2. Declarativo: se basa en describir cómo es la solución, no en cómo se hace paso a paso. En otras
palabras, se centra en describir las propiedades de la solución buscada, sin indicar el conjunto de
instrucciones necesario para encontrar esa solución.

Funcional: entiende la programación como la evaluación de funciones matemáticas (Lisp).


Lógico, se basa en definir reglas lógicas que, combinadas con un motor de inferencia, resuelven
los problemas (Prolog).

2
3. Orientado a Objetos: un programa se compone de objetos que cooperan y se comunican entre
sı́ (Smalltalk, Java, C++).

Los paradigmas Imperativo y Procedimental suelen entenderse juntos, la mayor parte de los lenguajes
imperativos siguen un enfoque modular. Cada lenguaje de programación suele seguir un cierto paradigma,
aunque es posible que un mismo lenguaje pueda combinar varios de ellos. Por ejemplo, el C++ puede
combinar la programación imperativa-procedimental y la orientada a objetos. En esta asignatura nos
centraremos en la Programación Orientada a Objetos, combinada con una Programación Imperativa.

Tabla 1.1: Caracterı́sticas de cada Paradigma de Programación

Paradigma Concepto Programa Ejecución Resultado


Imperativo Instrucción Secuencia de Ejecución de Estado final de la
instrucciones instrucciones memoria
Funcional Función Colección de Evaluación de Valor de la
Funciones funciones función principal
Lógico Predicado Fórmulas lógicas Prueba lógica Fracaso o éxito de
y teoremas la prueba
Orientado a Objeto Colección de Intercambio de Estado final de
objetos clases de objetos mensajes entre los objetos
objetos

En la Tabla 1.1 están detalladas las caracterı́sticas que diferencian los distintos paradigmas citados
anteriormente.
Para ver la diferencia entre los paradigmas Declarativo e Imperativo vamos a ver un simple ejemplo.
Se trata de hacer un programa que averigüe si, dados dos individuos X e Y, sepamos si X es superior
de Y. En el enfoque declarativo se indica lo que significa ser superior. Para que X sea superior de Y, o
bien es su jefe directo o algunos de los empleados directos de X son superiores de Y (puede haber varios
jefes intermedios entre X e Y). Lo importante es que se aprecie que lo que expresa el programa no es
ninguna instrucción directa, sino más bien el conocimiento de lo que significa ser superior dentro de una
organización jerárquica. En contraposición, en el caso imperativo lo que se hace es detallar explı́citamente
las operaciones que hay que hacer para averiguar si X es superior o no de Y. No importa que ahora no se
entienda qué significan las instrucciones, lo que interesa es que se vea la diferencia entre ambos enfoques.

Programa Declarativo - Lenguaje Prolog


1 superior(X,Y) :- jefe(X,Y).
2 superior(X,Y) :- jefe(X,Z), superior(Z,Y).

Programa Imperativo - Lenguaje C++


1 bool superior(individuo X, individuo Y, conjunto C) {
2 if ( jefe(X,Y) ) return true;
3 else {
4 C = sacar(C,X); C = sacar(C,Y);
5 while ( !vacio(C) ) {
6 Z = buscarEn(C);
7 if ( jefe(X,Z) && superior(Z,Y) ) return true;
8 C = sacar(C,Z);
9 }
10 return false;
11 }
12 }

3
1.3. Lenguajes de programación
1.3.1. Definición, sintaxis y semántica
La forma de aplicar esos paradigmas de programación y de escribir nuestros programas es empleando
un lenguaje de programación. Lo primero que necesitamos es ver cómo podemos escribir nuestros progra-
mas de forma que el ordenador los pueda ejecutar. Veremos la forma que tiene el texto del programa y
las instrucciones que podremos usar. Todo ello viene dado por el lenguaje de programación que usemos.

definición Lenguaje de Programación: Idioma formado por un conjunto de sı́mbolos y reglas sintácticas y
semánticas que definen la estructura y el significado de las instrucciones de que consta el lenguaje.

En realidad no deja de ser un “lenguaje”, como lo puedan ser el español o el inglés, pero:

son mucho más limitados, constan de muchı́simas menos palabras (generalmente unas pocas dece-
nas),

son sintácticamente mucho más estrictos, de hecho un programa no puede tener ni un solo error
sintáctico, y

no permiten las ambigüedades del lenguaje natural, cada instrucción tiene una semántica clara y
precisa.

Conocer un lenguaje de programación significa precisamente eso, saber su sintaxis, las instrucciones
que contiene y la semántica de todas ellas. Al final, conocer un lenguaje consiste en aprender todas sus
reglas (sintácticas y semánticas) y aplicarlas correctamente. Y estas reglas son muchı́simo más estrictas
que en el lenguaje hablado. Se puede decir “El perro ladró” o “Ladró el perro” y el oyente lo entenderá en
cualquier caso. Sin embargo cuando escribamos un programa en Java, para declarar un clase Cı́rculo,
no podremos escribir “class Cı́rculo public”, ya que la sintaxis correcta es “public class Cı́rculo”. Los
lenguajes de programación son más estrictos que el lenguaje natural.
Llegar a dominar un lenguaje implica más cosas que conocer todas sus reglas sintácticas y semánticas,
ya que los lenguajes de programación vienen acompañados de muchas funcionalidades que lleva tiempo
conocer (eso solamente se adquiere con la experiencia de un programador con un lenguaje).
La realidad es que hay cientos de lenguajes, casi se podrı́a decir que cada semana surge algún lengua-
je nuevo, lo que puede sugerir muchas preguntas. ¿Por qué hay tantos? ¿Es necesario profesionalmente
conocer muchos de ellos? ¿Son realmente tan diferentes? ¿Qué es lo que realmente debe saber un pro-
gramador?

Proliferación: hay muchos lenguajes por varias razones, intereses empresariales, nuevas necesidades,
etc.

Eso divide el nivel de utilización de cada uno de ellos: no hay un lenguaje claramente dominante.

Hay muchos lenguajes parecidos entre sı́, tienen instrucciones equivalentes (misma semántica, aun-
que diferente sintaxis).

Lo que más te interesa conocer como programador:

1. las metodologı́as y paradigmas que existen,


2. los lenguajes que soportan cada metodologı́a,
3. centrándonos en los lenguajes imperativos, las instrucciones que todos ellos poseen: asignación,
condicionales, bucles,..., y
4. las construcciones o algoritmos tı́picos que se pueden hacer con ellas.

4
! Aprender nuevos lenguajes es sencillo si se adquieren los conocimientos básicos sobre la
programación y sus metodologı́as.

4
1.3.2. Tipo de lenguajes
Se podrı́an hacer muchas clasificaciones, atendiendo a diversos factores, de los lenguajes de progra-
mación que existen. Vamos a centrarnos solamente en algunos aspectos:

1. Según la complejidad de sus instrucciones:

Lenguajes de bajo nivel: sus instrucciones son las básicas que entiende el procesador (Funda-
mentos de Computadoras y Redes).
Lenguajes de alto nivel: instrucciones más complejas que suelen implicar varias instrucciones
básicas.

2. Según la forma de ejecutar los programas que con ellos se escriben:

Lenguajes compilados: el compilador traduce el código fuente y genera un programa ejecutable


(por el sistema operativo).
Lenguajes interpretados: partiendo del código fuente, se genera un código intermedio que
requiere de un programa llamado intérprete para poder ser ejecutado.

El lenguaje Java que emplearemos en este curso es un lenguaje de alto nivel pseudointerpretado.

1.3.3. El lenguaje Java


Si existen tantos lenguajes de programación, ¿por qué precisamente hemos escogido Java en este
curso? Hay varios motivos:

1. Es un lenguaje de alto nivel: la tendencia en la industria es a usar lenguajes de alto nivel, el uso
de los lenguajes de bajo nivel es marginal,

2. es un lenguaje orientado a objetos: introducir el paradigma Orientado a Objetos es el objetivo de


esta asignatura,

3. es un lenguaje relativamente sencillo, con menos complejidad conceptual que otros, y

4. es posiblemente el lenguaje más utilizado hoy en dı́a.

Figura 1.1: El Lenguaje Java de Sun Microsystems

1.4. Hola, Mundo


El primer programa de todos (o casi todos) los libros de programación de cualquier lenguaje suele ser
el programa “Hola, Mundo”. Es el primero porque sirve para demostrar que nuestro primer programa “da
señales de vida” y es capaz de escribir el mensaje “Hola, Mundo”, de ahı́ su nombre, en la pantalla. Como
haremos en todo el curso, los programas vendrán precedidos con un enunciado, que es una descripción
bastante formal de lo que el programa debe hacer.

5
enun- Realizar un programa que escriba en la pantalla el mensaje “Hola, Mundo”.
ciado
El programa es evidentemente el más simple que haremos en todo el curso, pero nos servirá para
presentar:

1. la estructura general de un programa en Java,

2. nuestra primera clase en Java,

3. el método main(), y

4. cómo imprimir mensajes de texto en la pantalla.

HolaMundo.java
1 /** Muestra por pantalla el mensaje "Hola, Mundo"
2 * @author los profesores de IP */
3 public class HolaMundo {

5 public static void main(String[ ] args) {


6 //Imprimimos el mensaje Hola, Mundo
7 System.out.print("Hola, Mundo");
8 }

10 }

1.4.1. Una clase


Aunque resulta imposible explicar, en este momento, todo lo que tiene un programa tan sencillo como
este, vamos a centrarnos en los aspectos más básicos de su estructura. Lo primero que se debe tener en
cuenta es que en Java en un lenguaje 100 % orientado a objetos:

Todo programa en Java debe contener una clase.

Una clase con el mismo nombre que el del fichero donde está escrita (en nuestro ejemplo Hola-
Mundo).

Para declarar la clase, hay que escribir las palabras reservadas public class y después el nombre
de la clase (lı́nea 3).

El contenido de la clase va entre llaves:

sintaxis Declaración de una clase:

public class nombre {


elementos que contiene la clase (atributos+métodos)
}

Estudiaremos todo lo relacionado con las clases en los Temas 3 y 6.

1.4.2. El método main()


Una vez declarada la clase, lo segundo que debemos hacer para poder escribir nuestro programa es
crear el método main(), que contendrá el programa principal:

En este caso, la clase sólo tiene el método main().

Serı́a engorroso explicar tan pronto todo lo que tiene su declaración, ası́ que de momento la escri-
biremos tal como aparece en la lı́nea 5:

6
sintaxis El método main():
public static void main (String [ ] args) {
secuencia de intrucciones
}

Lo más importante que se debe saber es que lo primero que se hará al ejecutar el programa serán
las instrucciones que contenga el método main(), de ahı́ su importancia.

El programa podrı́a tener parámetros (variable args).

1.4.3. Imprimir en pantalla


Todos los lenguajes tienen que proporcionar formas de comunicarse con sus usuarios. Una de las más
habituales es escribiendo mensajes o avisos en la pantalla:

En Java, la pantalla está asociada al objeto System.out.

Para imprimir podemos usar su método print().

El método print() imprime en la pantalla una cadena de texto.

La cadena se pasa entre comillas dobles.

Compliquemos un poco nuestro programa HolaMundo para ver otro método para escribir en pantalla
en Java.
enun- Realizar un programa que escriba en la pantalla las cadenas “Bienvenido a” e “Introducción a la Progra-
ciado mación”, cada una en una lı́nea.

En este caso debemos usar el método println().

También imprime una cadena de texto, pero después salta a la lı́nea siguiente.

Bienvenido.java
1 /** Muestra en consola un texto de bienvenida a IP
2 * @author los profesores de IP */
3 public class Bienvenido {
4 public static void main(String[ ] args) {
5 /* Imprimimos las cadenas "Bienvenido a" e
6 "Introducción a la Programación" en dos lı́neas */
7 System.out.println("Bienvenido a");
8 System.out.print("Introducción a la Programación");
9 }
10 }

1.4.4. Comentarios
Si nos fijamos, en ambos programas aparecen una serie de lı́neas que realmente NO contienen ins-
trucciones del programa, pero que sin embargo son muy útiles para que este tenga mayor calidad. Son
los comentarios.

definición Comentarios: Explicaciones útiles, escritas dentro del programa, para aclarar la lógica de funcio-
namiento del mismo. Sirven para ayudar a entender mejor el programa.

En Java se pueden escribir tres tipos de comentarios:

1. //: el comentario empieza tras las dos barras y acaba al final de la lı́nea,

7
2. /*: el comentario empieza con /* y acaba cuando encuentra */ (generalmente en otra lı́nea), y
3. /**: similar al anterior, empieza en /** y acaba en */. La diferencia es que estos comentarios, pro-
cesados con algunas herramientas, permiten generar automáticamente documentación del programa
(Javadoc).
Es muy importante comentar bien los programas: valoraremos mucho este aspecto en esta asignatura.

1.5. Propiedades fundamentales de los programas


Ahora que hemos hecho nuestros primeros programas, no está de más fijar las propiedades que un
buen programa debe cumplir. Hay un famoso dicho que dice que “todo programa hace algo... lo difı́cil
es que haga exactamente la tarea para la que está programado”. Y sin embargo, ni eso es suficiente. Un
programa además de hacer exactamente la tarea que se desea, tiene que cumplir otros requisitos:
1. correcto o eficaz, que produzca resultados coherentes con su especificación,
2. eficiente, que optimice el consumo de recursos:
tiempo: tarde en ejecutarse lo menos posible,
memoria: use la menor cantidad de memoria posible para sus datos,
tiempo y memoria son dos variables a veces imposibles de optimizar juntas, para que un
programa tarde menos debe ocupar más memoria y viceversa,
3. legible o claro, fácilmente comprensible por el autor (tiempo después de su diseño) y por cualquier
otro programador que lea el código fuente (junto con la documentación aportada),
4. reutilizable, algunos trozos (clases o funciones, según la metodologı́a) deberı́an poder ser utilizados
en otros casos, y
5. modificable, a lo largo de su ciclo de vida.
De los cincos aspectos antes citados, en está asignatura nos vamos a centrar especialmente en los tres
primeros. Los dos últimos, aunque tal vez sean los más importantes desde el punto de vista del Módulo
de Software de Aplicaciones, se irán adquiriendo a medida que se estudien las asignaturas del citado
modulo. Como estamos empezando, nos centraremos en ser capaces de hacer, dado un problema no muy
extenso y claramente especificado (ya veremos qué es esto más adelante), un programa correcto, eficiente
y legible que lo resuelva. Nuestros dos programas anteriores cumplen esas tres premisas:
1. correcto: todos los programas hacen algo, lo difı́cil es lograr que hagan exactamente las cosas para
las que se diseñaron, “tienen que funcionar”.
2. eficiente: entre todos los programas que funcionan, uno es el más eficiente (óptimo). Cuanto más
se aproxime un programa a ese óptimo, mejor.
3. legible: pero no bastará con que sea correcto y eficiente, además tendrá que estar bien escrito y
documentado (muy importante):
Sangrados: bien sangrado para facilitar su lectura. Obsérvese como el método main(), al
estar dentro de la clase HolaMundo está más sangrado, del mismo modo que la instrucción
print() lo está más que el main() al estar dentro de él. Nuestros programas se compondrán
de una serie de sentencias que estarán anidadas unas dentro de otras. Cada anidamiento debe
implicar un mayor sangrado.
Comentarios: deben ayudar a entender cómo funciona internamente el programa, cuál es su
lógica de funcionamiento. No deben repetir, en castellano, lo que dice el programa en Java,
tienen que ser aportaciones y aclaraciones que sirvan para que el programa se entienda mejor.
Documentación: otros documentos (distintos al código fuente) necesarios para que otros
programadores puedan entender los detalles técnicos del programa.
Nuestros dos programas anteriores cumplen esas tres premisas. Del mismo modo, el resto de programas
que expondremos durante el curso también lo harán. En todos los programas que se desarrollarán durante
el curso valoraremos especialmente estos tres aspectos.

8
1.6. Ciclo de vida de un programa
Aunque esto se estudiará con todo detalle en la asignatura de Ingenierı́a del Software, es importante
que comprender cuáles son las fases que se suelen seguir a la hora de realizar una aplicación informática
o programa.
Cuando se acomete un programa, sus programadores NO se lanzan a escribir el código del mismo.
Antes de hacer eso, siguen las siguientes fases:

1. Analizan qué debe hacer el programa (Especificación) y determinan las partes en que se puede
descomponer (Análisis),

2. se diseña claramente cómo va a ser internamente cada parte (Diseño),

3. solamente tras realizar el Análisis y un Diseño, el programa se escribe (Codificación),

4. en aplicaciones de cierta entidad, compuestas de muchas partes, serı́a necesario juntar todas ellas
(Integración),

5. cuando el programa es operativo, se prueba si cumple todos los requisitos y se detectan sus fallos
(Pruebas), y

6. durante su vida útil, el programa se corrige o se le añaden nuevas funcionalidades (Mantenimiento).

Especificación
y Análisis

Diseño

Codificación

Integración y
Pruebas

Mantenimiento

Figura 1.2: Ciclo de vida de un Programa o Aplicación

En la Figura 1.2 aparece una ilustración gráfica de todo este proceso. Como se puede observar es
un proceso cı́clico e iterativo, cuando se detecta un error en cualquiera de las fases, se vuelve a la fase
anterior que corresponda para solucionarlo y se vuelve a someter al programa a las fases subsiguientes.
El ciclo de vida anterior se refiere, obviamente a una aplicación de una cierta entidad, realizada
por equipos de programadores. En esos casos, cada una de las fases es muy compleja y requiere de la
colaboración de varios analistas/programadores/testers.

Dado que nuestros programas serán simples, todas esas fases resultarán más sencillas y cortas.

9
Partiremos siempre de un enunciado preciso, que indicará lo que el programa debe hacer. Esto
hará el análisis más sencillo.

Pero siempre se aplicarán todas las fases. Por ejemplo, hay que probar muy bien los progra-
mas. Y no nos referimos a probarlos una vez, como suelen hacer muchos programadores inexpertos.
Es necesario hacer una baterı́a de pruebas suficientemente amplia para garantizar que el programa
cumple todos los requisitos para los que se diseñó y que funciona correctamente.

Cada fase tiene su propio lenguaje.

La Figura 1.3 muestra el lenguaje propio de cada una de las fases del desarrollo de un programa.
El único lenguaje que puede resultar desconocido es el lenguaje algorı́tmico. Es una forma de detallar
de forma precisa las instrucciones de que consta un programa o una parte de él. Lo estudiaremos en la
Sección 1.6.2.

Problema Enunciado Algoritmo Programa


Análisis Diseño Codificación
Lenguaje Lenguaje Lenguaje Lenguaje
Natural Científico Algorítmico Programación

Figura 1.3: Etapas de un programa simple

1.6.1. Análisis
Todas las fases del ciclo de vida de un programa son importantes si se quiere garantizar su calidad.
Sin embargo, es probable que las dos fases más trascendentales, tal vez por ser las primeras, sean el
Análisis y el Diseño.

definición Análisis: El objetivo del Análisis es descomponer el programa en unidades más simples, facilitando
ası́ su realización.

Pero:

No se trata de partir el programa como si fuera una tarta.

Es más bien dividirlo en unidades funcionales básicas, con una entidad propia, clara y bien definida.

Este proceso permite varias cosas:

1. entender mejor las distintas partes que componen el programa,


2. propiciar la división del trabajo, y
3. favorecer las fases posteriores de la elaboración del programa (es más sencillo hacer el diseño
o realizar las pruebas).

Las dos formas más extendidas para descomponer y simplificar la construcción de programas son:

1. Programación Modular: la unidad básica es la función, el programa se compondrá de un


conjunto de funciones.

2. Programación Orientada a Objetos: la unidad básica es la clase, el programa se formará por


la unión de varias clases, relacionadas entre sı́ por herencia y composición.

Se van a estudiar en:

10
Fundamentos de Informática: Programación Modular.
Introducción a la Programación: Programación Orientada a Objetos (construir una clase, compo-
sición).
Metodologı́a de la Programación: Programación Orientada a Objetos (jerarquı́as de clases, herencia,
polimorfismo).

1.6.2. Diseño
Dado que vamos a seguir un enfoque Orientado a Objetos e Imperativo, en la fase de Diseño deberemos
detallar:
1. Cómo van a ser las clases que tendrá nuestro programa (notación UML - Unified Modeling Lan-
guage)
2. Cómo van a ser los acciones (métodos) que puedan hacer los objetos de esas clases (lenguaje
algorı́tmico o pseudocódigo)
Para explicar cómo van a ser las clases que tendrán nuestros programas usaremos la notación UML
(Unified Modeling Language). Sirve para indicar los elementos que tiene una clase:
1. sus atributos (los datos que tiene internamente cada objeto de la clase), y
2. las funciones (métodos) que los objetos de esa clase pueden realizar.
En la Figura 1.4 se muestra la forma de representar ambas cosas en notación UML. En la figura
de la izquierda aparece una representación genérica, con el nombre de la clase arriba, con los atributos
o campos (datos) en el medio, y con las funciones que pueden realizar abajo. En el caso de la clase
HolaMundo, no guarda ningún dato, y solamente realiza el método main(). El sı́mbolo + delante de
main() indica que este método es accesible desde fuera de la clase (todo esto lo estudiaremos con detalle
en los Temas 3 y 6).

Clase
Atributos HolaMundo

Métodos
+ main(String[])

Figura 1.4: Notación UML para representar la especificación de una clase. Ejemplo: clase HolaMundo

Para diseñar cómo van a ser internamente los métodos de las clases que hagamos, dado que estamos
en un enfoque de Programación Imperativa, indicaremos cuando proceda los algoritmos expresados en
lenguaje algorı́tmico o pseudocódigo que realicen esas acciones. Han aparecido dos términos nuevos,
definámoslos.
definición Algoritmo: Conjunto finito de instrucciones ordenadas que permite realizar una acción mediante
pasos sucesivos. Dado un estado inicial y unos datos, siguiendo dichos pasos sucesivos se llega a un
estado final (solución).

Ejemplos de algoritmos en la vida cotidiana hay muchos. Desde los manuales para montar muebles,
hasta las recetas de cocina. En todos ellos se nos detalla, paso a paso, las acciones que tenemos que
hacer, bien para conseguir finalmente montar un mueble o hacer un determinado plato de comida.

11
definición Lenguaje algorı́tmico o Pseudocódigo: Permite describir a alto nivel un algoritmo, empleando
una mezcla de lenguaje natural y algunas convenciones sintácticas propias de los lenguajes de
programación, como asignaciones, bucles y condicionales.

El pseudocódigo facilita a las programadores la comprensión de un algoritmo, omitiendo, tal vez,


ciertos detalles irrelevantes que sin embargo son necesarios en la implementación, pero que se supone
que el programador experto en ese lenguaje sabrá adaptar. Al no tener definidas unas reglas precisas,
como pasa con los lenguajes de programación, cada analista/programador suelen utilizar convenciones
propias. Sin embargo, el pseudocódigo suele ser comprensible sin necesidad de conocer un lenguaje de
programación especı́fico. Es tarea del programador adaptar dicho pseudocódigo a un lenguaje concreto.
Por ejemplo, en el pseudocódigo del método main() de la clase HolaMundo, dice simplemente “Escribir
en pantalla” el mensaje “Hola, Mundo”. Como es obvio no se escribirı́a ese mensaje de la misma forma
en lenguajes de programación diferentes. Pero dado que el programador conoce perfectamente, o deberı́a,
el lenguaje de programación en el que está realizando la implementación, será sencillo para él saber la
instrucción que debe usar. Por ejemplo en Java, sabe que esta puede ser print() o println().

Algoritmo 1.1 HolaMundo - método main()


Escribir en pantalla “Hola, Mundo”

1.7. Pruebas de software


Para finalizar este tema, vamos a introducir un elemento capital a la hora de hacer buenos progra-
mas; son las pruebas de software. Para obtener un buen programa es imprescindible probarlo suficiente.
Habitualmente a las pruebas se les da menos importancia de la que tienen, pero es esencial probar
concienzudamente los programas que escribimos. Muchas veces los programadores se centran mayorita-
riamente en escribir el programa y dedican muy poco tiempo a probarlo. Pero en la realidad solamente
podremos garantizar que nuestros programas son correctos si los sometemos a las pruebas adecuadas. Los
programadores, sin embargo, tienden a hacer menos pruebas de las necesarias, en parte porque es difı́cil
desde un punto de vista psicológico aceptar que algo que hemos hecho está mal o al menos tiene errores,
y también porque, como pasa con la documentación, es visto como una tarea tediosa. Sin embargo, un
buen programador es precisamente aquél que es capaz de detectar y corregir rápidamente los errores que
comete al escribir sus programas. La única manera de lograrlo es probando los programas de una forma
sistemática.
definición Pruebas de Software: Las pruebas de software son todos los procesos que permiten evaluar y
verificar la calidad de un producto software.

Las pruebas de software incluyen, por tanto, todos los aspectos que tienen que ver con medir y
comprobar la calidad de los programas. Cuando tenemos en cuenta programas de una cierta entidad
eso incluye muchos aspectos, no solamente los funcionales (que el programa funcione bien), sino otros
aspectos no funcionales que también pueden ser importantes, por citar solamente algunos: la seguridad,
la eficiencia computacional o la escalabilidad (que el programa sea capaz de manejar adecuadamente un
volumen de datos cada vez mayor).

1.7.1. Tipos de pruebas


Hay muchos tipos de pruebas. Cada uno de ellos trata de evaluar unos aspectos concretos, o tiene
objetivos diferentes o se hace en distintos momentos durante la construcción del programa. Sin que la
lista pretenda ser exhaustiva, podrı́amos citar los siguientes tipos de prueba a modo de ejemplo:
Funcionales: verifican que el programa hace, y a su vez permite hacer, correctamente todas sus
funcionalidades.
No funcionales: se analizan otros aspectos no funcionales, como los citados anteriormente, eficiencia,
seguridad, escalabilidad, etc.

12
Unitarias: se prueba individualmente cada una de las partes, módulos o unidades en las que se ha
descompuesto el programa durante la fase de análisis. Si se ha empleado una programación orientada
a objetos, cada una de las unidades será un clase, en cambio si se ha usado una programación
modular, las unidades serán las funciones en que se divide el programa. El objetivo es probar que
cada una de esas partes funciona correctamente.

De integración: una vez verificado que cada modulo funciona correctamente mediante las pruebas
unitarias, en las pruebas de integración se comprueba que no surgen errores al unir o integrar todos
los módulos de la aplicación. Normalmente los módulos se van integrando de forma incremental, en
lugar de integrarlos todos ellos de una vez, lo que dificultarı́a encontrar los errores más rápidamente.

De rendimiento: que miden distintos parámetros sobre la eficiencia de la aplicación, por ejemplo,
estabilidad, tiempos de respuesta, uso de recursos, etc. Se pueden incluir en este grupo las pruebas
de carga, que comprueban que el sistema funciona adecuadamente con grandes cantidades de datos
o muchos usuarios.

De carga: se prueba que el sistema funciona adecuadamente con grandes cantidades de datos o
muchos usuarios.

Alfa: son las pruebas que los equipos de desarrollo realizan internamente, normalmente con las
versiones iniciales del programa que pueden ser todavı́a inestables o no incluir todas las funciona-
lidades.

Beta: vienen después de las pruebas alfa y consisten en que una versión ya casi definitiva del
programa se distribuye, a veces de forma privada, a una serie de usuarios externos que mediante
el uso del programa detectan los errores que aún tiene.

De seguridad: garantizan principalmente la confidencialidad de los datos.

De compatibilidad: se evalúa la compatibilidad entre el programa y el sistema operativo u otras


aplicaciones.

De usabilidad y accesibilidad: chequean si la aplicación es fácil de usar para los usuarios, incluyendo
los usuarios con discapacidades.

De internacionalización: se prueba que el programa sigue funcionando a pesar de haber sido tra-
ducido a otra lengua o adaptado para una localización diferente (formato de fecha, moneda, etc).

De instalación: se comprueba que la aplicación se instala correctamente en el hardware del usuario.

De aceptación: las realiza el cliente que ha contratado la realización de la aplicación y comprueba


que esta cumple los requisitos solicitados.

En este curso, dada la dimensión de los programas que haremos, nos centraremos únicamente en las
pruebas funcionales. Es decir, nuestros objetivos serán:

1. Detectar y corregir los errores que contiene el programa

2. Comprobar que el programa funciona correcta y eficientemente

Dado que además nuestros programas serán bastante simples, consistirán únicamente en una clase
o en unas pocas funciones, en realidad vamos a hacer pruebas funcionales unitarias en las que lo que
comprobaremos es que una clase o una función hace correctamente las funcionalidades para las que se
diseñó. Esto lo veremos con más detalle en la Sección 4.11. Antes, y para introducir lo que veremos sobre
pruebas funcionales, vamos a definir qué son los requisitos funcionales de un programa y qué relación
tienen con las pruebas funcionales y, desde un punto de vista más formal, vamos a explicar cómo se
especifica formalmente un programa o trozo de código.

13
1.7.2. Requisitos funcionales y casos de prueba
Las pruebas funcionales se diseñan partiendo de los requisitos del programa. Pero ¿qué significa
requisito en este contexto?

definición Requisito funcional: Es cada una de las funcionalidades que tiene un programa o un sistema
software.

Vamos a verlo con un sencillo ejemplo. Imaginemos que queremos hacer una programa de tratamiento
de imágenes. Durante la fase de Especificación y Análisis del ciclo de vida antes descrito (Sección 1.6),
concretamente al inicio de la misma, lo que haremos es definir exactamente qué debe hacer nuestro
programa de tratamiento de imágenes. Una forma de expresar todas esas funcionalidades es a través de
lo que se llama catálogo de requisitos que no es otra cosa que una lista en la que de forma textual se
expresan cada una de las funcionalidades que el programa debe realizar. Algunos de los requisitos de
nuestro programa de tratamiento de imágenes podrı́an ser los siguientes:

RF1 Abrir y leer ficheros de imágenes en formato jpg, tiff y bmp.


RF2 Convertir imágenes en color a escala de grises.
RF3 Grabar una imagen a fichero en cualquier de los formatos soportados, independientemente del
formato de la misma.

Como se puede ver, los requisitos son básicamente las cosas que el programa debe hacer. Desde el
punto de vista de las pruebas, el interés que tiene el catálogo de requisitos es que proporciona exactamente
los elementos funcionales que debemos comprobar. Obviamente, si queremos garantizar que el programa
funciona correctamente, deberı́amos probar exhaustivamente cada una de esas funcionalidades. Por ello,
las pruebas funcionales se diseñan fundamentalmente a partir del catálogo de requisitos.
Siguiendo con el ejemplo, para cada requisito funcional vamos a diseñar un conjunto de casos de
prueba lo más exhaustivo posible para poder probar que el programa hace correctamente dicho requisito.
Por ejemplo, para el RF1 los casos de prueba que vamos realizar van a ser los siguientes:

Prueba 1.1 Abrir un fichero en formato jpg y comprobar que el programa lo abre correctamente.
Prueba 1.2 Abrir un fichero en formato tiff y comprobar que el programa lo abre correctamente.
Prueba 1.3 Abrir un fichero en formato bmp y comprobar que el programa lo abre correctamente.
Prueba 1.4 Abrir un fichero en uno formato que no sea ni jpg, ni tiff, ni bmp. El programa debe dar un
mensaje de error.

Como se puede ver, se ha diseñado una prueba individual para comprobar cada uno de los aspectos
ya que incluso dentro de un solo requisito hay varios elementos que hay que comprobar.
Lo mismo habrı́a que hacer con el resto de requisitos funcionales, lo que podrı́a darnos una lista de
casos de prueba como la siguiente:

Prueba 2.1 Convertir una imagen jpg en color a escala de grises.


Prueba 2.2 Convertir una imagen tiff en color a escala de grises.
Prueba 2.3 Convertir una imagen bmp en color a escala de grises.

Prueba 3.1 Grabar una imagen jpg en formato tiff.


Prueba 3.2 Grabar una imagen jpg en formato bmp.
Prueba 3.3 Grabar una imagen tiff en formato jpg.
Prueba 3.4 Grabar una imagen tiff en formato bmp.
Prueba 3.5 Grabar una imagen bmp en formato jpg.

14
Prueba 3.6 Grabar una imagen bmp en formato tiff.

A medida que los programas tienen funcionalidades más complejas, las listas de los casos de pruebas
son obviamente más largas. Aunque eso hace que realizar las pruebas funciones lleve más tiempo, la buena
noticia es que los procesos de prueba se pueden automatizar en ciertas situaciones. En nuestro caso, en
el que los programas serán mucho más pequeños, realizar las pruebas funcionales consistirá básicamente
en ejecutar el programa con unos pocos casos de prueba.

1.7.3. Especificación formal de un programa


Para finalizar esta sección e introducir lo que necesitaremos para realizar pruebas funcionales a partir
del Tema 4, vamos a ver cómo puede especificarse formalmente un programa. Emplearemos el siguiente
ejemplo:
enun- Realizar un programa que permita al usuario introducir dos números enteros por teclado y muestre su
ciado suma en la pantalla.

Como se comentó, todos los problemas los introducimos mediante un enunciado. De él es de donde
determinamos lo que el programa debe hacer:

Siempre partiremos de enunciados claros, que indican lo que los programas deben hacer.
Es importantı́simo saber LEER y ENTENDER los enunciados de los programas.
Es imprescindible para que los programas hagan lo que deben. Si no se entiende bien el enunciado,
será imposible que el programa funcione.
Los enunciados siempre tienen dos partes:
1. el resultado que el programa debe producir o hacer, y
2. los datos que usa y todos los condicionantes que el programa debe tener en cuenta.

Todo trozo de código, ya sea un programa completo, o una pocas lı́neas, puede especificarse for-
malmente indicado sus condiciones de partida y lo que finalmente produce, o lo que es lo mismo, su
precondición y postcondición. Coinciden con los dos elementos antes citados. Definamos ambas:

definición Precondición: Indica el conjunto de condiciones para las que el programa producirá una salida
correcta.

Ejemplo: viene dado por la frase “permita al usuario introducir dos números enteros”. El programa
funcionará cuando el usuario introduzca dos números enteros, no otra cosa.

definición Postcondición: Indica el resultado que el programa producirá si se cumplen las condiciones que
indica la precondición.

Ejemplo: “muestre su suma en la pantalla”. Al final, el programa mostrará la suma de ambos números,
esa es la postcondición.
Todo algoritmo debe diseñarse teniendo en cuenta ambas condiciones simultáneamente: debe alcanzar
la postcondición, pero solamente cuando las condiciones de partida vienen dadas por la precondición.
4
! Un programa solamente deberá funcionar correctamente, es decir, deberá alcanzar su post-
condición, cuando se cumplan las condiciones indicadas en su precondición.
En nuestro ejemplo:

El programa solamente devolverá la suma correcta cuando el usuario introduzca dos números
enteros.
Si introdujera por ejemplo números reales, el programa acabará con un error o con una salida
incorrecta.

15
Figura 1.5: Error cuando tratamos de sumar dos números reales

Obsérvese la ejecución del programa SumaDosEnteros de la Figura 1.5. En lugar de teclear dos
números enteros, hemos escrito dos números reales. El programa no ha sumado los dos números y ha
producido un error.
¿Está mal el programa?

NO, hace lo que se pedı́a.


Si se deseaba poder sumar números reales, la especificación (su enunciado) tendrı́a que haber sido
otro.

Esto puede parecer pobre, pero debe tenerse en cuenta que en muchas situaciones hacer que un
programa funcione, no solamente en los casos indicados en la precondición, sino en otros más, complicarı́a
mucho el mismo, haciéndolo seguramente más ineficiente para el caso concreto de la precondición de ese
trozo de código.
Una forma de entender esta forma de desarrollar nuestros algoritmos es pensando en que en realidad
formarán parte de un programa mayor, la aplicación que estemos desarrollando con otros programadores
ya sea dentro de una empresa o, por ejemplo, en un proyecto de software libre con otros desarrolladores.
Cada trozo de esa aplicación (clase, método, función, . . . ) debe hacer una tarea concreta y muy especı́fica,
y nunca otra, aunque esa otra sea más completa mirada individualmente. Solamente ası́ se consigue que
cada parte del programa sea lo más eficiente posible y, con ello también lo será la aplicación en su
conjunto.

16
Tema 2

Tipos de datos básicos


Objetivos
Diferenciar entre variables y constantes y su uso en los programas.
Ser capaz de declarar y usar una constante o una variable, eligiendo el tipo adecuado.
Utilizar el operador de asignación para asignar valores a variables.
Dominar el uso de los operadores matemáticos (+, −,∗,. . . ) en expresiones con reales y enteros.
Entender y saber aplicar los conceptos de precedencia y asociatividad de los operadores.
Ser capaz de imprimir datos en la consola y leer datos de teclado.

2.1. Variables
Como haremos a lo largo de todo el curso, presentaremos los nuevos conceptos que se vayan intro-
duciendo mediante aplicaciones concretas que los requieran. En el primer problema de este tema nos
limitaremos a realizar un programa para sumar dos números enteros.
enun- Realizar un programa que permita al usuario introducir dos números enteros por teclado y muestre su
ciado suma en la pantalla.
Aunque el programa parezca simple (y lo es), tiene muchos conceptos nuevos que tenemos que apren-
der a manejar para realizarlo:
1. Trabajar con datos enteros dentro de un programa.
2. Leer datos del teclado.
3. Realizar operaciones matemáticas con esos datos.
4. Imprimir información con formato en la pantalla.
Como ocurrı́a en los programas del Tema 1, de nuevo simplemente tendremos que crear una clase,
SumaDosNúmeros, con un método main(). La especificación en UML de la clase es la que aparece en la
Figura 2.1.
Antes de empezar con todos esos detalles técnicos de Java, vamos a escribir el pseudocódigo del
algoritmo que resuelve nuestro problema. Podrı́a ser el siguiente:

Algoritmo 2.1 SumaDosNúmeros - main()


//PRE: el usuario escribirá dos números enteros
Leer los números enteros del teclado
Calcular su suma
Mostrar la suma en pantalla
//POS: muestra la suma de los dos enteros

17
SumaDosNúmeros

+ main(String[])

Figura 2.1: UML - Clase SumaDosNúmeros

En este caso el algoritmo es bastante simple:

Consiste en una secuencia de acciones.


Lo más importante es el orden en el que se ejecutan.
Si se cambiara el orden de las acciones el programa NO producirı́a el resultado correcto.

Ese último detalle es trascendental, ya que realizaremos una programación imperativa y secuencial.
En este tipo de programación lo más importante es el orden, la secuencia en la que las ordenes imperativas
se realizan. Si la secuencia es incorrecta, el programa no funcionará. Parece evidente que antes de poder
calcular la suma de los dos números es necesario leer sus valores. Hacerlo al revés no tendrı́a sentido. El
orden es, por tanto, primordial.
Veamos ya el código en Java que implementa el pseudocódigo de nuestro programa para sumar dos
números enteros.

SumaDosNúmeros.java
1 import java.util.Scanner;

3 /** Lee dos números enteros de teclado y muestra su suma en la consola


4 * @author los profesores de IP */
5 public class SumaDosNúmeros {

7 public static void main(String[ ] args) {


8 int número1; //Declaramos dos variables enteras
9 int número2;
10 //Objeto Scanner asociado con el teclado
11 Scanner teclado= new Scanner(System.in);
12 //Leemos ambos enteros
13 System.out.print("Introduce dos números: ");
14 número1=teclado.nextInt();
15 número2=teclado.nextInt();
16 //Calculamos su suma
17 int suma=número1+número2;
18 //Mostramos el resultado en la pantalla
19 System.out.printf("Su suma es %d\n",suma);
20 }
21 }

Vamos a explicarlo con detalle. Lo primero que necesitamos para desarrollar nuestro programa es
poder guardar los dos números en la memoria del ordenador y poder hacer cálculos con ellos, sumarlos
en nuestro problema. Para ello es necesario declarar variables.

definición Variable: Nombre simbólico que un programa utiliza para referenciar una posición de la memoria.
En dicha posición el programa guarda un dato que necesita para su funcionamiento.

Las variables son el mecanismo que permite a los programas almacenar datos en la memoria del
ordenador. Se caracterizan por dos propiedades fundamentales:

1. el nombre o identificador, será lo que nos permitirá acceder dentro del código del programa al
dato que almacena, y

18
2. el tipo de valores que pueden almacenar. Cada tipo de dato puede almacenar datos diferentes y
realizar ciertas operaciones.

Para declarar una variable dentro de un programa básicamente hay que indicar esos dos elementos:

sintaxis Declaración de una variable:


tipo nombre [= valorinicial];

Como se ve en la sintaxis, la declaración de una variable tiene tres elementos, siendo el tercero de
ellos opcional:

1. Tipo: indica la clase de valores que guardará la variable.

2. Nombre o Identificador: cómo nos referiremos a esa variable dentro del programa.

3. Valor inicial (opcional1 ): primer valor que toma la variable.

En una misma declaración es posible declarar varias variables del mismo tipo separándolas mediante
comas:

sintaxis Declaración de varias variables del mismo tipo:


tipo nombre1 [= valorinicial], nombre2 [= valorinicial], . . . ;

Vamos a estudiar con detalle en las secciones siguientes cómo son los elementos que intervienen en la
declaración de una variable.

2.2. Tipos de datos


El primer elemento de la declaración de una variable es indicar su tipo. Pero, ¿qué es un tipo de dato?

definición Tipo de dato: Se define mediante el conjunto de valores de que consta y el conjunto de operaciones
que se pueden realizar con ellos.

Es un concepto similar al de ciertos conjuntos en Matemáticas, como los números naturales (N), los
enteros (Z) o los reales (R). Cada uno de ellos representa un conjunto de valores y con ellos se pueden
hacer operaciones, como las operaciones matemáticas tı́picas en el caso de los conjuntos antes citados.
Analizando desde un punto de vista práctico esta definición, cuando escogemos el tipo que le damos
a una variable estamos decidiendo dos cosas fundamentales:

el conjunto de valores que la variable podrá tomar, y

las operaciones que podremos hacer con esa variable.

En nuestro problema concreto, necesitamos un tipo que:

permita almacenar números enteros, y

sumar dos números enteros.

19
Tabla 2.1: Tipos básicos

Enteros byte, short, int, long


Reales float, double
Caracteres char
Lógicos boolean

2.2.1. Tipos básicos


Como todos los lenguajes de programación, Java viene con un serie de tipos predefinidos que son
necesarios en cualquier programa. El tipo de una variable puede ser cualquiera de los tipos básicos
que vienen predefinidos, ası́ como nuevos tipos que el programador puede crear para adaptarse a las
necesidades de su aplicación (ver Temas 3, 4 y 6). De momento nos centraremos en los tipos básicos,
todos ellos están descritos en la Tabla 2.1. Iremos viendo la utilidad de cada uno de ellos a lo largo de
este tema y de los siguientes.
En nuestro programa todas las variables son enteras, y las hemos declarado como int, el tipo más
común de los enteros:
8 int número1; //Declaramos dos variables enteras
9 int número2;
17 int suma=número1+número2;

Es importante observar que a la variable suma le hemos dado como valor inicial la suma de las otras
dos variables. El valor que debe tener se puede calcular desde el momento de su declaración, ya que
debe ser la suma del contenido de las variables número1 y número2, por eso en esta ocasión le damos un
valor inicial. En el caso de las otras dos variables, en el punto en que las declaramos en el programa no
sabemos el valor que van a tomar cuando este se ejecute, ya que dependerá de los valores que el usuario
introduzca por teclado, que se leen con las instrucciones que van a continuación. Por ello no le asignamos
un valor inicial a ninguna de ellas.

2.3. Tipos enteros


Existen cuatro tipos enteros predefinidos. La única diferencia entre ellos es el rango de valores que
pueden almacenar, que depende del espacio que ocupan en memoria (ver Tabla 2.2).

Tabla 2.2: Tipos enteros

Tipo Espacio Rango de valores


byte 1 byte -128 .. 127
short 2 bytes -32768 .. 32767
int 4 bytes -2147483648 .. 2147483647
long 8 bytes -9223372036854775808 .. 9223372036854775807

Para escoger el tipo entero adecuado para las variables enteras de nuestros programas debemos tener
en cuenta los siguientes aspectos:

No se deben escoger por sistema los más grandes (int o long).

Antes al contrario, hay que escoger el más pequeño que pueda almacenar todos los valores factibles.
Es decir, si por ejemplo tenemos una variable que va a tener un valor siempre comprendido entre
0 y 10000 podrı́a declararse perfectamente con short ya que tiene un rango adecuado a los valores
factibles de la variable.
1 Pondremos entre corchetes [ ] los elementos opcionales de la sintaxis de Java. Los corchetes no se deben escribir.

20
En vectores grandes y si los valores lo permiten, se puede usar byte o short en lugar de int para
ahorrar memoria. Los vectores se estudian en el Tema 5.

2.3.1. Operadores matemáticos con enteros


Con cualquiera de los tipos enteros se pueden realizar las operaciones matemáticas tı́picas (véase
Tabla 2.3). Para ello se usan operadores, en este caso, los aritméticos.

definición Operador: Sı́mbolo del lenguaje que permite realizar una operación.

Tabla 2.3: Operadores aritméticos, tipos enteros

Tipo Operador Operación Ejemplo


Aditivos + suma 7 + 4 11
− resta 4 − 7 −3
Multiplicativos ∗ multiplicación 7 ∗ 4 28
/ división entera 7/4 1
% módulo o resto 7 %4 3

Los operadores matemáticos comparten dos caracterı́sticas:


Son binarios, necesitan dos operandos (cardinalidad).
Producen un resultado entero.

definición Cardinalidad de un operador: Es el número de operandos que necesita dicho operador.

En nuestro programa, usamos el operador suma, +, y el valor que genera, la suma de los valores que
tengan las variables número1 y número2, se utiliza como valor inicial de la variable suma.

2.4. Caracteres
Aunque no los vamos a usar en ninguno de los ejercicios desarrollados en este tema, vamos a intro-
ducir el tipo char que sirve para representar caracteres. El motivo es porque, en realidad, un carácter se
representa mediante un número entero de acuerdo a una cierta codificación. Esto no solamente ocurre
en Java sino que pasa en todos los lenguajes. Existen codificaciones aprobadas por organismos interna-
cionales que definen los estándares para representar caracteres. Dichas codificaciones definen qué código
entero le corresponde a cada carácter. Por eso se puede decir que un carácter internamente es un entero,
más allá de que el entero sirva para representar una letra. El código más famoso históricamente era el
código ASCII, pero se ha quedado obsoleto antes las nuevas codificaciones que incluyen los caracteres de
prácticamente todos los lenguajes que se hablan en el mundo. Por ejemplo, en ASCII el carácter ’A’ se
representa con el código 65, la ’a’ con el 97.
En primer lugar estudiaremos las caracterı́sticas de la representación, tanto interna como en los
programas, de los caracteres empleados en Java:

Java representa los caracteres mediante el tipo básico char.


Codificación: Unicode. El código Unicode incluye el código ASCII y comprende los caracteres de
prácticamente todos los idiomas conocidos.
Espacio: se representa mediante 2 bytes.
Rango: 0 .. 65535. Es decir, podemos representar más de 65000 caracteres, bastantes de los que
se usan en el alfabeto del español.

21
Constantes carácter: se representan entre comillas simples. Como ha quedado claro en el ejemplo
anterior, no es el mismo carácter el de una letra en minúscula, que el de su correspondiente versión
en mayúsculas. En la Tabla 2.4 se muestran las distintas formas de representar una constante
carácter. Por ejemplo, las representaciones octal y hexadecimal representan los caracteres a través
de sus códigos enteros: 141 en octal es 97, lo mismo que 61 en hexadecimal.

Tabla 2.4: Formas de representación de constantes char

Formato Ejemplo
Sı́mbolo ’a’
Secuencia de escape ’\n’
Octal \141 → ’a’
Hexadecimal (Unicode) \u0061 → ’a’

Las secuencias de escape sirven para representar caracteres especiales. Por ejemplo, la secuencia de
escape ’\n’ representa el carácter “nueva lı́nea”. Las estudiaremos con detalle en la Sección 2.6.2,
de momento es suficiente con fijarse en que empiezan por una barra invertida (backslash), para
indicar que es una secuencia de escape, seguida de un carácter que es el que realmente representa
el carácter especial. Aunque tenga dentro de las comillas dos caracteres, solamente representa uno,
como ocurre con el resto de constantes carácter.

Desde el punto de vista de su uso en los programas, además de definir variables de tipo char, es muy
importante dominar la representación interna y las operaciones que se pueden hacer con los caracteres:

No se debe confundir el carácter ’4’ con el número 4. El código del carácter ’4’, es el número 52.
Como se puede apreciar, no tiene nada que ver una cosa con la otra. Sencillamente la constante
carácter ’4’ no se representa el número entero 4. El código que tiene la constante es el que se
definió en los estándares, primero ASCII, y luego Unicode. Pero no es ninguna desventaja, para
representar el número 4, se usa un entero, no un carácter.

Como se representan con enteros, los caracteres soportan los mismos operadores matemáticos que
el resto de tipos enteros.

Sı́, ¡se pueden sumar dos caracteres!

La suma de dos caracteres tendrá como el resultado el carácter que tenga cómo código la suma de
los códigos de los caracteres que intervienen en la suma. Esto es mejor verlo con un ejemplo:
char c=’A’+’0’; //c valdrá ’q’. 65+48=113=’q’

Las letras minúsculas están separadas de las mayúsculas por 32, una potencia exacta de 2. Eso
facilita las transformaciones entre minúsculas y mayúsculas.

2.5. Identificadores
La otra parte clave de la declaración de una variable es darle un nombre. Todos los nombres que se
usan en un programa, ya sean de clases (como la clase HolaMundo), variables o de otros elementos que
estudiaremos, como constantes, objetos o métodos, reciben el nombre técnico de identificadores.

definición Identificador: Nombre que se da a un elemento (variable, clase, método, etc.) dentro de un pro-
grama.

Para elegir el nombre de uno de esos elementos debemos seguir dos reglas fundamentales:

1. Debe ser un identificador válido dentro del lenguaje Java:

22
Esto es obligatorio.
Todos los lenguajes tienen reglas para restringir los identificadores que se pueden usar.
Ejemplo: una variable puede llamarse apellido1, pero no 1apellido.

2. Debe ser descriptivo, indicando a través del nombre la utilidad de ese elemento dentro del pro-
grama:

No es obligatorio, . . .
pero sı́ recomendable, aumenta la claridad del programa y lo hace más comprensible para
otros programadores.
Ejemplo: si una variable va a guardar la altura de algo, en lugar de llamarla a, es preferible
que se llame altura. En el mismo nombre ya está expresado el uso que va a tener y el dato
que va a guardar.

La variable suma, no solamente tiene un identificador válido, sino que además es descriptiva de su
utilidad: se emplea para guardar el resultado de la suma de los dos números enteros. Esto facilitará la
comprensión de nuestro programa cuando lo lea otro programador.
Pero volvamos al primer punto, los identificadores válidos. En Java, como en cualquier otro lenguaje
de programación, no se puede utilizar un nombre cualquiera como identificador. Hay nombres que por
su composición no están permitidos. Las reglas básicas son:

Debe comenzar con una letra del alfabeto, el carácter de subrayado ( ), o el signo dólar ($).

Es decir, no puede empezar por un dı́gito.

Los siguientes caracteres, pueden ser cualquier carácter del estándar Unicode, salvo espacios en
blanco. Es decir, á, é, ó, ú, ı́, o nuestra querida ñ, son válidos. Java usa el estándar Unicode (65,535
caracteres) en lugar de la tabla ASCII (256 caracteres). Cuidado porque en otros lenguajes solo se
permiten caracteres ASCII.

No puede haber espacios en blanco.

No pueden coincidir con una palabra reservada del lenguaje. Ver Apéndice A.

Son case-sensitive, Longitud y longitud son identificadores diferentes.

Ejemplos:

Válidos: ancho años $saldo perı́metro π

No válidos: 1número primer valor

Hay un motivo para estas reglas: facilitar la tarea de los compiladores del lenguaje. Es decir, no
son reglas gratuitas. Por ejemplo: que una variable no pueda empezar por un dı́gito facilita que el
compilador pueda reconocer desde la primera letra si lo que está analizando es una constante numérica
o un identificador.

2.6. E/S formateada


En los apartados anteriores hemos estudiado cómo manejar los números enteros con los que tiene que
trabajar nuestro pequeño programa. Lo que nos falta por explicar es cómo permitir al usuario introducir
los números enteros y cómo mostrarle el resultado de su suma. En el Tema 1 vimos cómo mostrar una
cadena de texto en pantalla, ahora necesitamos también mostrar el valor de un número entero. Pero
antes expliquemos algo más sobre la consola y la E/S (Entrada/Salida) estándar.
Hay varias formas de “comunicación” entre los programas y los usuarios:

23
Figura 2.2: Aplicación de consola para sumar dos números

E/S estándar (consola): es el sistema más básico, los datos se leen directamente del teclado y se
muestra en la pantalla. La Figura 2.2 muestra un aplicación tı́pica de consola en la que el usuario
interacciona con el programa a través de una terminal de consola, introduciendo los datos con el
teclado y leyendo los resultados en la propia terminal.
Entornos gráficos: el usuario introduce datos a través de ventanas (formularios) que tienen distintos
campos. Es la forma más habitual ya que hace que el uso de los programas sea más amigable, faci-
litando su utilización por usuarios menos expertos en el dominio de los ordenadores, ver Figura 2.3.

Figura 2.3: Aplicación gráfica para sumar dos números

Por el momento usaremos la forma más básica, la E/S estándar, y en las prácticas de la asignatura
veremos cómo puede un programa en Java mostrar ventanas y solicitar con ellas datos a los usuarios.

2.6.1. Clase Scanner


Existen varias formas para leer datos de teclado en Java. Muchas de ellas tienen cierta dificultad,
especialmente cuando se pretende leer cosas como un número entero. Para reducir toda esa complejidad,
en este curso usaremos las funcionalidades que aporta la clase Scanner.
Para comenzar a leer datos usando la clase Scanner necesitamos hacer tres cosas:

1. Incluir en nuestro programa la clase Scanner (directiva import).


2. Crear un objeto de la clase Scanner.
3. Asociarlo con el teclado, representado por System.in.

Para incluir una clase que viene con el lenguaje Java debemos usar la directiva import. Al principio
del archivo, antes de la declaración de nuestra clase SumaDosNúmeros, se escribe la palabra reservada
import y después el nombre de la clase que queremos importar a nuestro programa, en este ejemplo
Java.util.Scanner (lı́nea 1). Si no pusiéramos la directiva import entonces para usar la clase deberı́amos
poner su nombre completo, Java.util.Scanner, lo cuál puede resultar más incómodo.
Las dos últimas cosas, crear el objeto y asociarlo al teclado, las conseguimos con la instrucción de la
lı́nea 11. El objeto se crea usando el operador new e indicado que la clase de ese nuevo objeto es Scanner.
En la propia creación lo asociamos con el teclado (System.in). El operador new lo estudiaremos más
profundamente en el tema siguiente. Al nuevo objeto le hemos dado como identificador teclado para
indicar su utilidad.

24
1 import java.util.Scanner;

10 //Objeto Scanner asociado con el teclado


11 Scanner teclado= new Scanner(System.in);

Una vez creado el objeto teclado podremos usarlo para leer nuestros datos enteros, llamando al
método nextInt():
14 número1=teclado.nextInt();
15 número2=teclado.nextInt();

Estudiaremos más profundamente qué es un objeto, cómo se crean y cómo se utilizan sus métodos
en el tema siguiente.

2.6.2. Impresión con formato en consola: printf()


Además de los métodos print() y println() que vimos en el Tema 1, existen formas más potentes
que permiten no solamente escribir cadenas de texto, sino otro tipo de información, como los números
de nuestro programa. Además, esa información se puede formatear para que sea más fácil de leer por
parte de los usuarios. Nos estamos refiriendo al método printf().

El método printf() permite realizar una salida formateada, incluyendo tabuladores, justificaciones,
etc.

Permite escribir el contenido de variables de todos los tipos básicos.

Tiene dos parámetros:

1. Una cadena de texto que incluye instrucciones de formato, y


2. la lista de las variables o expresiones que se quieren imprimir.

Cada variable o expresión, normalmente, se inserta por orden en la posición en la que aparece
información de formato en la cadena.

Ejemplos:
System.out.printf("Su suma es %d", suma);
System.out.printf("\n %f \t %d", a, b);

El primero de los ejemplos se corresponde con la sentencia de nuestro programa SumaDosNúmeros, e


imprime el resultado que es un entero ( %d). En el segundo ejemplo, se muestra cómo es posible imprimir
más de un valor, combinado con información para formatear dicha salida. Primero se hace un salto de
lı́nea (\n), después se imprime un número real (los veremos en una sección a continuación), luego se
inserta un tabulador (\t), y por último otro dato entero. La variable a deberı́a ser real y b entera. Vamos
a detallar un poco más todo esto.
En el Apéndice E se encuentra toda la información que se necesita para manejar suficientemente el
método printf(). Aquı́ vamos a mostrar de forma resumida los comandos de formato más habituales.
El texto de formato puede contener:

1. Texto normal: se imprimirá tal cual.

2. Especificadores de formato: implican la impresión de un dato. En la Tabla 2.5 aparece la forma de


imprimir los valores de los diferentes tipos básicos. Una descripción más amplia se encuentra en el
Apéndice E.

3. Secuencias de escape: permiten imprimir caracteres especiales. Comienzan siempre por el carácter
\ seguido de otro carácter. Las secuencias más habituales están el la Tabla 2.6. El Apéndice E
también incluye la información sobre otras secuencias de escape disponibles.

25
Tabla 2.5: Lista de conversiones de formato para los tipos básicos

formato descripción
%d imprime un entero en formato decimal
%f imprime un número real en formato decimal
%c imprime un carácter Unicode
%b imprime “true” o “false” en función del valor booleano

Tabla 2.6: Secuencias de escape más usadas

formato descripción
\n salto de lı́nea
\t tabulador horizontal
\‘‘ comilla doble
\\ barra invertida

2.7. Tipos reales


Nuestro próximo programa tendrá una complejidad muy similar al anterior, pero nos permitirá in-
troducir algunos conceptos nuevos. Las dos diferencias principales serán que, en este caso, manejaremos
números reales en lugar de enteros y usaremos, no solamente variables, sino también constantes.
enun- Realizar un programa que permita al usuario introducir el radio de un cı́rculo (número real) y muestre
ciado su área en la pantalla.

La especificación en UML de la clase que necesitamos crear está en la Figura 2.4.

ÁreaCírculo

+ main(String[])

Figura 2.4: UML - Clase ÁreaCı́rculo

El pseudocódigo del método main() es el siguiente:

Algoritmo 2.2 ÁreaCı́rculo - main()


//PRE: el programa recibirá el radio (en formato real)
Leer el valor del radio del teclado
área ← radio2 ∗ π
Mostrar el área en pantalla
//POS: se muestra el área del cı́rculo en la pantalla

Necesitamos hacer varias cosas nuevas:

Usar números reales en lugar de enteros,


calcular el cuadrado de un número, y
representar la constante π.

Veamos el código en Java que nos permite calcular el área de un cı́rculo.

26
ÁreaCı́rculo.java
1 import java.util.Scanner;

3 /** Lee de teclado el radio de un cı́rculo y muestra su área en la consola


4 * @author los profesores de IP */
5 public class ÁreaCı́rculo {

7 public static void main(String[ ] args) {


8 final double PI=3.1416; //Constante para pi
9 double radio; //variable para el radio
10 //Objeto Scanner asociado con el teclado
11 Scanner teclado= new Scanner(System.in);
12 //Leemos el radio
13 System.out.print("Introduce el radio: ");
14 radio=teclado.nextDouble();
15 double área;
16 área=radio*radio*PI;
17 //Mostramos el área en la pantalla
18 System.out.printf("El área es %f\n",área);
19 }
20 }

La primera cosa nueva es que el programa maneja números reales. Existen dos tipos reales predefini-
dos. Como pasaba con los tipos enteros, la única diferencia entre ellos es el rango de valores que pueden
representar, que de nuevo depende del espacio que ocupan en memoria (ver Tabla 2.7).

Tabla 2.7: Tipos reales

Tipo Espacio Rango de valores


float 4 bytes ±3.4∗10−38 .. ±3.4∗1038
double 8 bytes ±1.7∗10−308 .. ±1.7∗10308

Algunas consideraciones sobre los tipos reales:


El tipo real más usado es double, al tener más precisión.
Pero, para guardar muchos reales (vector) se debe emplear float si tiene la precisión suficiente.
Eso nos permitirá ahorrar memoria. Los vectores los estudiaremos en el Tema 5.
Para leer números reales de teclado con la clase Scanner se utilizan los métodos nextFloat() y
nextDouble().

Para imprimir con printf() se usan los indicadores de formato %e o %f.

14 radio=teclado.nextDouble();
18 System.out.printf("El área es %f\n",área);

2.7.1. Operadores matemáticos con reales


Con ambos tipos reales se pueden realizar las operaciones matemáticas habituales. La sintaxis y
semántica de cada uno de ellos está en la Tabla 2.8.
Como pasaba con los operadores enteros, los operadores matemáticos con reales:
tienen cardinalidad binaria, necesitan dos operandos, y
producen un resultado real.
En nuestro programa necesitamos elevar al cuadro el radio, pero no existe el operador potencia o
“elevado a”. Como en este caso es un cuadrado simplemente, usaremos el operador producto, ∗.
4
! No existe el operador potencia. Debes usar la multiplicación o métodos que realicen la
potencia (Math.pow).

27
Tabla 2.8: Operadores matemáticos, tipos reales

Tipo Operador Operación Ejemplo


Aditivos + suma 4.8 + 2.1 6.9
− resta 2.1 - 4.8 -2.7
Multiplicativos ∗ multiplicación 4.8 * 2.1 10.08
/ división real 4.8 / 2.1 2.29
% módulo o resto 4.8 % 2.1 0.6

Otro detalle importante en que hay que fijarse es que los operadores división y módulo, / y %
respectivamente, calculan el cociente y el resto reales, y no enteros como ocurrı́a con los operandos enteros
(ver Tabla 2.3). Es decir, son los mismos sı́mbolos u operadores pero realizan operaciones diferentes en
función del tipo que tengan sus operandos.

2.8. Constantes
Uno de los elementos nuevos que aparece en este programa es que necesitamos representar la constante
π para poder calcular el área. Aunque una constante sea casi la antı́tesis de un variable, prácticamente
se manejan igual dentro de un programa, aunque obviamente no se podrá hacer las mismas cosas con
cada una de ellas. Veamos primero la definición de lo qué es una constante.

definición Constante: Nombre simbólico que un programa utiliza para referirse a un valor que no cam-
biará durante la ejecución del programa.

Se caracterizan casi por las mismas cosas que una variable:

1. Un identificador para poder referirnos a ellas en el código del programa,

2. el tipo que tiene el valor que representa la constante, y

3. el valor de la constante (no podrá cambiar). Es obligatorio que tengan un valor desde su
declaración.

4
! Una vez creada un constante, nunca se podrá cambiar su valor, como se hace con las
variables.

Para declarar una constante dentro de un programa hay que indicar esos tres elementos:

sintaxis Declaración de una constante:

final tipo nombre = valor;

8 final double PI=3.1416; //Constante para pi

Hay dos diferencias respecto a la declaración de una variable:

1. Se incluye al principio la palabra reservada final. Esto es lo que indica que la declaración es de
una constante y no de una variable.

2. El valor no es opcional. Podrá ser cualquiera de los valores del rango de ese tipo. Precisamente
lo que define una constante es que representa un valor concreto.

28
Para usar una constante en una expresión simplemente tendremos que indicar su nombre, como
hacemos con las variables. Para calcular el área de nuestro cı́rculo tendremos que multiplicar la constante
PI por el cuadrado del radio (usando el operador ∗):

16 área=radio*radio*PI;

Como pasaba con las variables, en una misma declaración es posible declarar varias constantes del
mismo tipo separándolas mediante comas. En este caso, se debe indicar obligatoriamente el valor de cada
una de ellas:

sintaxis Declaración de varias constantes del mismo tipo:


final tipo nombre1 = valor1, nombre2 = valor2, . . . ;

2.8.1. Constantes sin nombre


Declarar una constante explı́citamente no es el único modo de usar una constante dentro de un
programa, aunque sı́ sea la más formal. Otra opción es usar constantes directamente sin declararlas, esto
es, sin darles un nombre ni indicar su tipo. En realidad su tipo va a venir implı́cito por el valor que
tengan. Estas son sus caracterı́sticas y algunos ejemplos:

Es posible utilizar un valor constante sin crear una constante. Por ejemplo,
área = radio * radio * 3.1416;

Aunque no se declaren, las constantes sin nombre tienen tipo. Por ejemplo, la constante 7 es de
tipo int. Y la constante 3.1416 será de tipo double.

Como regla general, se suelen usar cuando se cumplen simultáneamente dos condiciones:

1. Ese valor solo aparece una vez en el programa. Por ejemplo, la cadena “Hola, Mundo” de
nuestro primer programa, también es una constante, una constante cadena. En ese caso, como
la cadena solamente va a aparecer una vez, no merece la pena declararla.
2. La constante realmente no tiene un significado dentro del programa. Cuando las constantes
tienen significado, entonces siempre es mejor darles un nombre. Por ejemplo, si nos referimos
en un programa a los meses del año, siempre es mejor usar constantes con nombre que los
valores del 1 al 12. Es más fácil de entender una sentencia que usa la constante ABRIL, que
otra que use el valor 4, el programador no necesita “convertir mentalmente” el 4 en ABRIL. A
muchos programadores les llevarı́a un tiempo recordar que el cuarto mes es ABRIL.

Problemas de las constantes sin nombre:

1. A veces, dan la impresión de ser números mágicos.


2. Dificultan el mantenimiento. ¿Qué pasa si hay que cambiar su valor alguna vez?

Este último aspecto es muy importante. Imaginemos que tenemos un programa grande en el que se
ha usado en muchas partes el valor constante de π, por ejemplo 3.1416, sin declarar la constante PI, como
hemos hecho nosotros. Si en algún momento necesitáramos cambiar ese valor, por ejemplo para incluir
más decimales y aumentar la precisión de los cálculos, tendrı́amos que sustituir TODAS las apariciones
del valor 3.1416 que hemos usado, por el nuevo valor con más decimales. Es cierto que los entornos
actuales permiten hacer ese cambio con cierta facilidad, pero puede ser que el programa se componga
de varios ficheros, lo cual puede hacer la tarea más tediosa y menos segura. En cambio, si se define la
constante dándole un nombre, simplemente debemos cambiar su valor en el punto en el que la definimos
y tendremos la absoluta certeza de que ese mismo valor se está usando en todo el programa.
En el Apéndice B se detalla una descripción completa de todas las constantes que se pueden usar en
Java con los distintos tipos de datos.

29
2.9. Asignación
La sentencia de la lı́nea 16 tiene más cosas, además del hecho de usar un constante o calcular el
cuadrado de un número. Representa, sin duda, el tipo de sentencias que más se usan dentro de la
programación imperativa. Es una asignación.

definición Asignación: Expresión que permite cambiar el contenido de una variable. Hace uso del operador
de asignación =.

La importancia de la asignación dentro de la programación imperativa es trascendental, ya que este


tipo de programación se basa precisamente en ir alterando sucesivamente el valor de las variables hasta
llegar al resultado final que se quiere computar. La única forma de modificar el valor de una variable es
haciendo una asignación a dicha variable.

sintaxis Asignación:
variable = expresión;

Ejemplo:
16 área=radio*radio*PI;

El elemento de la izquierda (left-value), debe ser obligatoriamente una variable. Nunca podrá apa-
rece una constante por ejemplo.

El de la derecha (right-value), puede ser cualquier expresión. Las expresiones combinan, mediante
operadores, variables, constantes y llamadas a métodos que devuelvan un valor. Los métodos los
empezaremos a estudiar en el tema siguiente.

El sı́mbolo = no significa comprobar si el valor que tenı́a la variable de la izquierda era o es igual al
valor de la parte derecha. Hay que leerlo a futuro, después de la asignación la variable tendrá el valor
de la expresión de la parte derecha. Lo que significa una asignación es que el valor de la variable de la
parte izquierda va a cambiar y va a pasar a tener el valor de la expresión de la parte derecha. El último
detalle importante es que para que eso se produzca, las dos partes, la izquierda y la derecha deben ser
de tipos compatibles.
4
! El tipo de la variable de la parte izquierda y de la expresión de la parte derecha deben ser
compatibles.
El concepto compatible es difı́cil de explicar en este momento. Una primera aproximación serı́a pensar
que el tipo de la expresión de la derecha está contenido en el tipo de la variable de la izquierda. Por
ejemplo, si la parte derecha es de tipo short y la variable de la izquierda es int, entonces la asignación es
correcta ya que el rango del tipo short está contenido en el rango del tipo int y al realizar la asignación
nunca se podrı́a perder información. Todo esto se entenderá mejor tras estudiar la Sección 2.11.2.
De momento, para simplificar las cosas, vamos a hacer que los dos tipos sean siempre el mismo. Por
ejemplo, es lo que ocurre en la asignación de nuestro programa, ambas partes son de tipo double.

2.10. Expresiones
El objetivo de esta sección es entender mejor cómo se ejecutan las expresiones que involucran a
variables, constantes y operadores. La asignación de la lı́nea 16 del programa ÁreaCı́rculo tiene, en
realidad, más complejidad de la que hemos comentado. Es lo que se denomina una expresión.

definición Expresión: Es una combinación de operadores y operandos (constantes, variables y llamadas a


métodos) que produce un resultado (valor) de un cierto tipo.

30
Varias cosas en esa definición:

Se forman al mezclar los operadores con sus operandos.

Todas las expresiones producen siempre un valor.

Ese valor será, como es lógico, de algún tipo.

Tanto el tipo como el valor dependerá de los operandos y de los operadores que se usen.

La expresión de la sentencia de asignación de la lı́nea 16 es:

de tipo double, y

su valor dependerá del valor de la variable radio.

Sin embargo, para entender realmente cómo funcionan las expresiones hay que dominar ciertos con-
ceptos, como la precedencia y la asociatividad, que todavı́a no hemos presentado. Vamos a ilustrarlos
mediante un programa para calcular la media de dos números.
enun- Realizar un programa que permita al usuario introducir dos números enteros y muestre su media (entera)
ciado en la pantalla.

La especificación en UML de la clase que necesitamos crear está en la Figura 2.5.

MediaDosNúmeros

+ main(String[])

Figura 2.5: UML - Clase MediaDosNúmeros

El pseudocódigo del método main() es el siguiente:

Algoritmo 2.3 MediaDosNúmeros - main()


//PRE: el programa recibirá dos números enteros
Leer el valor de los dos números enteros
media ← número1+número2
2
Mostrar la media en pantalla
//POS: se muestra la media en la pantalla

Aunque a primera vista parece sencillo realizar este programa, tiene una pequeña complejidad que
no habı́a aparecido hasta ahora: en la expresión para calcular la media aparecen dos operadores, además
son de grupos distintos, uno aditivo y otro multiplicativo.
Necesitamos comprender algunas cosas nuevas:

Cómo se ejecutan exactamente las expresiones que tienen más de un operador,

qué son la precedencia y la asociatividad, y

entender mejor el operador de asignación.

Veamos el código en Java que nos permite calcular la parte entera del valor medio de dos números
enteros:

31
MediaDosNúmeros.java
1 import java.util.Scanner;

3 /** Lee dos números enteros de teclado y muestra su media


4 * @author los profesores de IP */
5 public class MediaDosNúmeros {

7 public static void main(String[ ] args) {


8 int número1; //Declaramos dos variables enteras
9 int número2;
10 //Objeto Scanner asociado con el teclado
11 Scanner teclado= new Scanner(System.in);
12 //Leemos ambos enteros
13 System.out.print("Introduce dos números: ");
14 número1=teclado.nextInt();
15 número2=teclado.nextInt();
16 //Calculamos su media
17 int media;
18 media=(número1+número2)/2;
19 //Mostramos el resultado en la pantalla
20 System.out.printf("Su media es %d\n",media);
21 }
22 }

2.10.1. Precedencia
Cuando en el pseudocódigo hemos escrito la expresión:
número1 + número2
media ←
2
Se entiende que antes de dividir, se suman los números.
Aplicamos la sintaxis y la semántica de las expresiones matemáticas.
Lo mismo ocurre con las expresiones dentro de un lenguaje de programación. Tendremos que
aplicar sus reglas sintácticas y semánticas.

En nuestro programa, podrı́amos haber escrito:


media = número1+número2 / 2; //No calcula la media!

pero sin embargo hemos puesto unos paréntesis:


18 media=(número1+número2)/2;

El motivo por el que hemos puesto los paréntesis es para que primero se haga la suma y después
la división. La evaluación de las expresiones, en concreto, el orden en que se evalúan sus operadores,
depende de dos conceptos, la precedencia y la asociatividad.

definición Precedencia: Sirve para decidir qué operador debe aplicarse primero cuando en una expresión
aparecen varios operadores de distintos grupos.

Ejemplo: si tenemos la expresión 4 + 5 ∗ 7,

Aparecen dos operadores ∗ y + que pertenecen a distintos grupos, uno es multiplicativo y el otro
aditivo.
El operador ∗ tiene más precedencia que el +. (Ver Tabla 2.9).
El resultado al evaluar la expresión será 39. Y no 63 que resultarı́a si se evaluara primero el operador
+. La Figura 2.6 muestra paso a paso el orden de evaluación de los operadores.
Si quisiéramos que la suma se hiciera primero deberı́amos usar paréntesis.

32
4 + 5 * 7

4 + 35

39
Figura 2.6: Ejemplo de la precedencia de los operadores

Tabla 2.9: Operadores de Java, ordenados según su precedencia (versión reducida de la Tabla D.1)

grupo operadores asociatividad


agrupamiento (expresión) ID
multiplicativos ∗ / % ID
aditivos + − ID
asignación = += −= ∗= /= %=
& = ˆ= | = <<= >>= >>>= DI

Para poder garantizar que nuestra expresión calcule la media debemos usar los paréntesis. Los parénte-
sis permiten cambiar el orden de evaluación de los operadores de una expresión. Si nos fijamos en la
precedencia de los operadores presentes en la asignación a la variable media, mostrada en la Tabla 2.9,
se observa que el operador de mayor precedencia son los paréntesis y el de menor la asignación.
Evaluemos nuestra expresión. Si los contamos, vemos que aparecen CUATRO operadores: =, (), +
y /. Revisando la Tabla 2.9, comprobamos que cada uno pertenece a un grupo distinto. El orden de
evaluación dependerá por tanto de la precedencia de cada uno de ellos:

1. Primero el agrupamiento, (), y dentro de él, la + que es su única operación,

2. después el operador /,

3. y por último la asignación =.

El valor de la expresión (número1+número2)/2 se asigna a la variable media. Justo lo que queremos


hacer. Es lógico que la asignación sea el operador con menos precedencia, ya que ası́ se garantiza que al
final de la evaluación de toda la expresión de la parte derecha se produzca la asignación a la variable. Si
no hubiéramos puesto los paréntesis el orden serı́a: /, + y =, tal como marca la tabla de precedencia de
operadores, y no se calcuları́a correctamente la media.

2.10.2. Asociatividad
Hemos visto en la sección anterior que el orden de ejecución de operadores de distintos grupos depende
de su precedencia. Pero, ¿qué ocurre cuando en una expresión tenemos operadores del mismo grupo?
¿Cuál se evalúa primero? En ese caso, depende de su asociatividad.

definición Asociatividad: Sirve para decidir qué operador debe aplicarse primero cuando en una expresión
aparecen varios operadores del mismo grupo (con la misma precedencia).

Ejemplo: si tuviéramos la expresión 4 ∗ 5/7,

los operadores que aparecen pertenecen al mismo grupo, son multiplicativos,

luego tienen la misma precedencia.

33
Para decidir cuál se aplicará primero debemos recurrir a la asociatividad de ese grupo.
Los multiplicativos tiene asociatividad de izquierda a derecha (ID). Ver Tabla 2.9.
El resultado al evaluar la expresión será 2. Y no 0 que resultarı́a si se evaluara primero el operador
/. Se puede ver el orden de evaluación en la Figura 2.7.

4 * 5 / 7

20 / 7

2
Figura 2.7: Ejemplo de la asociatividad de los operadores

En realidad dominar la asociatividad es mucho más sencillo que dominar la precedencia, donde
tendrı́amos que conocer el orden de todos los operadores del lenguaje y que están recogidos en la Ta-
bla D.1. La regla básica sobre la asociatividad no puede ser más sencilla:

Todos los grupos de operadores tienen asociatividad de izquierda a derecha (ID),


salvo, los de asignación y los unarios, que es de derecha a izquierda (DI).
El hecho de que la asignación tenga asociatividad DI permite asignar a varias variables el mismo
valor.

Ejemplo: expresión a = b = c = 0;

Si la asociatividad de la asignación fuera ID, entonces a la variable a se le asignarı́a b, a esta c y c


valdrı́a 0. Serı́a difı́cil de entender, ya que a cada variable se le asigna un valor distinto.
Al ser de DI, todas valdrán 0.

a = b = c = 0

a = b = 0

a = 0

Figura 2.8: Ejemplo de la asociatividad de los operadores de asignación

4
! El valor que produce una asignación es el valor asignado, esto es el valor de la expresión
de la derecha.
Este detalle suele ignorarse con cierta frecuencia. Surge de que todas las expresiones producen siempre
un valor. Eso incluye, por supuesto, a las expresiones que tienen operadores de asignación. Producirán
como resultado el valor que se asigna y como tipo, el tipo de ese valor. Esto se aprovecha en el siguiente
ejemplo:

34
System.out.printf("Su suma es %d", suma=número1+número2);

A la variable suma se le asigna la suma de las otras dos variables, y el resultado de toda la expresión,
esto es, el valor de esa suma, se imprime en pantalla mediante el método printf(). De todas maneras,
aunque este código serı́a perfectamente correcto, a efectos de claridad es mucho mejor separar la sentencia
anterior en dos, tal como hicimos en el programa SumaDosNúmeros.

2.11. Conversiones
Vamos a cambiar ligeramente el enunciado de nuestro anterior programa para poder explicar cómo
se pueden convertir las expresiones de un tipo a otro.
enun- Realizar un programa que permita al usuario introducir dos números enteros y muestre su media (real)
ciado en la pantalla.

Tendrı́amos varias formas de modificar nuestro programa para que se calculara la media real de los dos
enteros. La primera es declarar directamente las dos variables como reales, por ejemplo de tipo double.
Al hacerlo, la división serı́a real y no entera como ocurrı́a en nuestro programa anterior. Sin embargo,
en esta sección estamos interesados en producir un valor real para la media, pero sin cambiar el tipo de
las dos variables que leemos de teclado. La idea es ver cómo lograrlo respetando a su vez la precondición
que nos indica el enunciado.
Por tanto, básicamente el objetivo es hacer un división real en lugar de entera, pero con variables
enteras. Para conseguirlo debemos tener en cuenta varias reglas semánticas del lenguaje:

Cuando los dos operandos de / son enteros o reales, la división es respectivamente entera o real.
Esto se explicó en las Secciones 2.3.1 y 2.7.1.

¿Qué pasarı́a si uno fuera real y el otro entero?

En ese caso, la división serı́a real.

¿Por qué? El motivo son las conversiones automáticas.

Luego, hay dos soluciones para nuestro problema:

1. Cambiar ambos operandos para que sean reales.


2. Cambiar uno y que el otro se convierta automáticamente.

2.11.1. Operador de conversión


Para cambiar el tipo del resultado de una expresión, la primera opción es emplear el operador de
conversión o casting.

sintaxis Operador de conversión:


( tipo ) expresión

El valor de la expresión se convertirá (cambiará) al tipo indicado entre paréntesis.

¡Ojo! El operador de conversión es uno de los de más precedencia (ver Tabla D.1).

No todas las siguientes expresiones producirı́an el resultado correcto:


media = (double) (número1+número2) / (double) 2;
media = (double) (número1+número2) / 2.0;
media = (double) ( (número1+número2) / 2 );

35
¿Cuál es el motivo? La respuesta tiene que ver con la precedencia de nuevo. El operador de conversión
no deja de ser eso, un operador, luego tiene precedencia y asociatividad como todos los demás. Si estudia-
mos de nuevo la Tabla D.1 vemos que es uno de los que más precedencia tiene, más que los operadores
aritméticos. En el primer ejemplo, después de evaluar el operador + ya que está entre paréntesis, se
realizarı́an las dos conversiones, ya que tienen más precedencia que la división. Como su asociatividad es
de derecha a izquierda, primero se hará la de la derecha: se convertirı́a la constante entera 2 a double. A
continuación se aplicarı́a el otro operador de conversión. La suma, al ser entre dos enteros, producirı́a a
su vez un valor entero, que serı́a convertido a double gracias al operador de conversión más a la izquierda.
Por último, el operador / se evaluarı́a entre los dos operandos reales produciendo otro valor double.
También serı́a correcta la segunda de las expresiones. Pero en este caso se evita la conversión de la
constante entera 2, haciendo que sea una constante double directamente (2.0). Ambas funcionan, pero
esta segunda serı́a preferible.
Sin embargo, en la tercera expresión los paréntesis añadidos cambian el orden de evaluación. Primero
se hace la suma, ya que está en los paréntesis más internos, pero luego la división. Dado que la suma
es entera y 2 también es una constante entera, la división serı́a entera, es decir, su valor no tendrı́a
decimales. Por mucho que después se convirtiera dicho valor a double, los decimales de la división se
habrı́an perdido ya, con lo que el valor final producido serı́a real pero siempre tendrı́a sus decimales a 0,
produciendo un resultado incorrecto en la mayorı́a de los casos (no siempre la media real de dos enteros
da un valor sin decimales).

2.11.2. Conversiones automáticas


A pesar de que hemos visto dos soluciones posibles a nuestro problema, podemos encontrar una
solución todavı́a más elegante. La forma de lograrlo es hacer uso de un poco de ingenio y de saber
qué son y cuándo se producen las conversiones automáticas entre los tipos básicos.

definición Conversión automática: Conversión que se produce implı́citamente, sin que la indique el progra-
mador, cuando en una expresión sus operandos pertenecen a diferentes tipos básicos.

Varias consideraciones sobre las conversiones automáticas:

En todos los operadores binarios matemáticos, relacionales o de comparación, los dos operandos
deben ser del mismo tipo. En el Apéndice C aparecen descritos todos esos operadores.

Cuando son de tipos diferentes, el operando del tipo “menor” se convierte al tipo “mayor”. Por ese
motivo a las conversiones automáticas se les llama promoción.

• El rango de valores del tipo menor está “contenido” en el rango de valores del tipo mayor.

En una asignación, solo la parte derecha se puede promover.

En una conversión automática NO se pierde información.

La secuencia de conversión entre los tipos numéricos, enteros y reales es la siguiente:

byte short
int long float double
char
Figura 2.9: Conversiones automáticas entre los tipos básicos

El esquema anterior debe interpretarse de la siguiente forma. Todo tipo, cuando es necesario, se
puede convertir automáticamente a los tipos con los que está conectado y que aparecen a su derecha.
Por ejemplo, si en una expresión aparece un operando short y otro de uno de los tipos que aparecen a la
derecha de short en el esquema, enlazado con flechas directa o transitivamente, por ejemplo long, entonces
dicho operando se promoverá automáticamente al otro tipo. En cambio, un tipo no se convertirá nunca

36
de forma automática a los tipos que tiene a su izquierda en el esquema. El motivo es que en todos esos
casos se podrı́a perder información. Por ejemplo, si pretendemos meter un dato long en un short es
posible que el valor esté fuera del rango de valores del tipo short, con lo que en la conversión se perderı́a
información. Para lograr ese tipo de conversiones hay que hacerlo explı́citamente mediante el operador
de conversión, bajo la responsabilidad del programador.
Como el tipo char se representa internamente como un entero, se puede convertir a los tipos enteros
grandes y a los reales. Únicamente no se puede convertir a los tipos enteros de rangos más bajos: byte
y short. No hay conversiones automáticas entre estos dos tipos y char en ninguno de los dos sentidos.
Las conversiones entre el tipo char y los enteros como int son bastante frecuentes, ya que en muchas
ocasiones se quiere conocer, no la letra que representa el carácter, sino el código del mismo, lo que se
consigue con la conversión de char a int. Eso se aprovecha en el siguiente ejemplo para obtener el código
de la letra ’a’.
int código=’a’;
System.out.printf("Código %d", código);

Todo lo comentado es, por supuesto, aplicable al operador de asignación, como ocurre en el ejemplo
anterior. En este caso, el operando que define la operación es el que está a la izquierda. Si es de un tipo
“mayor” que el tipo del valor de la expresión a la derecha, este se promoverá automáticamente para
actualizar el valor de la variable con un valor de su mismo tipo. Si por el contrario, el tipo de la variable
a la que se está asignando el valor es “menor“ que el tipo de la expresión de la derecha, entonces se
genera un error, salvo que se realice una conversión explı́cita con el operador de conversión.
Haciendo uso de las conversiones automáticas, podemos simplificar aún más las soluciones a nuestro
problema para obtener la media real a partir de dos enteros:
media = (double) (número1+número2) / 2;
media = (número1+número2) / 2.0 ;

En ambos casos la división será real:

La idea es que solamente uno de los dos operandos sea double.


El otro se convertirá automáticamente gracias a un conversión automática.
En el primer caso usamos el operador de conversión para el numerador y la constante entera 2 se
convertirı́a automáticamente a double.
En el segundo, al ser el denominador una constante double, lo que se convierte automáticamente
es el numerador.

La más elegante de las dos es la segunda, por lo que es la solución que se muestra en el listado de
nuestro programa.

MediaRealDosNúmeros.java
1 import java.util.Scanner;

3 /** Lee dos números enteros de teclado y muestra su media (real)


4 * @author los profesores de IP */
5 public class MediaRealDosNúmeros {

7 public static void main(String[ ] args) {


8 int número1; //Declaramos dos variables enteras
9 int número2;
10 //Objeto Scanner asociado con el teclado
11 Scanner teclado= new Scanner(System.in);
12 //Leemos ambos enteros
13 System.out.print("Introduce dos números: ");
14 número1=teclado.nextInt();
15 número2=teclado.nextInt();
16 //Calculamos su media
17 double media;
18 media=(número1+número2)/2.0;
19 //Mostramos el resultado en la pantalla
20 System.out.printf("Su media es %f\n",media);

37
21 }
22 }

38
Tema 3

Clases y Objetos
Objetivos
Comprender la diferencia entre los conceptos clase y objeto.
Utilizar objetos de clases ya implementadas.
Ser capaz de diseñar un clase sencilla, con atributos y métodos.
Entender la diferencia entre la representación en memoria de los tipos básicos y los tipos referen-
ciados.
Comprender las ventajas que ofrece la encapsulación y cómo se logra al programar una clase.

3.1. Introducción
El objetivo principal de este tema es presentar cómo se puede realizar un programa sencillo usando
clases y objetos. Como comentamos en el Tema 1, en esta asignatura pretendemos introducir los prin-
cipios básicos de la Programación Orientada a Objetos. Este paradigma de programación se basa en
descomponer los programas en clases, cuyos objetos cooperarán entre sı́ para solucionar la tarea que
resuelve el programa. Aunque en los temas anteriores ya hemos hecho clases, como vimos desde el pro-
grama HolaMundo todos los programas en Java tienen que tener una clase, nunca hemos usado las clases
en el sentido en el que la Programación Orientada a Objetos las define. En este tema es donde vamos a
empezar a usarlas realmente de ese modo.
Los principios básicos de la Programación Orientada a Objetos podrı́an resumirse en los siguientes
aspectos:
Analiza los objetos que intervienen en un programa:
1. cómo son, y
2. cómo se comportan.
Busca en los enunciados los sustantivos que aparecen (serán los objetos) y los verbos (las acciones
que podrán realizar).
Todos los objetos que presentan un mismo comportamiento son de la misma clase.
La clase es el elemento central de la Programación Orientada a Objetos.
Dado un problema, se construye una colección de clases.
El programa usará objetos, instancias de esas clases, para cumplir con sus requisitos de funciona-
miento.
La diferencia fundamental entre los programas anteriores y muchos de los que haremos a partir de
ahora, es que la resolución de los mismos se realizará creando y usando objetos de las clases que definamos,
y no simplemente ejecutando sus métodos main().

39
3.2. Clases y objetos
Vamos a ver con un sencillo ejemplo cómo se puede realizar un pequeño programa usando el paradigma
de la Programación Orientada a Objetos. Inicialmente nos servirá para explicar la diferencia entre los
dos primeros conceptos importantes del tema y que es fundamental aprender a distinguir, lo que es una
clase y lo que es un objeto. El programa que emplearemos es uno de los que usamos en en el Tema 2,
calcular el área de un cı́rculo (ver Sección 2.7).
enun- Realizar un programa que permita al usuario introducir el radio de un cı́rculo (número real) y muestre
ciado su área en la pantalla.

Lo primero que debemos hacer para abordar el programa es no pensar en cada pequeño detalle
que tendrá (en este caso, cómo se calcula el área), sino en identificar los objetos que aparecen en él.
Esta manera de proceder se denomina abstracción y es una idea esencial a la hora de realizar la fase
de análisis de un programa: nos centramos en la funcionalidad que debe tener, en todas las cosas que
debe hacer, pero sin pensar en cómo se hará luego internamente cada una de esas funcionalidades. Eso
se pensará en la fase de diseño. En este problema resulta realmente simple identificar los objetos que
aparecen: el programa debe manejar objetos que representen cı́rculos.
Una vez detectada la clase de objetos que deben manejarse, nos fijaremos en los dos elementos
principales que contiene todo objeto: los datos que lo definen y el comportamiento o las acciones que
puede realizar. En la fase de análisis tendremos que identificar todos esos elementos. En el ejemplo que
nos ocupa no resulta complicado:

Las clases de objetos que se manejan → Cı́rculo


Cada clase se definen por dos elementos principales:
1. Los datos que definen esos objetos → radio
2. Las acciones que pueden realizar → calculaÁrea

Este análisis inicial nos darı́a una primera aproximación a la especificación de la clase Cı́rculo que
vamos a crear. Su representación en UML aparece en la Figura 3.1. A diferencia de las clases del tema
anterior, nuestra clase Cı́rculo sı́ tiene atributos o datos, por eso en la primera sección aparece el nombre
del campo radio. Como pasaba en todos los programas estudiados, en el segundo bloque aparecen las
funciones que la clase realiza. Allı́ eran los métodos main() y aquı́ el método calculaÁrea().

Círculo
- radio: double

+ calculaÁrea(): double

Figura 3.1: UML - Clase Cı́rculo (versión inicial)

Veremos a lo largo del tema que esta primera aproximación a nuestra clase Cı́rculo es demasiado
simple y que vamos a necesitar más cosas para darle a la clase toda la funcionalidad que precisaremos
para poder hacer nuestro programa. Por ejemplo: debemos usar una constante para π (¿cómo declararla?)
y necesitaremos métodos para modificar el radio de un objeto de la clase Cı́rculo. Pero antes de entrar
en esos detalles técnicos, vamos a definir formalmente los conceptos de clase y objeto.

definición Clase: Representación abstracta de un conjunto de objetos que se comportan igual.

Concretando un poco más esta definición podrı́amos decir que:

Una clase es la definición de un nuevo tipo de objetos.

40
Determina qué datos los describen y las funciones que hacen.

Puede entenderse como una extensión de los tipos de datos que estudiamos en la Sección 2.2:

• definen un nuevo tipo de dato,


• con las operaciones que pueden hacer gracias a sus métodos.

Informalmente, una clase puede verse como el molde que nos permite crear objetos del mismo tipo.

Volviendo a nuestro ejemplo, debe apreciarse que todos los cı́rculos “del mundo” tienen los mismos
datos, su radio. Dos objetos de la clase Cı́rculo podrán tener un valor distinto para su campo radio,
pero todos ellos tendrán un atributo radio. Del mismo modo, todos los objetos Cı́rculo podrán calcular
su área. Es decir, todos ellos pueden hacer las mismas cosas. Esta representación común es lo que reflejan
las clases.
Su sintaxis ya la vimos en el primer tema (ver Sección 1.4.1) y la estudiaremos con más profundidad
en el Tema 6. De momento seguiremos usando la misma sintaxis ya vista:

sintaxis Declaración de una clase:


public class nombre {
elementos que contiene la clase (atributos+métodos)
}

En nuestro ejemplo:
4 public class Cı́rculo {
...
28 }

Es especialmente importante distinguir entre clases y objetos. Las clases representan la definición
de cómo son un determinado tipo de objetos. Puede haber muchos objetos de una misma clase, pero
la clase en sı́, es una. Podemos tener muchos cı́rculos diferentes, pero el concepto de lo que es y lo que
puede hacer un cı́rculo es uno solo. Ese concepto único es la clase y cada cı́rculo que utilizaremos en un
programa será un objeto concreto (con unos datos concretos) de esa clase. Crear una clase equivale a
hacer un molde que nos servirá para, a partir de él, crear muchos objetos que se comporten igual.

definición Objeto: Cada una de las instancias creadas a partir de una clase.

Es decir:

Un objeto es cada variable cuyo tipo es una clase.

El objeto tendrá todos los elementos que define su clase:

1. sus atributos o campos, y


2. los métodos o acciones que realiza.

A los objetos también se denomina instancias, y al hecho de crear un objeto a partir de una clase,
instanciación.

Para poder usar un objeto en un programa, primero hay que crearlo.

Se crean de forma similar a las variables, salvo que su tipo, en lugar de ser un tipo básico, será el
nombre de su clase.

41
Si reflexionamos un poco, el concepto de clase no deberı́a resultar tan desconocido, hemos visto varias
“clases”, la clase número entero o la clase número real. Ambas representan datos, datos que a su vez
pueden realizar operaciones, no con funciones pero si con los operadores, que al fin y al cabo vienen a
ser algo muy parecido. Hay una analogı́a evidente entre una clase y un tipo de dato, en realidad definir
una clase es crear un nuevo tipo de dato, que guardará datos, y que tendrá, como pasa con los enteros
o los reales, operaciones para manipular esos datos. A su vez, la relación entre una clase y sus objetos
es exactamente la misma relación que tenı́amos entre un tipo de dato básico y las variables de ese tipo:
el tipo entero es único, pero variables de tipo entero hay muchas. No se puede crear un objeto sin que
antes exista su clase, del mismo modo que no se podrı́a crear una variable de tipo int si no existiera ese
tipo básico. La analogı́a entre tipo-variable y clase-objeto es conceptualmente clara.
En nuestro ejemplo, podemos crear un objeto de la clase Cı́rculo del siguiente modo:
9 Cı́rculo c = new Cı́rculo();

Como se aprecia, el tipo que le hemos dado no es un tipo básico como int o double, sino que es de
tipo Cı́rculo. El resto de la declaración la estudiaremos en la Sección 3.5.
Una vez que tenemos clara la diferencia entre los conceptos de clase y objeto, vamos a definir nuestra
primera clase. Una vez creada, podremos resolver nuestro problema haciendo un programa que usará ob-
jetos de la clase Cı́rculo. Vamos a añadir a nuestra clase algunos métodos más para darle más funcio-
nalidad. Todos ellos aparecen en la especificación de la Figura 3.2. Explicaremos por qué son necesarios
los métodos getRadio() y setRadio() en las próximas secciones.
El listado completo de la clase Cı́rculo podrı́a ser el siguiente:

Ejemplo1Cı́rculo/Cı́rculo.java
1 /** Representa objetos Cı́rculo, con un campo Radio
2 * @author los profesores de IP
3 * @version 1.0 */
4 public class Cı́rculo {
5 /**Valor del radio del objeto Cı́rculo*/
6 private double radio;
7 /**Constante matemática pi */
8 private static final double PI=3.1416;

10 /**Devuelve el valor del radio del objeto Cı́rculo


11 * @return el radio del objeto */
12 public double getRadio() {
13 return radio;
14 }

16 /**Cambia el valor del radio del objeto, para que valga r


17 * @param r nuevo valor para el radio del objeto
18 * @return nada */
19 public void setRadio(double r) {
20 radio=r;
21 }

23 /**Devuelve el área del objeto Cı́rculo


24 * @return el valor del área del Cı́rculo*/
25 public double calculaÁrea() {
26 return PI*radio*radio;
27 }
28 }

3.3. Atributos
Lo primero que debemos definir al crear una clase son los datos que tendrán sus objetos. Se denominan
de muchas formas: campos, atributos, propiedades, . . .

definición Atributos o Campos: Son los datos que tienen los objetos de una clase.

Su sintaxis es muy similar a la declaración de una variable:

42
sintaxis Declaración de un atributo:
[private | public] [static] tipo nombre;

Sin embargo, presenta algunas diferencias:


Se declaran fuera de todos los métodos. En las clases anteriores, todas las variables las declarábamos
dentro del método main(). Para crear un atributo hay que poner su declaración fuera de todos los
métodos.
Se deben declarar como privados usando la palabra reservada private. Solo serán accesibles den-
tro de la clase. Aunque en la sintaxis es posible declarar un atributo como público o privado,
en la práctica lo recomendable es hacerlos todos privados. Esto encapsulará los datos y permi-
tirá ocultárselos al usuario de la clase. Este es un aspecto esencial en el diseño de una clase. Lo
analizaremos en la Sección 3.6.
No suelen ser estáticos, cada objeto tendrá su atributo. Un elemento estático es compartido por
todos los objetos de una clase, es decir, pertenece a la clase en sı́, no a cada objeto individual.
Como queremos que cada objeto tenga su propio valor en un atributo, en la declaración no se pone
la palabra reservada static. Estudiáremos más sobre los elementos estáticos de una clase en el
Tema 6.
Puede dárseles un valor inicial, aunque no se suele hacer.
La clase también puede tener constantes (estáticas). En ese caso se pondrı́a la palabra reservada
final antes del tipo del atributo. Las constantes sı́ suelen ser estáticas, ya que de está forma las
comparten todos los objetos de la clase. De ese modo no se desperdicia espacio haciendo que cada
objeto de la clase tenga que guardar el valor de una misma constante.

5 /**Valor del radio del objeto Cı́rculo*/


6 private double radio;
7 /**Constante matemática pi */
8 private static final double PI=3.1416;

En la clase Cı́rculo hemos creado, siguiendo la especificación, un atributo radio de tipo double, pero
además hemos añadido la constante PI para poder calcular el área del objeto. Como toda constante debe
tener un valor inicial. No es el caso de los atributos no constantes. Por defecto, cuando se cree un objeto
de la clase Cı́rculo el valor del atributo radio será 0, ya que es de un tipo básico. En su declaración,
los atributos no se suelen inicializar, ya que cada objeto de la clase tendrá valores diferentes. La forma
de inicializarlos será a través de unos métodos llamados constructores, cuya misión principal es esa,
inicializar los atributos de los objetos. Estudiaremos todo lo necesario sobre constructores en el Tema 6.

3.4. Métodos
Una vez declarados los atributos de una clase, el siguiente paso es crear los métodos de la clase.

definición Métodos: Son las funciones o acciones que pueden realizar los objetos de una clase.

La sintaxis para declararlos es idéntica a la que vimos en la Sección 1.4.2 cuando estudiamos cómo se
creaban los métodos main(). Ahora vamos a detallarla con más precisión, indicando para qué sirve cada
uno de los elementos de la declaración. Primero, su sintaxis:

sintaxis Declaración de métodos:


[private|public] [static] tipo nombre (lista de parámetros) {
código
}

43
Es una declaración más compleja que la de un atributo:

Se suelen declarar como públicos, usando la palabra reservada public. Serán accesibles fuera de
la clase. Aunque de nuevo la sintaxis permite declarar un método como público o privado, lo más
habitual es que casi siempre sean públicos. Sin embargo, en este caso la regla es menos estricta y
sı́ pueden existir métodos que sea más conveniente declararlos como privados. Veremos los motivos
en la Sección 3.6.

Un método estático (static) no necesitará de un objeto para poder ser llamado. Es el caso de
los métodos main() que hemos usado, que se ejecutaban sin que creáramos un objeto. Cuando
los métodos no son estáticos, hace falta crear un objeto e invocar el método usando ese objeto,
indicando que la acción a realizar tiene que realizarse sobre dicho objeto.

tipo: es el tipo del valor que devuelve el método. Si no devuelve nada se pone la palabra reservada
void. Como tipo de retorno de un método se puede poner cualquier tipo básico o el nombre de una
clase, que también es un tipo.

parámetros: lista (separada por comas) con los parámetros que el método necesita. Por cada uno
se indica tipo y nombre. El nombre lo usaremos en el método para referirnos al parámetro.

En el listado del código fuente de la clase aparece la implementación de todos los métodos de nuestra
clase Cı́rculo.

3.4.1. Métodos que devuelven un resultado


Podrı́amos distinguir dos tipos de métodos en función de si producen o no un resultado:

1. Métodos que no producen resultados (métodos void). En algunos lenguajes, las funciones o métodos
que no devuelven un valor reciben el nombre de procedimientos.

2. Métodos que devuelven un resultado, un valor. Su tipo de retorno será algún tipo básico o el nombre
de una clase. Es quizás el caso más habitual.

Para devolver el resultado se emplea la sentencia return

sintaxis Sentencia return:

return expresión;

Podrı́amos resumir sus propiedades en los siguientes aspectos:

La sentencia return hace que el método acabe. La siguiente instrucción a ejecutar será la que vaya
a continuación de la sentencia en la que se produjo la llamada al método.

Por ello suele colocarse al final del código del método. La idea es que la sentencias anteriores, si las
hay, hagan las operaciones pertinentes, y al final se retorne el resultado del método.

Puede haber más de una sentencia return en un método. Desde el punto de vista de la claridad,
es más sencillo de entender un método con una sola sentencia return situada justo al final, ya que
entonces solamente habrá un punto de salida o finalización del método. Si el método tiene varias
sentencias return entonces hay que comprobar en más sitios si el método acaba de forma correcta.

La expresión tiene que ser del mismo tipo que el tipo escogido en la declaración como tipo de retorno
o de un tipo que se pueda convertir de forma automática a él. Por ejemplo, si el tipo de retorno
es double y el valor de la expresión que se retorno es int, entonces dicho valor se convertirá de
forma automática a double para ser devuelto, siguiendo el proceso de conversión automática que
se explicó en la Sección 2.11.2.

44
25 public double calculaÁrea() {
26 return PI*radio*radio;
27 }

Por ejemplo, en el método calculaÁrea() de la clase Cı́rculo, el valor que se retorna es el producto
de la constante PI por el cuadrado del atributo radio. La expresión de esa sentencia return es de tipo
double al ser todos sus operandos de ese mismo tipo. En este caso no es necesaria conversión alguna, ya
que el tipo de la expresión y el tipo del valor de retorno es el mismo.
Un error tı́pico que muchos programadores noveles comenten es colocar alguna instrucción después
de una sentencia return en un esquema secuencial de instrucciones simples. Debe recordarse siempre que
una sentencia return hace que finalice el método, por lo que si después de ella hay alguna otra sentencia
debe verificarse que puedan ser ejecutadas. Esto solo serı́a posible si la instrucción return está dentro
de una sentencia condicional y no se ejecuta en todas las circunstancias. Volveremos sobre esto en el
Tema 4.

3.4.2. Métodos set() y get()


Aunque cumpla el requisito de permitir a los objetos de la clase Cı́rculo calcular su área, la espe-
cificación de la clase dada en la Figura 3.1 no es completa. El motivo es que no tenemos una forma de
acceder y trabajar con el radio de los objetos. Debe tenerse en cuenta que hemos declarado ese atributo
como privado, con lo que el acceso a él solamente puede realizarse dentro de la clase, pero nunca fuera.
Es decir, si en un programa creáramos un objeto Cı́rculo, no tendrı́amos un modo de cambiar su radio.
La forma en la que eso se consigue es mediante unos métodos tı́picos que siempre comienzan por get()
y set():

Los métodos get() y set() permiten a los programas que usan objetos de una clase poder ver y
modificar sus atributos.
Proporcionan la forma de trabajar con los atributos privados.
Se define un método get() y set() por cada atributo.
Su nombre siempre debe empezar por get() o set(), seguido del nombre del atributo. El nombre no
es gratuito, algunas herramientas relacionadas con Java necesitan que los nombre sea precisamente
esos.
A veces también se crea un método set() para poder cambiar a la vez varios atributos de una
objeto.

En nuestra clase como solamente tenemos un atributo, radio, tendremos que crear solamente un
método get() y otro set(). Sus nombres serán getRadio() y setRadio(). La especificación final de
nuestra clase serı́a por tanto la que aparece en la Figura 3.2.

Círculo
- radio: double

+ getRadio(): double
+ setRadio(double)
+ calculaÁrea(): double

Figura 3.2: UML - Clase Cı́rculo (versión inicial + métodos set() y get())

Comencemos por las caracterı́sticas de los métodos get():

Su misión es permitir a los programas obtener el valor de un atributo.

45
Tı́picamente consta de una única sentencia en la que se retorna el valor de dicho atributo.

El valor de retorno del método get() debe ser el mismo que el del atributo para el que está hecho.

No necesita parámetros.

10 /**Devuelve el valor del radio del objeto Cı́rculo


11 * @return el radio del objeto */
12 public double getRadio() {
13 return radio;
14 }

En el listado del clase Cı́rculo aparece el método getRadio(). Obsérvese como no tiene parámetros,
su tipo es double ya que es el tipo del atributo radio, y únicamente contiene una sentencia return para
que el método devuelva el valor de dicho atributo. Para referirnos al campo radio ponemos simplemente
su nombre. El método devolverá el radio del objeto para el que se aplique.
Pasemos ahora a ver las propiedades de los métodos set():

Su misión es permitir a los programas modificar el valor de un atributo.

El valor de retorno del método set() es void, ya que no tiene que devolver nada.

Necesita un parámetro que sea del mismo tipo que el del atributo con el que está asociado.

Su objetivo es garantizar cambios seguros del atributo y asegurar que el objeto siempre está en un
estado correcto.

Como se puede apreciar en el listado del clase Cı́rculo, el método setRadio() no devuelve nada, es un
método void, y tiene un parámetro que es de tipo double, el tipo del atributo radio. Por sencillez debe
darse al parámetro un nombre diferente al nombre del atributo, en esta ocasión le hemos puesto como
identificador r. Lo que hace el método es cambiar el valor del atributo radio, asignándole el valor de r.
El aspecto más interesante de los métodos set() es que permiten mantener siempre un estado válido
del objeto, esto es, que sus atributos siempre tengan valores correctos desde el punto de vista de su
significado. Para ello suelen incluir sentencias condicionales (ver Sección 4.4) que comprueban, antes de
realizar la asignación, que el valor que se va a asignar al atributo es correcto dado lo que significa en la
representación de la clase. Discutiremos más profundamente esto en la Sección 3.6.
16 /**Cambia el valor del radio del objeto, para que valga r
17 * @param r nuevo valor para el radio del objeto
18 * @return nada */
19 public void setRadio(double r) {
20 radio=r;
21 }

3.5. Creación y uso de objetos


En realidad ya hemos usado clases y objetos en todos los programas que hemos hecho. Por ejemplo,
hemos usado en repetidas ocasiones los métodos print(), println() y printf() con el objeto System.out.
Pero la clase que más hemos empleado y de una forma más completa además ha sido la clase Scanner.
En ese caso, no nos hemos limitado a llamar a métodos, sino que hemos creado objetos de dicha clase, los
hemos asociado con el teclado, para finalmente emplear sus métodos, como nextInt() o nextDouble(),
y leer datos de la entrada estándar. Lo que ocurre es que hasta ahora lo habı́amos hecho de una forma
intuitiva, ahora vamos a explicar formalmente cómo se hace y lo que significa cada uno de los elementos
que usamos en todas esas sentencias.
En el siguiente listado aparece un ejemplo de cómo utilizar la clase Cı́rculo para finalmente cumplir
con los requisitos de nuestro enunciado y crear un programa que lea de teclado el radio de un cı́rculo y
muestre en pantalla su área.

46
Ejemplo1Cı́rculo.java
1 import java.util.Scanner;

3 /** Ejemplo 1 de uso de la clase Cı́rculo


4 * @author los profesores de IP */
5 public class Ejemplo1Cı́rculo {

7 public static void main(String[ ] args) {


8 //Objeto de la clase Cı́rculo
9 Cı́rculo c = new Cı́rculo();
10 //Objeto Scanner asociado con el teclado
11 Scanner teclado= new Scanner(System.in);
12 //Leemos el radio
13 System.out.print("Introduce el radio: ");
14 c.setRadio(teclado.nextDouble());
15 //Mostramos el área en la pantalla
16 System.out.printf("El área es %f\n",c.calculaÁrea());
17 }
18 }

Vamos a comentar paso a paso cada uno de los elementos que tiene el programa. Básicamente hay
que hacer dos cosas para usar un objeto:

1. Crearlo, usando el operador new.


2. Una vez creado, se pueden invocar los métodos asociados a las acciones que queremos que haga el
objeto. Usaremos para ello el operador punto (.).

3.5.1. Crear un objeto: el operador new


Antes de poder usar un objeto en un programa es necesario crearlo. Por ejemplo, en nuestro caso,
antes de poder calcular el área de un objeto Cı́rculo, debemos crear dicho objeto. La sintaxis para
hacerlo es la siguiente:

sintaxis Creación de un objeto (por defecto):


clase nombre = new clase()

Si solamente se declara el objeto, se reserva el espacio para la variable que se referirá a él, pero NO
se crea el objeto.
Cı́rculo c; //El objeto NO se crea

Para crearlo hay que hacer un new y asignárselo a la variable. Normalmente se hace justo en la
declaración del objeto.
Después del nombre de la clase se indica cómo se inicializa el objeto entre paréntesis. En el Tema 6
veremos que se pueden definir distintas formas para inicializar los objetos a través de métodos
especiales llamados constructores.
Si simplemente se ponen () el objeto se inicializa por defecto. Los atributos:
• de los tipos básicos → 0
• de una clase (tipos referenciados) → null (sin crear)
En nuestro programa (lı́nea 9) usamos new e inicializamos el objeto de la clase Cı́rculo por defecto.
Eso significa que su radio valdrá 0. Sin embargo, cuando creamos el objeto de la clase Scanner no lo
inicializamos por defecto, sino que hacemos que esté asociado con la entrada estándar (System.in).
9 Cı́rculo c = new Cı́rculo();
11 Scanner teclado= new Scanner(System.in);

47
3.5.2. Usar un objeto: el operador punto
Para acceder a las funcionalidades que posean los objetos de la clases que usemos en nuestro programa
emplearemos el operador punto (.). La sintaxis es muy sencilla, se indica primero el objeto, después el
punto y finalmente el elemento que se desea utilizar,

sintaxis Operador punto . :


objeto.elemento publico

Debe tenerse en cuenta varios aspectos:

El elemento de la izquierda debe ser un objeto de una clase.


El elemento de la derecha (ya sea un atributo o un método) debe ser público (public). Normalmente
los únicos elementos públicos de una clase son los métodos y las constantes, ya que, cómo hemos
comentado, los atributos suelen ser privados.
Si el elemento fuera privado (private) se producirı́a un error en tiempo de compilación.
El operador . es uno de los que más precedencia tiene, por lo que se accederá al elemento antes de
realizar otras operaciones. Puedes consultar la precedencia de los operadores en el Apéndice D.

14 c.setRadio(teclado.nextDouble());

16 System.out.printf("El área es %f\n",c.calculaÁrea());

En el programa de ejemplo Ejemplo1Cı́rculo.java usamos tanto elementos de la clase Scanner como


de nuestra clase Cı́rculo. Por ejemplo, en la lı́nea 14 usamos un método de cada uno de los dos objetos.
Primero llamamos al método nextDouble() de la clase Scanner para obtener el siguiente número real que
el usuario haya tecleado. El valor que devuelva ese método se pasa directamente al método setRadio()
del objeto c de la clase Cı́rculo. Con ello modificamos su atributo radio de forma que valga el valor
que se ha leı́do de teclado. Para finalizar, invocamos el método calculaÁrea() para imprimir el área del
cı́rculo que representa el objeto c.

3.5.3. Llamadas a métodos


Toda vez que lo normal es que los únicos elementos públicos de una clase sean los métodos de la
misma, normalmente usaremos el operador punto para llamar a métodos de la clase. Dichas llamadas
deben seguir una reglas sintácticas y semánticas para que puedan ser correctas:

sintaxis Llamadas a métodos:


objeto.metodo (lista de argumentos)

Si el método no tiene parámetros, se indica su nombre y los paréntesis. La lista de argumentos


será vacı́a, por ejemplo, como cuando hacı́amos teclado.nextInt(). Es decir, siempre se ponen los
paréntesis.
Los argumentos se pasan separados por comas. Serán por tanto una lista en la que cada elemento
se separa del siguiente por una coma.
Se asignan a los parámetros en orden. Como es obvio, el primer argumento se asigna al primer
parámetro, el segundo al segundo y ası́ sucesivamente.
La lista de argumentos debe coincidir en número con la lista de parámetros que tenga el método. No
tendrı́a sentido realizar una llamada a un método que necesite tres parámetros y pasarle solamente
dos argumentos. El número de argumentos debe ser, naturalmente, el mismo.

48
Pueden ser tanto variables, como expresiones que involucren variables, constantes y operadores.

Lo importante es que el tipo de cada argumento sea “compatible” con el tipo de su parámetro
correspondiente.

Se realizan las conversiones automáticas oportunas en caso de que un argumento y un parámetro


no sean del mismo tipo.

Lo habitual es que los argumentos y los parámetros coincidan en tipo, como en nuestro ejemplo en el
que el método setRadio() tiene un parámetro de tipo double que es precisamente el tipo del argumento
que le pasamos (es el tipo de lo que devuelve el método nextDouble()).
Pero si no coincidieran, el argumento debe ser de un tipo que se pueda convertir de forma automática
al tipo de su parámetro correspondiente. Por ejemplo, si el tipo de un parámetro es int y el tipo del
argumento que se pasa en la llamada es short, a pesar de que no son exactamente del mismo tipo, se
producirá una conversión automática de short a int. El valor de esa conversión será lo que se asigne al
parámetro. Las conversiones automáticas siguen el proceso que se expuso en la Sección 2.11.2.

3.6. Encapsulación de la información: acceso público y privado


La encapsulación es otro concepto muy importante dentro de las ideas en que está inspirada la
Programación Orientada a Objetos.

definición Encapsulación: Consiste en ocultar al programador que usa una clase los detalles de cómo está pro-
gramada internamente dicha clase.

Implica varias ideas:

Una clase es una unidad de código con unas funcionalidades proporcionadas a través de su interfaz
(los métodos que tiene).

El programador que use la clase debe servirse de esas funcionalidades.

No necesita saber cómo está programada por dentro la clase. La idea es similar a cuando empleamos
cualquier equipo electrónico en nuestra casa, por ejemplo, un DVD. Como usuarios sabemos lo que
hace la tecla Play, y la usamos, aunque no sepamos internamente lo que ocurre cuando pulsamos
dicha tecla dentro del propio aparato. De forma análoga, el programador que usa una clase no
necesita saber cómo funciona o está programada por dentro la clase.

Una clase debe proporcionar una unidad de código sin errores, eficiente y que además sea resistente
a los errores que puedan cometer los programadores que la usen.

La parte esencial para “encapsular” correctamente una clase es decidir qué miembros serán públicos
y cuáles privados. Como hemos visto, los miembros de una clase pueden declararse de una de las dos
formas. Cuando se diseña una clase debemos decidir qué elementos de la clase serán accesibles desde
los programas que utilicen los objetos de la clase y qué miembros estarán ocultos y por tanto serán
inaccesibles desde fuera de la clase. Los primeros los definiremos como públicos (public) y los segundos
los declararemos como privados (private). Por defecto, si no indicamos ninguna de las dos los miembros
serán públicos. Ası́ que hay que asegurarse de poner la palabra reservada private delante de todos los
miembros que queramos que sean privados.
No obstante, debe quedar claro que ambas etiquetas tienen sentido fuera de la clase. Dentro de las
clases, en el código de los métodos, no se tiene en cuenta si un elemento es privado o público. Un método
de una clase puede acceder a todos los atributos y métodos de la propia clase, aunque estos sean privados.
Los niveles de acceso sólo intervienen fuera de la clase, nunca dentro.
Analizando la Figura 3.2, cada uno de los elementos viene precedido de un signo + ó −. Esos sı́mbolos
no eran ni mucho menos gratuitos, precisamente lo que indicaban era, respectivamente, si el elemento
debı́a ser público o privado. Como puede apreciarse, cada uno de los métodos estaba especificado como
público (+), mientras el atributo radio se definı́a como privado (−).

49
Para lograr una correcta encapsulación lo principal es proteger el acceso a los atributos, garantizando
de esta forma que los datos siempre tengan valores correctos de acuerdo al significado que tenga cada
atributo para esa clase de objetos:

¿Qué ocurrirı́a si declaráramos los atributos como públicos? Un programador que usara la clase
Cı́rculo podrı́a hacer esto:

c.radio = -1; //El radio no puede ser negativo!!!

Por eso los atributos se hacen privados. Ası́ los programadores que usen la clase no podrán modificar
libremente los valores de los atributos. Esto puede parecer una limitación, pero es todo lo contrario.
Lo que define el estado de un objeto es el valor de sus atributos. Si cualquiera programador que usara
la clase pudiera poner el valor que quisiera en cualquier atributo, posiblemente se producirı́a un
comportamiento anómalo de los objetos de esa clase. Por ejemplo, un radio negativo no representa
a ningún cı́rculo.

Se evita que los usuarios de la clase puedan usarla mal. Se les protege de ellos mismos.

Ni siquiera necesitan saber los atributos que tiene una clase. ¿Conocemos los atributos que tiene la
clase Scanner? A pesar de ello podemos usarla perfectamente. No es necesario saber cómo es por
dentro una clase para usarla y aprovechar sus funcionalidades.

Los métodos set() serán la forma de garantizar que el objeto de una clase esté siempre en un
estado correcto. Podrı́a decirse con cierta razón, que usando el método setRadio() de nuestra clase
Cı́rculo podemos poner el atributo radio a un valor negativo (c.setRadio(-1)). Pero eso solamente
es porque todavı́a no hemos estudiado las sentencias condicionales del Tema 4. Cuando lo hagamos,
el radio de un objeto nunca podrá ser negativo.

Usaremos sentencias condicionales para comprobar que el valor que se pretende asignar a un atri-
buto es válido.

Solamente si lo es, el valor del atributo se actualizará.

Por el contrario, los métodos, dado que implementan las acciones que realizan los objetos, deben ser
públicos. Constituyen la interfaz con la que usar la clase. Siguiendo con la analogı́a del DVD, son como
el mando de un DVD, cada uno de ellos es un botón que permite hacer algo con “el objeto DVD” que
tenemos en nuestra casa.
Por todo ello, la regla general es hacer los métodos públicos y los datos privados. Obviamente hay
excepciones a esa regla, principalmente respecto a los métodos. En muchas clases habrá métodos que
no implementen las acciones principales de los objetos, sino más bien se tratará de funciones auxiliares
que serán utilizadas por otros métodos para definir el verdadero comportamiento de los objetos. Esas
funciones auxiliares serán privadas.

3.7. Documentación Javadoc


Una de los propiedades que indicamos debı́an cumplir nuestros programas es que fuera legibles y
estuvieran bien documentados (ver Sección 1.5). Observando el código fuente de nuestra clase Cı́rculo,
se ve que hemos puesto muchos comentarios del tipo /**. Estos comentarios cumplen básicamente dos
objetivos muy importantes: por un lado sirven para documentar el código fuente por si otros programa-
dores deben entenderlo o modificarlo, y por otra parte, se emplearán para construir de forma automática
la ayuda de la clase para los usuarios de la misma. Esa ayuda se ve por ejemplo en los entornos de
programación para Java cuando en un programa se está empleado un objeto de esa clase.
Este tipo de documentación automática se genera con un herramienta llamada Javadoc y sirve tanto
para documentar la clase en su conjunto, como cada uno de sus atributos y métodos. Para lograrlo se
deben poner estos comentarios antes de la declaración de cada elemento de los la clase, incluida su propia
declaración.

50
3.7.1. Documentación de una clase
Para lograr documentar la clase en su conjunto se debe hacer lo siguiente:

Se genera con un comentario /** . . . */ justo antes de la clase.

En las primeras lı́neas se puede poner una descripción genérica de la clase.

Después se incluyen diversas etiquetas (comandos @):

1. @author: nombre del autor. Se pueden incluir varias de estas etiquetas, una por autor.
2. @version: número de la versión de la clase.
3. @since: fecha de creación de la clase.
4. @see: nombre de otras clases que el usuario podrı́a necesitar consultar. Pueden ponerse varias
etiquetas @see.

No es obligatorio poner todas las etiquetas, solamente las necesarias.

Se puede ver cómo queda en el Eclipse (pestaña Javadoc).

1 /** Representa objetos Cı́rculo, con un campo Radio


2 * @author los profesores de IP
3 * @version 1.0 */
4 public class Cı́rculo {

3.7.2. Documentación de atributos y métodos


En el caso de los atributos y métodos, el proceso es muy similar, aunque con algunas diferencias
importantes en el caso de los métodos:

En ambos casos, lo primero debe ser incluir una descripción del elemento.

Los atributos solamente pueden tener etiquetas @see.

Los métodos incluyen además otro tipo de etiquetas:

1. @param: una por cada uno de los parámetros.


2. @return: explicación de lo que el método retorna como resultado.
3. @see: información adicional que el usuario podrı́a necesitar consultar.

Las etiquetas @param y @return sirven para especificar formalmente los métodos.

Es muy importante este último. Además de para generar una documentación útil de la clase para
los usuarios de la misma, las etiquetas @param y @return nos deben servir para especificar formalmente
lo que los métodos hacen. Es decir, no son simplemente una ayuda para los usuarios, también tienen
mucha utilidad para el programador, ya que tiene un comentario muy útil del método, que le indica
lo que significa cada parámetro y lo que la función debe hacer. Es exactamente la forma de especificar
formalmente un método, y son equivalentes a la Precondición (los parámetros que el método recibe) y a
la Postcondición (lo que devuelve) que estudiamos en el Tema 1.
6 /**Valor del radio del objeto Cı́rculo*/
7 private double radio;
16 /**Cambia el valor del radio del objeto, para que valga r
17 * @param r nuevo valor para el radio del objeto
18 * @return nada */
19 public void setRadio(double r) {

51
3.8. Reutilización
Uno de los conceptos más importantes y más provechosos de emplear una Programación Orientada
a Objetos es el concepto de la reutilización de código.

definición Reutilización: Consiste en diseñar código que pueda (re)usarse en muchos programas.

Es decir:

A veces el objetivo no es crear un programa.

El objetivo puede ser crear clases para que otros programadores las usen.

Lo habitual es crear librerı́as1 de clases que están relacionadas entre sı́.

Por ejemplo: una librerı́a para tratar imágenes.

En este caso, nuestros usuarios son programadores.

Creamos un código que será reutilizado muchas veces.

Ventajas: reducción de costes, código libre de errores y eficiente, . . .

Por ejemplo, cuando usamos la clase Scanner, sabemos que esa clase ha sido probada por cientos
de programadores. Todos los errores que pudiera tener en un principio ya habrán sido depurados y
corregidos, con lo que dicha clase no será una fuente de errores en nuestro programa. Podremos estar
(casi) totalmente seguros que si nuestra aplicación falla será por otra parte del código, con lo que la
detección de dichos errores será más sencilla. Habrá partes, las clases que emplemos, que no serán las
principales candidatas como posibles sitios donde se encuentra el error. Es una de las principales ventajas
de la reutilización, se emplea código eficiente y libre de errores.

3.8.1. Clase Fecha


Vamos a demostrar las ventajas de la reutilización haciendo una clase que podrı́amos usar en muchos
programas.
enun- Realizar una clase para representar fechas, que incluya un método para poder imprimirlas en formato
ciado dd/mm/aaaa.

En este caso el enunciado nos pide explı́citamente crear una clase Fecha. Sin embargo, el enunciado
omite totalmente los datos que tendrı́an los objetos de esa clase. Obviamente todo el mundo sabe que
una fecha se identifica por tres atributos: dı́a, mes y año. Por otro lado, en este caso de forma precisa,
se nos indica que la clase debe incluir un método para imprimir la fecha en un cierto formato. Teniendo
en cuenta todos esos detalles y los métodos get() y set() necesarios para proporcionar una interfaz
a los programadores que vayan a utilizar nuestra clase, su representación en UML podrı́a ser la de la
Figura 3.3. Obsérvese cómo, ya en la propia especificación, estamos indicando qué elementos deben ser
privados o públicos mediante los sı́mbolos − y +, respectivamente.
Es importante resaltar que, para ahorrar memoria, definimos los atributos como de tipo short, ya
que con dos bytes podemos representar los años, que serı́a el dato con el rango de valores posibles más
grande. Los dı́as (1 − 31) y los meses (1 − 12) tienen ambos un rango de valores que permitirı́an usar
el tipo byte, sin embargo el año no permite su representación con un solo byte (valor máximo 255). Por
último, hemos incluido un método adicional setFecha() para cambiar todos los atributos de un objeto
Fecha con una sola instrucción.
Aunque no se nos pide en el enunciado, para que se vea cómo un programador podrı́a usar la clase
Fecha empleando la especificación proporcionada, por ejemplo a través de la representación UML de la
Figura 3.3 y sin conocer internamente el código de la clase (haciendo de nuevo un ejercicio de abstracción),
vamos a mostrar un pequeño programa que lee los valores de una fecha de teclado y los imprime en
1 El término correcto en español serı́a biblioteca, pero el término más empleado habitualmente es librerı́a aunque sea

una traducción incorrecta del inglés library

52
Fecha
- día: short
- mes: short
- año: short

+ getDía(): short
+ setDía(short)
+ getMes(): short
+ setMes(short)
+ getAño(): short
+ setAño(short)
+ setFecha(short,short,short)
+ imprimeFecha()

Figura 3.3: UML - Clase Fecha (versión inicial)

formato dd/mm/aaaa. Todo ello usando dos clases de las que no conocemos todavı́a su implementación:
la clase Scanner, que nos proporciona los métodos para el manejo de la E/S estándar, y nuestra clase
Fecha. Primero leemos los datos de una fecha de teclado, después modificamos un objeto Fecha con dichos
valores, y por último imprimimos la fecha en el formato habitual usado el método imprimeFecha().

Ejemplo1Fecha.java
1 import java.util.Scanner;

3 /** Ejemplo 1 de uso de la clase Fecha


4 * @author los profesores de IP */
5 public class Ejemplo1Fecha {

7 public static void main(String[ ] args) {


8 //Objeto de la clase Fecha
9 Fecha f = new Fecha();
10 //Objeto Scanner asociado con el teclado
11 Scanner teclado= new Scanner(System.in);
12 System.out.print("Introduce la fecha (dı́a, mes y año): ");
13 //Variables para leer la fecha y lectura de los datos
14 short dı́a=teclado.nextShort();
15 short mes=teclado.nextShort();
16 short año=teclado.nextShort();
17 //Cambiamos el objeto fecha con los datos leı́dos
18 f.setFecha(dı́a, mes, año);
19 //Mostramos la fecha en la pantalla
20 f.imprimeFecha();
21 }
22 }

Finalmente el código de nuestra clase Fecha podrı́a ser el siguiente:

Ejemplo1Fecha/Fecha.java
1 /** Representa objetos Fecha
2 * @author los profesores de IP
3 * @version 1.0 */
4 public class Fecha {
5 /**Valor del dı́a del objeto Fecha*/
6 private short dı́a;
7 /**Valor del mes del objeto Fecha*/
8 private short mes;
9 /**Valor del año del objeto Fecha*/
10 private short año;

12 /**Devuelve el valor del dı́a del objeto Fecha

53
13 * @return el dı́a del objeto */
14 public short getDı́a() {
15 return dı́a;
16 }

18 /**Cambia el valor del dı́a del objeto, para que valga d


19 * @param d nuevo valor para el dı́a del objeto
20 * @return nada */
21 public void setDı́a(short d) {
22 dı́a=d;
23 }

25 /**Devuelve el valor del mes del objeto Fecha


26 * @return el mes del objeto */
27 public short getMes() {
28 return mes;
29 }

31 /**Cambia el valor del mes del objeto, para que valga m


32 * @param m nuevo valor para el mes del objeto
33 * @return nada */
34 public void setMes(short m) {
35 mes=m;
36 }

38 /**Devuelve el valor del año del objeto Fecha


39 * @return el año del objeto */
40 public short getAño() {
41 return año;
42 }

44 /**Cambia el valor del año del objeto, para que valga a


45 * @param a nuevo valor para el año del objeto
46 * @return nada */
47 public void setAño(short a) {
48 año=a;
49 }

51 /**Cambia la fecha completa del objeto


52 * @param d nuevo valor para el dı́a del objeto
53 * @param m nuevo valor para el mes
54 * @param a nuevo valor para el año
55 * @return nada */
56 public void setFecha(short d, short m, short a) {
57 setAño(a);
58 setMes(m);
59 setDı́a(d);
60 }

62 /**Imprime la fecha en consola en formato dd/mm/aaaa


63 * @return nada */
64 public void imprimeFecha() {
65 System.out.printf(" %02d/ %02d/ %04d",getDı́a(),getMes(),getAño());
66 }
67 }

Tanto en el diseño de la clase, como en su implementación definitiva, hemos aplicado todos los
conceptos vistos a lo largo del tema:

Los atributos de la clase son privados para impedir accesos incorrectos a dichos campos.
Los métodos son públicos, ya que proprocionan las acciones que pueden hacer los objetos.
Incluimos métodos get() y set() para facilitar una interfaz con los atributos.
Solamente los métodos set() modifican los atributos.
El acceso a sus valores se realiza mediante los métodos get().
Se incluye una documentación apropiada de la clase para facilitar su uso.

54
Hemos creado una trozo código con una funcionalidad clara y bien definida, reutilizable y libre de
errores.

4
! Concentrar el acceso a los atributos en los métodos get()/set() facilita la depuración de
errores y el mantenimiento de la clase.
Para finalizar resaltar un aspecto interesante de nuestra clase Fecha. Como se puede ver, incluso dentro
de la clase, donde podrı́amos acceder directamente a los atributos, también se utilizan los métodos get()
y set() para trabajar con ellos. Por ejemplo, en el método setFecha() hacemos llamadas a los métodos
set() de cada atributo. Y en el método imprimeFecha() se usan los métodos get() para obtener los
valores de los atributos e imprimirlos. Es decir, la clase reutiliza sus propios métodos. La ventaja
de esta manera de programar una clase es que el acceso a los atributos se lleva a cabo únicamente en
los métodos get() y set(). Si se detectara un error en la clase o se decidiera cambiar la representación
interna de los atributos (p. ej. el tipo de dato que tienen), la única parte del código que habrı́a que
revisar o volver a programar serı́an dichos métodos. El resto de código, como emplea la funcionalidad
que ellos proporcionan, no habrı́a que tocarlo. Todo esto tiene que ver con la fase de mantenimiento de
un programa. Ya cuando lo diseñamos, debemos tener en mente hacerlo de tal forma que se facilite la
comprensión del mismo, su posible modificación y la depuración de errores, es decir, todas las posibles
tareas de mantenimiento del programa, en este caso, de la clase.

3.9. Representación en memoria


Para concluir el tema vamos a discutir cómo se representan en memoria tanto las variables de los
tipos básicos que vimos en el Tema 2, como los objetos que hemos aprendido a manejar en este tema.
Este aspecto, como veremos, es muy importante a la hora de manejar correctamente objetos, o en general
variables de tipos referenciados, por ejemplo al hacer asignaciones o al pasarlos como argumentos a una
función. Primero estudiaremos cómo se representan las variables “normales” y luego veremos cómo lo
hacen los objetos. Verás que la representación difiere de forma importante.

3.9.1. Variables de los tipos básicos


Las variables de los tipos básicos se representan en memoria del siguiente modo:

Se guardan en una estructura denominada Pila (Stack en inglés).


Lo que contiene la posición de memoria que representa la variable es el valor de la misma.

Veámoslo con un ejemplo. Si declaramos las siguientes variables:


int a = 1;
double b = 2.0;
int c = 3;
double d = 4.0;

La representación en la Pila será la que se muestra en la Figura 3.4. Las variables se van apilando en
la Pila a medida que se declaran. En esas posiciones de memoria se guardan sus valores.

d 4.0

c 3

b 2.0
a 1

Figura 3.4: Representación en la Pila de variables de los tipos básicos

55
Cuando se realiza una asignación entre dos variables, simplemente se copia el valor de una en la
otra.
Si luego cambiamos una de ellas, la otra no se ve afectada.
c = a; // c pasará a valer 1
a = 7; // c sigue valiendo 1

Es el comportamiento que habı́amos explicado al hablar de la asignación. La única novedad hasta


aquı́ es que las variables se guardan en una estructura llamada Pila, en la que como su propio nombre
indica, las variables se van apilando una encima de otra en orden inverso a como están declaradas. La
última variable declarada está en el tope de la Pila.

3.9.2. Objetos y tipos referenciados


Las cosas cambian cuando lo que se representan son objetos, o en general lo que se llaman tipos
referenciados.

definición Tipo referenciado: Un objeto o variable será de un tipo referenciado cuando se cree con el operador
new.

Es decir, los objetos de una clase son variables de tipo referenciado ya que se crean con new. Las
diferencias respecto a las variables de los tipos básicos están en cómo se representan en memoria:

También se guardan en la Pila.


Pero la variable solo contiene una referencia (dirección) a la posición de la memoria donde está real-
mente el objeto.
Al crearse el objeto con new, este se guarda en otra zona de la memoria llamada Montón (Heap
en inglés).

Imaginemos que declaramos las siguientes variables:


1 int a = 1;
2 double b = 2.0;
3 Circulo c=new Circulo();
4 double d = 4.0;

La representación en la Pila y el Montón será la que se aprecia en la Figura 3.5. De nuevo las variables
se apilan en la Pila. La diferencia está en el objeto c que en este caso es de la clase Cı́rculo. Lo que se
guarda en la variable es la referencia a la posición en el Montón donde se encuentra el objeto que se ha
creado.

d 4.0

c radio 0
b 2.0
a 1

Figura 3.5: Representación en memoria de un objeto

Es muy importante entender la forma en la que se guardan y manipulan los objetos en memoria para
poder comprender qué ocurre cuando se hace una asignación entre objetos de la misma clase. Imaginemos
que añadimos las siguientes sentencias a las declaraciones que hicimos antes.

56
Cı́rculo e;
e = c;

Primero creamos la variable e para contener un objeto Cı́rculo, pero no creamos un nuevo objeto. En
ese momento el valor que contiene la posición de memoria de e es indefinido, ya que está sin inicializar. A
continuación, le asignamos al objeto e, el objeto c. Lo que tiene c es la referencia al objeto en el Montón.
Por ello lo que se asigna es esa referencia, con lo que las dos variables quedan apuntando a un único
objeto. La representación gráfica de esta situación aparece en la Figura 3.6.

e
d 4.0

c radio 0
b 2.0
a 1

Figura 3.6: Asignación entre objetos

Si ahora cambiáramos el radio de e usando el método setRadio():


e.setRadio( 5 );

4
! Estarı́amos cambiando el radio del objeto c.
Esto no habrı́a pasado, como vimos antes, si las variables fueran de un tipo básico. Pero cuando se
hacen asignaciones entre variables de tipos referenciados, lo que se asigna es precisamente eso, su referen-
cia. Esto que hemos analizado con objetos, funcionará exactamente igual con cualquier otro elemento que
sea de un tipo referenciado, que guarde una referencia a elementos que están en otra zona de la memoria
(p. ej. los vectores del Tema 5). Veremos en el Tema 6 la forma en la que, al definir una clase, se puede
crear un mecanismo para producir una copia de un objeto, es decir, un objeto que sea exactamente igual
pero que esté en otra posición de memoria diferente.

3.9.3. Paso de parámetros


Esto mismo que hemos contado aplicado a la asignación sirve también para el paso de parámetros a
un método:

La semántica de una llamada a un método es la inicialización de sus parámetros. Es decir, cuando se


llama a un método sus parámetros se inicializan con los argumentos que se emplean en la llamada.

En Java se inicializan con el valor de sus argumentos.

Es lo que se conoce como paso por valor.

Si tenemos en cuenta la representación en memoria:

1. variables de tipos básicos: se pasa el valor que contienen,


2. tipos referenciados (p. ej. objetos de clases): se pasa la referencia del objeto al que apuntan.

Esto implica que el método puede cambiar un objeto que reciba. Dado que el parámetro tendrá una
referencia al objeto que se pase como argumento podrá modificarlo. Por ejemplo, si un método tie-
ne un parámetro de la clase Cı́rculo podrı́a llamar a setRadio() para cambiar su atributo radio.
Eso cambiarı́a el objeto al que apuntan, tanto el parámetro del método, como el argumento que se
utilizó en la llamada. Lo podemos ver en el ejemplo gráfico de la Figura 3.8.

57
La ventaja que se obtiene es de eficiencia, ya que ningún elemento grande (p. ej. un objeto con
muchos campos) se duplica en memoria.

Vamos a estudiar con un par de ejemplos la diferencia entre los tipos básicos y los tipos referen-
ciados, a la hora de pasarlos como argumentos a un método. Comencemos por los tipos básicos, cuyo
funcionamiento puede resultar un poco más sencillo de comprender.
En el listado siguiente tenemos un método, llamado incrementaInt() que recibe un parámetro entero
que internamente se llama n y que lo modifica incrementándolo en una unidad:
5 public static void incrementaInt(int n) {
6 n=n+1;
7 }

Imaginemos ahora que ese método es usado en un programa principal como el que sigue:
18 public static void main(String[ ] args) {
19 //Variable entera
20 int num=5;
21 System.out.printf("\nAntes de la llamada num= %d",num);
22 incrementaInt(num);
23 System.out.printf("\nDespués de la llamada num= %d",num);
30 }

La variable num se pasa como argumento a incrementaInt(), de forma que su parámetro n recibirá como
valor inicial el valor de num, esto es 5. El método incrementa el valor de n en una unidad, pasando a valer
6. Pero el valor de num no ha cambiado, sigue siendo 5. La representación gráfica de la situación en la
Pila es la que se muestra en la Figura 3.7.

n 6
...
num 5

Figura 3.7: Paso de parámetros con los tipos básicos

4
! Una variable de un tipo básico, usada como argumento en una llamada a un método, no
cambia NUNCA tras dicha llamada.
Veamos qué ocurre ahora con un objeto de una clase, o lo que es lo mismo, con el paso de tipos
referenciados. El ejemplo es análogo al anterior. En este caso tenemos el método incrementaRadio() que
recibe un parámetro de la clase Cı́rculo y lo modifica incrementando en una unidad su radio:
9 public static void incrementaRadio(Cı́rculo c) {
10 c.setRadio( c.getRadio()+1 );
11 }

Si ahora se llama a dicho método en un programa como el siguiente:


13 public static void main(String[ ] args) {
20 //Objeto de la clase Cı́rculo
21 Cı́rculo cir = new Cı́rculo();
22 System.out.printf("\nAntes de la llamada radio= %f",cir.getRadio());
23 incrementaRadio(cir);
24 System.out.printf("\nDespués de la llamada radio= %f",cir.getRadio());
30 }

El objeto cir se pasa como argumento a incrementaRadio(). Dado que se ha definido por defecto, y
no se ha hecho con él ninguna llamada al método setRadio(), el valor de su atributo radio será 0 antes de
dicha llamada. En este caso, lo que recibe el parámetro c será la referencia al objeto al que está apuntado

58
cir.Es decir, durante toda la ejecución de las instrucciones del método, tanto el parámetro c, como el
argumento cir, están apuntando al mismo objeto en memoria. Por este motivo, cuando se cambia el
radio del objeto al que apunta c, indirectamente también se está cambiando el radio del objeto al que
apunta cir, ya que es el mismo objeto.

c
...
cir radio 1

Figura 3.8: Paso de parámetros con los tipos referenciados

4
! Un objeto de una clase, y en general de un tipo referenciado, puede cambiar si se pasa
como argumento en una llamada a un método.
Se debe ser muy cuidadoso con este funcionamiento ya que es posible cambiar, sin querer, objetos
cuando se pasan a métodos. Por ello siempre hay que estar seguro, desde la especificación del método, si
este va a cambiar los parámetros y la forma en la que lo va a hacer.
En definitiva, en cuanto al paso de parámetros Java no puede ser más sencillo, comparado con otros
lenguajes que permiten más formas de pasar parámetros a una función, por ejemplo C++. Por una parte
eso ofrece más sencillez a la hora de pensar en cómo realizar un método ya que no hay más alternativas
que el paso por valor, pero por otro lado es menos potente, ya que no se pueden obtener algunas de las
funcionalidades que se logran en otros lenguajes en los que las opciones para decidir cómo se define un
parámetro de una función son más amplias.

3.9.4. Métodos que reciben objetos de la propia clase


Para finalizar vamos a ver otro ejemplo que se da, muy habitualmente, del paso de objetos como
parámetros a métodos. En este caso se trata de pasar a un método de una clase un objeto de la propia
clase. En la clase Cı́rculo hemos añadido el método copiaRadio() que recibe un parámetro de la clase
Cı́rculo y que modifica el objeto que realiza la llamada haciendo que tenga el mismo radio que el objeto
que se pasa como parámetro. El código del método es el siguiente:
29 /**Cambia el valor del radio del objeto, para que valga lo
30 * mismo que el radio del objeto pasado como parámetro
31 * @param c objeto Cı́rculo del que vamos a copiar el radio
32 * @return nada */
33 public void copiaRadio(Cı́rculo c) {
34 setRadio(c.getRadio());
35 }

La mayor dificultad de esta clase de métodos estriba en que en el código tenemos que acceder a dos
objetos: el que invoca el método y el que se pasa como parámetro. La dificultad, o la confusión que
produce inicialmente, viene propiciada porque el acceso a los atributos o la llamada a los método de la
clase con ambos objetos se hacen de distinta forma en cada caso:

Objeto que invoca el método: se pone simplemente el nombre del atributo o del método. Es exac-
tamente lo que hicimos en todos los ejemplos de este tema. En este caso, llamamos al método
setRadio() que se encargará a su vez de modificar el atributo radio del objeto que invocó el
método copiaRadio() con el valor del argumento que le pasamos.
Objeto pasado como parámetro: es necesario anteponer el nombre del objeto y el operador punto.
En el ejemplo ponemos el nombre del parámetro, en este caso c, a continuación el operador punto,
y por último nombre del método que deseamos utilizar, getRadio(). Obsérvese que el acceso es
idéntico al que realizábamos en el ejemplo del método incrementaRadio(), y a los accesos que
hacemos cuando tenemos un objeto creado en un programa.

59
A la hora de usar un método de este tipo, tenemos que tener en cuenta que necesitamos crear dos
objetos, uno de ellos lo usamos para cambiar el valor del atributo radio del otro:
26 //Usamos un método de la clase Cı́rculo que recibe un objeto Cı́rculo
27 Cı́rculo otro = new Cı́rculo();
28 otro.copiaRadio(cir);
29 System.out.printf("\nradio= %f",otro.getRadio());

Además del objeto cir que ya tenı́amos creado del ejemplo anterior, declaramos y creamos un objeto
nuevo que hemos llamado otro. Inicialmente el valor de su atributo radio es cero. Para hacer que tenga
el mismo valor del atributo radio que tiene el objeto cir lo que podemos hacer es llamar al método
copiaRadio() pasándole como argumento el objeto cir. El método copiaRadio() se encargará de hacer
que el objeto otro tenga el mismo radio que el objeto cir.
En el Tema 6 se verán varios ejemplos de métodos de este tipo, por ejemplo cuando se crea un
constructor de copia (Sección 6.2.3). Para entender el motivo de ese acceso diferenciado con los dos
objetos es necesario comprender qué es y cómo funciona el objeto this (Sección 6.6).

60
Tema 4

Programación estructurada
Objetivos

Comprender los principios de la Programación Estructurada como forma de producir software


eficiente y fácil de entender y mantener.

Conocer el funcionamiento de los esquemas de composición de acciones secuenciales, condicionales


(if-else o switch) e iterativas (while, for o do-while).

Ser capaz de utilizarlos para hacer programas sencillos.

Dominar la escritura de expresiones condicionales complejas usando los operadores && , || y !.

Entender los esquemas iterativos básicos y su combinación mediante bucles anidados.

Ser capaz de identificar el ámbito de una variable y conocer cómo se produce su creación y la
liberación de la memoria que ocupa.

4.1. Introducción
El objetivo de este tema es exponer las distintas sentencias que proporcionan los lenguajes de al-
to nivel, como Java, para realizar una Programación Estructurada que produzca programas fáciles de
comprender. Desde un punto de vista algorı́tmico, éste será el tema más complejo. Estudiaremos los
esquemas algorı́tmicos fundamentales de los que se compone cualquier programa.
Pero antes de entrar en detalles, vamos a explicar por qué surge la Programación Estructurada:

En la construcción de los programas se empleaban saltos incondicionales (sentencias goto). Un


salto incondicional es una instrucción que salta a otra sentencia del código sin depender de una
condición, es decir, salta siempre.

Esto hacı́a que pudiera ser complicado seguir el flujo de un programa y entenderlo. Cuando un
programa tiene saltos incondicionales de un punto a otro del código, es más difı́cil de entender que
cuando las sentencias son secuenciales, sin saltos y pueden leerse seguidas.

Los programas eran difı́ciles de depurar y mantener. Cuanto más difı́cil sea entender un programa,
más complicado resultará detectar los errores que tiene (esto se denomina depuración) o modificarlo
para seguir manteniéndolo en uso.

En los años 70 se determinó que habı́a que seguir un forma más estructurada de escribir los pro-
gramas.

La idea principal es hacer programas imperativos que tengan una estructura lógica, clara y secuen-
cial, sin saltos incondicionales.

61
Detectados los problemas antes citados, se decidió dotar a los lenguajes de programación de las
sentencias necesarias para poder llevar a cabo una Programación Estructurada con las propiedades y
ventajas comentadas. En los programas realizados en los temas previos no hemos hecho uso de los
elementos más importantes de este enfoque de programación. Las novedades que abordaremos en el resto
de este tema podrı́an resumirse en los siguientes aspectos:

No todos los programas pueden resolverse con un secuencia de acciones simples como hemos hecho
hasta ahora.
Hay veces que queremos que determinadas instrucciones solamente se ejecuten cuando se cumple
una cierta condición.
Del mismo modo, una situación muy común en cualquier programa es que ciertas instrucciones se
deban repetir varias veces.
De ambas situaciones surge la necesidad de emplear sentencias condicionales e iterativas.
Junto con las sentencias simples, constituyen los tres elementos que se combinan para diseñar
cualquier algoritmo.
Programar es sencillo, ¡solamente tenemos tres tipos de instrucciones!

Aunque parezca mentira, la Programación Estructurada utiliza únicamente tres tipos de sentencias.
En primer lugar, las instrucciones que hemos empleado en los programas previos, sentencias simples.
Básicamente existen dos tipos de sentencias simples, por un lado las asignaciones y por el otro las
llamadas a métodos. Ambas las hemos usado en los temas anteriores. Además de ellas, y para lograr
cubrir las necesidades antes comentadas, se utilizan sentencias condicionales, en las que las acciones a
realizar dependen de una condición, y sentencias iterativas, en las que las mismas acciones se ejecutan
un cierto número de veces. La Figura 4.1 muestra el diagrama de flujo de cada una de ellas.

definición Diagrama de Flujo: Gráfico que representa el flujo o la secuencia de las acciones que realiza un
algoritmo o una sentencia.

Cada uno de los gráficos expresa el “flujo” de ejecución de las acciones que aparecen. Las flechas
indican la acción que se realizará a continuación. Ası́, en una sentencia simple (izquierda), la acción se
ejecuta siempre, justo después de la acción anterior, y a continuación se ejecutará la instrucción siguiente.
En una sentencia condicional (centro), la acción que se ejecuta depende del valor de una condición: si es
cierta se ejecuta la acción-1 y si es falsa la acción-2. Después de ejecutar la acción que corresponda, el
programa sigue con la siguiente instrucción. Por último, en una sentencia iterativa (derecha) la acción
se ejecuta varias veces, hasta que la condición sea falsa, momento en el que se ejecutará la siguiente
instrucción.
Lo más interesante es que cualquier programa puede ser escrito combinando estos tres tipos de
instrucciones.
4
! El teorema del programa estructurado (Böhm-Jacopini) demuestra que todo programa
puede escribirse utilizando únicamente sentencias simples, condicionales e iterativas.
La complejidad de la Programación Estructurada está en la forma de combinar esos tres tipos de ins-
trucciones. Hay dos formas posibles de combinarlas. La primera es combinarlas secuencialmente, situando
una sentencia tras otra. La segunda forma es anidarlas, esto es, dentro de las acciones de una sentencia
condicional o iterativa, situar a su vez otras instrucciones condicionales o iterativas. La dificultad estriba
en que tanto las posibles combinaciones como el nivel de anidamiento son infinitos.
En la Figura 4.2 pueden verse un par de ejemplos en los que se combinan las secuencias de instrucciones
y los anidamientos. En la parte de la izquierda están combinadas de manera secuencial con anidamientos
muy sencillos (acciones simples). Primero se ejecuta una sentencia condicional, que incluye una acción
simple en cada una de sus dos ramas. A continuación se ejecutará una sentencia simple, y por último una
sentencia iterativa que realizará varias veces una sentencia simple. En el ejemplo de la derecha también
están combinadas de manera secuencial (siempre lo estarán ya que la programación imperativa es en
esencia secuencial), pero en este caso tenemos anidamientos más complejos. En primer lugar tenemos

62
sentencias simples condicional iterativa

T F
acción Condición

acción
acción-1 acción-2

T
Condición

Figura 4.1: Tipos de sentencias de la Programación Estructurada

una sentencia iterativa que repite dos acciones secuencialmente, primero una acción simple y luego una
sentencia condicional. Dicha sentencia condicional tiene a su vez, en una de sus ramas, una sentencia
iterativa. El anidamiento podrı́a ser mayor si, por ejemplo, esta sentencia iterativa tuviera dentro otra
sentencia condicional o iterativa. En el ejemplo simplemente se repite una acción simple. Como se puede
apreciar, las combinaciones que se pueden formar con estos tres tipos de sentencias son muchı́simas, lo
que permite construir cualquier algoritmo.
En los primeros temas nos hemos limitado a realizar secuencias de acciones simples, en las que
únicamente se hacı́an asignaciones a variables y llamadas a métodos de nuestras clases o de otras incluidas
en Java. En este tema vamos a ampliar las posibles sentencias o instrucciones que podremos usar en los
programas. Comenzaremos explicando cómo funcionan las sentencias condicionales.

4.2. Sentencia if
La versión más simple de sentencia condicional es la instrucción if. Veamos cómo usarla en el siguiente
problema.
enun- Modificar la clase Cı́rculo de forma que se garantice que el radio de un objeto de la clase no pueda tomar
ciado un valor negativo.

Básicamente lo que nos está pidiendo el enunciado es que garanticemos que el método setRadio(),
que hicimos en el tema anterior (ver Sección 3.2), nunca cambie el valor del atributo radio cuando el
valor que se va a asignar sea negativo. Traduciendo esta idea a un algoritmo tenemos el pseudocódigo
siguiente:

Algoritmo 4.1 Método setRadio()


Función setRadio (r: real) : nada
si r ≥ 0 entonces
radio = r
fin si

Para lograr escribir en Java este algoritmo tenemos que:

Aprender lo qué es una sentencia condicional.

Conocer las sentencias condicionales que tiene Java.

63
T F

T F

F
T T

F
F

Figura 4.2: Formas de combinar sentencias: secuencias y anidamientos

Saber lo que es una condición.

Escribir la condición adecuada, dominando los operadores relacionales y lógicos.

Comenzaremos estudiando la sintaxis de la sentencia condicional más simple, el if:

sintaxis Sentencia if simple:

if (condición) acción

Sus propiedades podrı́an resumirse en los siguientes aspectos:

La semántica de la sentencia es que la acción solamente se realiza cuando la condición es cierta.

Cuando la condición es falsa, la sentencia no hará nada.

Si la acción está formada por una secuencia de acciones, entonces se deben encerrar entre llaves.

En cambio, cuando solamente tiene una instrucción se debe finalizar con punto y coma (;), indicando
el final de la sentencia if. En realidad nada impide que aunque solamente conste de una instrucción,
ésta vaya encerrada entre llaves. Muchos programadores lo prefieren porque, por una lado permite
visualizar mejor el bloque de sentencias que se ejecuta cuando la condición es cierta, y por otro,
deja el programa mejor preparado por si es necesario añadir alguna acción adicional.

Teniendo en cuenta la sintaxis expuesta, la traducción del pseudocódigo anterior a un código en Java
serı́a la siguiente:

64
T F
Condición

acción

Figura 4.3: Diagrama de flujo de la sentencia if simple

Ejemplo2Cı́rculo/Cı́rculo.java
19 public void setRadio(double r) {
20 if ( r >= 0 ) radio=r;
21 }

En este caso:

La asignación radio=r solamente se ejecuta cuando la condición ( r >= 0 ) es cierta.


Si la condición es falsa, es decir si r es menor que 0, la asignación no se hace.
El método setRadio() garantiza ahora que, en todos los objetos Cı́rculo, el atributo radio siempre
será no negativo. Debe recordarse que el atributo radio era privado, con lo que el acceso fuera de
la clase para modificar su valor solamente podı́a lograrse llamando al método setRadio(). Con la
modificación realizada, eso implica que el atributo nunca podrá tomar valores negativos, lo cuál es
lógico ya que lo que representa (el radio de un cı́rculo) nunca es negativo.

4
! Si lo que se quiere lograr es que se ejecute una acción cuando una condición sea falsa, se
debe escribir la condición complementaria o negarla con el operador de negación (!).
Precisamente lo que vamos a hacer ahora es exponer todo lo que se necesitamos saber sobre condi-
ciones.

4.3. Condiciones
Comenzaremos definiendo formalmente qué es una condición.

definición Condición: Es una expresión lógica, es decir, una expresión que, o bien produce el valor cierto
(true), o bien produce el valor falso (false).

Luego,

Son expresiones que producen valores lógicos (tipo boolean).


Pueden ser:
1. Expresiones simples, con un único término lógico.
2. Expresiones compuestas, que combinan expresiones simples usando los operadores lógicos: Y
(&&), O (||) y NO (!).
Hay cuatro clases de expresiones simples:

65
1. variables de tipo boolean,
2. expresiones con operadores relacionales,
3. expresiones con operadores de comparación (o el método equals() con objetos), y
4. métodos que devuelvan un valor lógico.

Obviamente también es una expresión lógica simple una expresión que simplemente contenga las
constantes true o false. Los valores lógicos suelen utilizarse especialmente para devolver valores en
métodos que retornen un valor lógico.
En el ejemplo en el que estamos trabajando, la condición usa una expresión simple con un operador
relacional (<=).

4.3.1. El tipo boolean


Cualquier dato de un programa debe pertenecer a un tipo. Ası́ vimos en el Tema 2 que 27 es de
tipo int, ó 1.25 es un double. Del mismo modo, los valores lógicos tienen que tener un tipo que defina
precisamente eso, los valores lógicos que existen y las operaciones que se pueden hacer con ellos. En Java
ese tipo se llama boolean. Quizá sea el más sencillo ya que es el que menos valores posibles tiene, el que
menos operadores soporta y el que permite menos posibles cambios o conversiones. Sus propiedades son
las siguientes:

Posibles valores: las constantes true y false.


Operadores: los lógicos y los de comparación. El tipo boolean no permite otros operadores, como
los relacionales o los aritméticos.
No permite ninguna clase de conversiones:
1. No se convierten de forma automática a ningún otro tipo básico.
2. Del mismo modo, ningún tipo básico se convierte automáticamente a boolean.
3. Es más, tampoco se permiten las conversiones usando el operador de conversión en ninguno
de los dos sentidos.
Este hecho es lo que obliga a que las condiciones sean siempre expresiones lógicas. En otros len-
guajes, los tipos enteros se convierten automáticamente a booleanos, con lo que es posible escribir
condiciones que sean expresiones enteras que posteriormente se convertirán a cierto o falso.

4
! El hecho de que el tipo boolean no permita ningún tipo de conversiones obliga a que las
condiciones siempre tengan que ser expresiones lógicas.
En los siguientes apartados vamos a estudiar los distintos operadores que se utilizan para escribir ex-
presiones lógicas y producir valores booleanos. Aunque se pueden utilizar para asignar valores a variables
de tipo boolean, se usan principalmente para escribir condiciones, ya sean de las sentencias condicionales
que estamos estudiando o de los bucles que veremos en la segunda parte del tema.

4.3.2. Operadores lógicos


Para comenzar a describir las expresiones lógicas lo mejor es empezar por los operadores lógicos:

Permiten combinar varias expresiones lógicas simples.


Sus operandos deben ser siempre expresiones lógicas, es decir, de tipo boolean.
Los tres más tı́picos son los operadores Y, O y NO. Los dos primeros son binarios, mientras el último
es unario. Están descritos en la Tabla 4.1 y sus tablas de verdad aparecen en la Tabla 4.2. En el
Apéndice C, la sección C.3 describe todos los operadores lógicos, incluyendo los operadores a nivel
de bit que no estudiaremos en este curso.

Un detalle importante de los operadores lógicos binarios && y || es que están optimizados para
producir el código más eficiente posible. Es lo que se conoce como operaciones en cortocircuito:

66
Tabla 4.1: Operadores Lógicos. Los ejemplos suponen que las variables a = true y b = false. El valor
más a la derecha es el resultado de la expresión

Op. Uso Devuelve true si. . . Ejemplo


&& op1 && op2 op1 y op2 son ciertos a && b false
|| op1 || op2 op1 o op2 son ciertos a || b true
! ! op op es falso !a false

Tabla 4.2: Tabla de verdad para los operadores lógicos Y, O y NO. Las letras T y F representan a las
constantes true y false respectivamente

p q p && q p || q !p
F F F F T
F T F T T
T F F T F
T T T T F

En una operación lógica Y ú O a veces es posible determinar el resultado sin necesidad de evaluar
toda la expresión.

Por ejemplo:

1. Si en una operación Y el primer operando es falso, es evidente que el resultado de la operación


será falso.
2. En un operación O, cuando el primer operando es cierto el resultado será cierto también.

Los operadores && y || hacen uso de esta propiedad para hacer su evaluación más rápida.

En muchos casos no será necesario evaluar el segundo operador.

El código generado es más eficiente y el programa se ejecutará más rápido.

Regla (si podemos determinarlo de antemano): poner primero el operando que con mayor proba-
bilidad sea cierto en una operación || y falso en una operación &&.

Además, hay situaciones, por ejemplo en una expresión &&, en las que la evaluación de uno de los
operandos sólo puede hacerse cuando el otro es cierto. En ese caso lo que se hace es poner primero este
último, y el que depende de él en segundo lugar. De esta forma, cuando el primero es cierto se evalúa
el segundo. Pero cuando el primero es falso, situación en la que no se podrı́a evaluar el segundo, no es
necesario hacerlo ya que la se aplicará la optimización por cortocircuito al saberse que la expresión en su
conjunto es falsa. El ejemplo más habitual de esta situación se produce en las búsquedas asociativas que
empezaremos a estudiar al final del tema (Sección 4.10.3), especialmente cuando se hacen sobre vectores
(Tema 5).

4.3.3. Operadores relacionales


Para poder escribir expresiones lógicas con los operadores lógicos vistos en la sección previa necesi-
tamos combinar expresiones simples. Como comentamos anteriormente, uno de los tipos de expresiones
lógicas simples son las que se forman con los operadores relacionales:

Son binarios y permiten comparar la relación de orden que se da entre sus dos operandos.

Se aplican sobre todos los tipos básicos, exceptuando el propio tipo boolean. Es decir, se puede
aplicar con operandos de los tipos enteros (byte, short, int, long), carácter (char) o reales (float,
double).

67
Todos esos tipos presentan una relación de orden entre sus valores. En los tipos numéricos eso
resulta evidente, todos entendemos entre dos valores enteros o reales cuál es su relación de orden.
La relación de orden también se da entre caracteres, ya que cada carácter viene representando por
un código entero (ver Sección 2.4). Por ese motivo también se pueden ordenar caracteres. De hecho,
en cualquiera de las codificaciones de caracteres habituales (ASCII, Unicode, . . . ), las letras del
alfabeto (a-z), o los dı́gitos (0-9) vienen ordenados de esa forma en la codificación. Esto se utiliza
cuando se quieren ordenar elementos formados por caracteres, como por ejemplo los apellidos de
un grupo de personas.

Si se comparan operandos de distintos tipos básicos se realizarán las conversiones automáticas


oportunas. Ver Sección 2.11.2.

Sea cual sea el tipo de los operandos, el valor de la expresión siempre será boolean.

En la Tabla 4.3 están detallados los cuatro operadores relacionales existentes: menor, menor o igual,
mayor y mayor o igual. Fijándonos en su sintaxis, hay que destacar que los operadores menor o igual y
mayor o igual se escriben con dos caracteres (<= y >=), y no de la forma que se hace habitualmente en
las expresiones matemáticas (≤ y ≥).

Tabla 4.3: Operadores Relacionales

Op. Uso Devuelve true si. . . Ejemplo


< op1 < op2 op1 es menor que op2 7<4 false
<= op1 <= op2 op1 es menor o igual que op2 4 <= 7 true
> op1 > op2 op1 es mayor que op2 7>4 true
>= op1 >= op2 op1 es mayor o igual que op2 4 >= 7 false

Un error muy tı́pico entre los programadores principiantes es escribir las operaciones relacionales
complejas del mismo modo en el que se hacen en Matemáticas.
Ejemplo: comprobar si una variable entera i está entre 0 y 10:
Matemáticas: 0 <= i <= 10
Programación: (0 <= i) && (i <= 10)
¿Por qué no se puede escribir como en Matemáticas?
Es una expresión y como en todas se aplican las reglas de precedencia y asociatividad conocidas.
Si utilizáramos la expresión habitual tı́pica en Matemáticas:
1. Dado que los dos operadores son del mismo grupo y tienen asociatividad ID, primero se hace la
comparación 0 <= i.
2. Esa expresión producirá un valor boolean.
3. Con lo que en la segunda comparación estarı́amos comparando un valor boolean con la constante int
10. Recuérdese que el tipo boolean ni se puede utilizar con los operadores relacionales, ni permite
conversiones automáticas.
4. En Java la expresión produce un error y el programa no se podrı́a ejecutar.

4.3.4. Operadores de comparación


El otro grupo de operadores que se usan en las expresiones lógicas son los de comparación. Están
descritos en la Tabla 4.4, y como se puede ver son solamentemente dos, el de igualdad (==) y el de
desigualdad (! =).
4
! No debe confundirse el operador de asignación (=) con el de igualdad (==).
Las propiedades fundamentales de los operadores de comparación son:

Comparan la igualdad o desigualdad entre sus dos operandos.

68
Tabla 4.4: Operadores de Comparación

Op. Uso Devuelve true si. . . Ejemplo


== op1 == op2 op1 y op2 son iguales 7 == 4 false
!= op1 ! = op2 op1 y op2 son distintos 4 ! = 7 true

Producen un valor boolean.

Se pueden aplicar sobre todos los tipos básicos, incluido el tipo boolean.

Pero:

1. Hay que ser cautos al hacer comparaciones de igualdad entre valores reales. Es muy difı́cil que
dos valores reales sean exactamente iguales. Tendrı́an que ser exactamente iguales su parte
entera y todos sus decimales. En lugar de comparar la igualdad o la desigualdad, lo que se
suele hacer es comprobar si la diferencia es o no muy pequeña. Por ejemplo, si entre dos
valores reales la diferencia es menor de 0.00001, entonces pueden considerarse iguales aunque
no lo sean exactamente. Pero para escribir si se verifica esa diferencia menor, no hace falta los
operadores de comparación, sino los relacionales.
2. Tampoco se suelen emplear con el tipo boolean, porque es mejor usar directamente sus valores
lógicos.

Se puede escribir:
boolean cond= ... ;
if ( cond == true ) ...

pero es mejor:
boolean cond= ... ;
if ( cond ) ...

En el segundo caso el valor de la variable cond se utiliza directamente en la condición, cuando su valor
sea true se harán las acciones asociadas al if. Que es exactamente lo mismo que hace el primer ejemplo.
Si quisiéramos hacer una acción cuando una variable o una expresión booleana fuera falsa, tampoco
tendrı́amos que escribir la comparación (cond==false), bastarı́a con negar su valor con el operador !
(!cond).
También es posible comparar objetos pertenecientes a la misma clase, pero en este caso usamos un
método estándar que hay que implementar en las clases nuevas que se construyan, es el método equals():

Se puede usar el operador == con objetos, y en general con cualquier variable de un tipo referen-
ciado.

Solamente devuelve true cuando las dos variables referencian el mismo objeto. Debe recordarse
que los objetos son tipos referenciados, es decir, la variable que está en la Pila hace referencia a un
objeto que está en otra parte en la memoria (ver Sección 3.9). Solamente cuando las dos variables
referencian el mismo objeto son iguales.

Pero, dos objetos pueden ser iguales sin ser exactamente el mismo objeto.

Ejemplo: dos Cı́rculos con el mismo radio son iguales.

Para poder evaluar la igualdad de objetos de una nueva clase se debe escribir un método llamado
equals(), que recibe un objeto y devuelve un valor boolean.

Por ejemplo, el método equals() para la versión actual de nuestra clase Cı́rculo podrı́a ser este1 :
1 En la Sección 6.9.1 se da una versión más correcta del método.

69
29 /**Devuelve cierto si dos objetos Cı́rculo son iguales
30 * @param c el objeto con el que se va a comparar
31 * @return true si el radio de los dos objetos es igual*/
32 public boolean equals(Cı́rculo c) {
33 return getRadio() == c.getRadio();
34 }

El método básicamente debe comprobar que el valor de todos los atributos de los dos objetos es el
mismo. En la clase Cı́rculo, dado que solamente cuenta con el atributo radio, comprobamos si éste es
igual o no. Para ello se verifica si el valor del radio del objeto con el que se llama al método (getRadio())
es igual que el valor del radio del objeto c que se pasa como parámetro (c.getRadio()). Es interesante
observar como directamente devolvemos el valor boolean resultante de la expresión de comparar ambos
valores con el operador ==, no hace falta usar una variable auxiliar para guardar previamente el resultado
de esa operación, ni usar una sentencia condicional. Para saber más sobre el método equals() ver la
Sección 6.9.1.

4.4. Sentencia if-else


La sentencia if tiene una utilidad limitada, ya que en muchos casos se pretenden hacer unas acciones
cuando una condición es cierta y otras cuando esa misma condición es falsa. Para eso necesitamos emplear
la sentencia if-else.
enun- Dados dos números reales leı́dos de teclado, realizar un programa que imprima el máximo valor de ambos.
ciado
Como hemos aprendido a hacer métodos en los temas anteriores, la parte fundamental de este pro-
grama puede hacerse escribiendo un método que dados dos números reales nos devuelva el valor máximo
de ambos. Ese método podrı́amos reutilizarlo en todos los programas que escribamos que necesiten esa
funcionalidad.

Algoritmo 4.2 Método Máximo2()


Función Máximo2 (a: real, b: real) : real
si a > b entonces
retorna a
sino
retorna b
fin si

En este caso, necesitamos una sentencia condicional que haga también algo cuando la condición
sea falsa.

Es un if en su forma más general, con parte entonces y sino. Puede verse su diagrama de flujo en
la Figura 4.4.

La sentencia que nos permite hacer esta acción es una sentencia if-else, es decir, la forma completa
de una sentencia if: no solamente se hace algo cuando la condición es cierta, sino también cuando es
falsa. Su sintaxis es:

sintaxis Sentencia if-else:


if (condición) acción-1
else acción-2

La semántica de la sentencia es que se ejecuta la acción-1 cuando la condición es cierta y la acción-2


cuando es falsa.

Nunca se ejecutarán las dos acciones, pero siempre se ejecutará una de las dos.

70
T F
Condición

acción-1 acción-2

Figura 4.4: Diagrama de flujo de la sentencia if-else

Como en los if simples, si la acción de cada parte es única se finaliza en punto y coma (;) y si
es una secuencia se pone entre llaves. De nuevo, algunos programadores usan llaves siempre para
producir un código más claro.

La sentencia if simple estudiada anteriormente no deja de ser un caso particular de la sentencia


condicional general if-else, en el que la parte else es vacı́a. Aplicando esta sintaxis a nuestro algoritmo
para el método Máximo2() tendrı́amos la siguiente implementación, con su especificación correspondiente:
7 /**Calcula el valor máximo de dos números reales
8 * @param a primer número real
9 * @param b segundo número real
10 * @return el valor máximo de a y b */
11 public static double Máximo2(double a, double b) {
12 if ( a > b ) return a;
13 else return b;
14 }

Una vez hecho el método simplemente tenemos que llamarlo en nuestro programa principal:

Ejemplo1Máximo.java
25 public static void main(String[ ] args) {
26 //Declaramos dos variables reales
27 double num1, num2;
28 //Objeto Scanner asociado con el teclado
29 Scanner teclado= new Scanner(System.in);
30 //Leemos ambos reales
31 System.out.print("Introduce dos reales: ");
32 num1=teclado.nextDouble();
33 num2=teclado.nextDouble();
34 //Mostramos el valor máximo en la pantalla
35 System.out.printf("Máximo: %f\n",Máximo2(num1,num2));

36 }

4.4.1. El operador ( ? : )
Cuando tenemos una sentencia if-else como la de este ejemplo, que cumple dos propiedades: a) es
sencilla, y b) sirve para calcular un valor, es posible sustituir dicha sentencia por una expresión con el
operador ( ? : ).

sintaxis Operador ( ? : ) :

( condición ? expresión-1 : expresión-2 )

71
Es muy importante recalcar que se trata de un operador y debe utilizarse como tal, nunca como
un sustituto de la sentencia if-else cuando estamos escribiendo sentencias condicionales complejas con
acciones en las partes entonces y sino:

Es un operador ternario (el único del lenguaje).

La semántica del operador es que la expresión total produce el valor de la expresión-1 cuando la
condición es cierta y el de la expresión-2 cuando es falsa.

Importante: NO está pensado para escribir sentencias condicionales con acciones. Para eso está el
if-else. Este operador se utiliza para reemplazar sentencias if-else sencillas cuyo objetivo es
producir un valor.

En las dos expresiones no se ponen acciones, sino expresiones que producen un valor.

Ambas expresiones suelen ser del mismo tipo.

Aplicando la sintaxis a nuestro código tendrı́amos la siguiente implementación:


20 public static double Máximo2bis(double a, double b) {
21 return ( a > b ? a : b );
22 }

El objetivo de la sentencia if-else del método Máximo2 es producir el valor del máximo de los dos
parámetros. En esas situaciones el operador ( ? : ) es perfecto, ya que es precisamente para lo que
sirve. En la implementación realizada, la expresión produce o bien el valor del parámetro a cuando la
condición a > b es cierta, o bien el valor de b si es falsa. El valor producido (el de a o el de b) se retorna
directamente.

4.4.2. Sentencias if-else anidadas


Es muy habitual, en trozos de código en los que se tienen que comprobar condiciones más complejas
que las que hemos visto en los ejemplos anteriores, usar sentencias if-else anidadas unas dentro de
otras. Vamos a verlo con un problema un poco más complejo que el anterior.
enun- Dados tres números reales leı́dos de teclado, realizar un programa que imprima el máximo valor de ellos.
ciado
De nuevo podemos hacer un método por si tenemos que reutilizarlo. Un posible pseudocódigo serı́a:

Algoritmo 4.3 Método Máximo3()


Función Máximo3 (a: real, b: real, c:real) : real
si a > b entonces
si a > c entonces retorna a
sino retorna c
fin si
sino
si b > c entonces retorna b
sino retorna c
fin si
fin si

La idea es ir descartando casos.

Los if anidados se van aprovechando de los anteriores y necesitan condiciones más simples.

En el algoritmo primero comprobamos cuál es el mayor entre a y b. Si es el primero, es decir, si la


condición del primer si es cierta, entonces ya sabemos que b no va a ser el mayor. El si anidado de la
parte entonces no necesita comparar c con b, ya que sabe que el mayor solamente puede ser a o c, nunca
b al ser menor que a. Lo mismo ocurre por la parte sino del primer si: su si anidado ya sabe que a no es
el mayor, ası́ que la comparación que resta por hacer es entre b y c, devolviéndose el mayor de los dos.

72
Ejemplo2Máximo.java
21 public static double Máximo3(double a, double b, double c) {
22 if ( a > b ) {
23 //b no es el mayor, no hace falta comparar b y c
24 if ( a > c ) return a;
25 else return c;
26 }
27 else {
28 //a no es el mayor, no hace falta comparar a y c
29 if ( b > c ) return b;
30 else return c;
31 }
32 }

En realidad, el método anterior podrı́a haberse escrito de una forma más simple, teniendo en cuenta
que ya tenı́amos programado el método Máximo2():

En cada paso lo que se hace es comparar entre dos de los números cuál es el mayor.

Lo hacemos dos veces: una en el primer if-else y otra en el if-else anidado.

Mirado de una forma más abstracta, el código anterior podrı́a resumirse en:

1. Primero miramos entre dos de ellos cuál es el mayor, y


2. luego, comparamos ese mayor con el tercero.

Lo mismo podrı́a lograse con dos llamadas encadenadas al método Máximo2().

39 public static double Máximo3bis(double a, double b, double c) {


40 return ( Máximo2( Máximo2(a,b) , c) );
41 }

En nuestro caso lo hemos hecho con sentencias if-else anidadas porque queremos aprender a mane-
jarlas. Hemos utilizado llaves tanto en la parte entonces como en la parte sino. En realidad podrı́amos no
haberlo hecho, ya que ambas partes solamente constan de una instrucción que a su vez es una sentencia
if-else. Lo hemos hecho de esta manera ya que el código puede resultar más fácil de entender.
Precisamente uno de los aspectos que puede presentar cierta dificultad de comprensión cuando se
escriben varios if-else anidados, es saber con qué if va cada uno de los else. La forma en la que se
asocia un else con su correspondiente if viene dada por la siguiente regla:
4
! Un else siempre va con el if más próximo de su bloque de código que no tenga otro else
asociado.
Fijémonos en los dos trozos de código siguientes:
1 if (a <= b)
2 if (c < d) a = b-2;
3 else if (d < e) b = b+3;
4 else if (e > f) c = a+5;

1 if (a <= b)
2 if (c < d) a = b-2;
3 else if (d < e) b = b+3;
4 else if (e > f) c = a+5;

¿Con qué if va el último else?


Por la forma de sangrar los programas, podrı́a pensarse que el último else va, en el primer caso con
el if de la penúltima lı́nea, y en el segundo ejemplo con el primer if. No es ası́.
La forma de sangrar el programa no influye, se aplica la regla anterior y en ambos casos va con el if
de la penúltima lı́nea. Si queremos que vaya con el primer if tenemos que usar las llaves para hacer que
el if más próximo de su mismo bloque sea el primero. Cualquiera de las dos siguientes soluciones serı́a
válida:

73
1 if (a <= b)
2 if (c < d) a = b-2;
3 else { if (d < e) b = b+3; }
4 else if (e > f) c = a+5;

1 if (a <= b) {
2 if (c < d) a = b-2;
3 else if (d < e) b = b+3;
4 } else if (e > f) c = a+5;

Resulta preferible la última simplemente por mayor claridad. En el primer ejemplo, como el if de la
penúltima lı́nea está encerrado en un bloque, es imposible que el último else vaya con él. Pertenecen a
bloques distintos. Por otro lado, el if de la segunda lı́nea ya tiene un else asociado (el de la tercera),
por lo que el más próximo es el de la primera lı́nea. En el segundo caso, dado que las llaves de la parte
entonces del primer if engloban las lı́neas 2 y 3, es evidente que el más próximo de su mismo bloque es
también el de la primera lı́nea.
Es importante que se vea que las llaves marcan, no solamente distintos bloques de código, sino también
distintos niveles de anidamiento.
Podemos aplicar todo lo que ya hemos visto sobre sentencias condicionales para resolver un problema
en el que se necesitan usar varias instrucciones if-else anidadas.
enun- Una compañı́a de autobuses cubre los trayectos entre Madrid y las principales ciudades del norte de
ciado España. Sus tarifas varı́an según el destino y la edad del viajero, ofreciendo precios más reducidos para
jóvenes y jubilados. El precio de cada billete se rige por la tabla que aparece a continuación. Diseñar un
programa que dada la edad del viajero y el destino (leı́do como un carácter, la inicial de la ciudad: C /
G / S / B ), imprima la tarifa a aplicar.

menor de 18 entre 18 y 64 mayor de 64


Coruña 30 35 30
Gijón 25 30 25
Santander 25 30 25
Bilbao 30 35 30

Una de las novedades de este programa es que por primera vez tenemos que manejar datos de tipo
char (ver Sección 2.4). La definición de una variable carácter es igual que cualquiera de las del resto de
tipos básicos. La diferencia está en cómo leer un valor de tipo char de teclado:

La clase Scanner NO tiene un método nextChar() que lea el siguiente carácter.


Para leerlo debemos usar dos métodos consecutivamente:
1. el método next() que lee el siguiente token o elemento.
2. el método charAt() que permite obtener un carácter de ese elemento. El primer carácter tiene
como ı́ndice 0 y no 1. En el tema siguiente se explicará por qué esto es ası́.

Como en los ejemplos anteriores, vamos a realizar un método para calcular la tarifa. Este método
será llamado desde un programa principal, que se encargará además de leer los datos introducidos por
el usuario y de mostrar la tarifa final. Algo como lo que sigue:

CalcularTarifa.java
33 public static void main(String[ ] args) {
34 char destino; //letra indicando el destino
35 int edad; //edad del viajero
36 //Objeto Scanner asociado con el teclado
37 Scanner teclado= new Scanner(System.in);
38 //Leemos los datos del viaje
39 System.out.print("Introduce el destino (C/G/S/B): ");
40 destino=teclado.next().charAt(0);
41 System.out.print("Introduce la edad: ");

74
42 edad=teclado.nextInt();
43 //Mostramos la tarifa en pantalla
44 System.out.printf("Tarifa: %f\n",Tarifa(destino,edad));
45 }

Para acabar nuestro programa, ya solamente necesitamos programar el método Tarifa() que, dada
la edad y el destino del viajero, nos devuelva la tarifa de ese viaje.
15 /**Calcula la tarifa
16 * @param d letra indicando el destino del viaje
17 * @param e edad del viajero
18 * @return la tarifa aplicable para ese destino/viajero */
19 public static double Tarifa(char d, int e) {
20 if ( d==’G’ || d==’S’ ) {
21 //Destino Gijón o Santander
22 if ( e >= 18 && e <= 64 ) return 30;
23 else return 25;
24 }
25 else {
26 //El destino es Coruña o Bilbao
27 if ( e < 18 || e > 64 ) return 30;
28 else return 35;
29 }
30 }

Si se analiza la tabla de precios, se puede apreciar que los importes para Santander y Gijón coinciden,
del mismo modo que los de Coruña y Bilbao. Por ello, lo primero que hacemos en el método Tarifa()
es comprobar en cuál de los dos casos estamos. Usamos para ello una condición con el operador ||, si el
destino es Gijón o Santander. La parte entonces de ese if será para esas dos ciudades, la parte sino para
Coruña y Bilbao. En ambas situaciones la tarifa final depende de si el viajero es joven o jubilado o no lo
es. Para hacer el programa más didáctico, en cada una de las ramas hemos comprobado una situación
diferente. En la parte entonces (destinos Gijón y Santander) comprobamos que la edad esté comprendida
entre 18 y 64 años. Para ello usamos el operador &&. Para la parte sino (destinos Coruña y Bilbao)
comprobamos lo contrario, que el viajero sea o bien joven o bien jubilado, es decir, usamos el operador
||.

4.5. Sentencia switch


Hay programas en los que dependiendo del valor de una determinada variable o expresión queremos
hacer una acción distinta. Esto puede resolverse, obviamente, empleando varias sentencias if-else anida-
das, de forma que en cada uno de los if anidados tratemos un caso diferente. El diagrama de flujo de
este tipo de programas condicionales puede verse el la Figura 4.5. Si el valor de la variable o expresión es
igual al Caso-1, entonces se ejecutará la acción-1, si es igual al Caso-2, la acción-2 y ası́ sucesivamente.
Como se puede apreciar:

Hay un conjunto más o menos numeroso de casos distintos, y

cada caso requiere hacer acciones diferentes.

Los casos se comprueban en orden.

Si todos los casos fallan, al final puede hacerse una acción por defecto:

1. dar un error, o
2. tratar el caso más numeroso.

Estas situaciones requieren escribir varios if-else anidados, lo que es un poco engorroso y hace
que el programa resultante pueda ser difı́cil de leer. Para hacer estos programas más claros, en lugar
de usar if-else se puede emplear la sentencia switch, cuyo uso se adapta precisamente a la situación

75
T
Caso-1 acción-1

F
T
Caso-2 acción-2

F
...

T
Caso-n acción-n

acción por defecto

Figura 4.5: Diagrama de flujo de la sentencia switch

antes planteada. Vamos a proponer un problema que resolveremos en parte usando switch. Ası́ podremos
discutir sus propiedades y conocer las situaciones en las que se puede utilizar.
enun- Modificar la clase Fecha de forma que se garantice que un objeto de la clase siempre tenga una fecha
ciado válida. Se entenderá por una fecha válida: un año positivo, un mes entre 1 y 12 y un número de dı́a
correcto de acuerdo con el resto de atributos.
Para realizar este programa necesitamos tener en cuenta varios aspectos:

Garantizar que los tres atributos de un objeto de la clase Fecha sean correctos.

Los métodos set() deben comprobar que el atributo correspondiente se está actualizando con un
valor válido.

Para el atributo dı́a hay distintos valores lı́mite en función del valor del mes y del año.

¿Cómo programar el método setFecha(), teniendo en cuenta que para determinar si una fecha es
correcta unos campos dependen de otros?

Comencemos por la parte más sencilla, que no requiere de conocimientos previos pero que presenta
alguna sutileza. En realidad el método setFecha() que permitı́a cambiar simultáneamente los tres atri-
butos, año, mes y dı́a, no lo hemos tocado, es igual que el que se mostró en la Sección 3.8.1. Como vimos,
el método llamaba a su vez a los métodos set() individuales para cambiar a través de ellos cada uno de
los atributos. De esta forma la modificación se realizaba en estos métodos y en un supuesto cambio de
la clase, por ejemplo el que vamos a hacer ahora, el método setFecha() no harı́a falta tocarlo (como no
lo hemos hecho). La sutileza de la que hablamos, y lo más importante del método, es que el orden en
el que se ejecutan los métodos set() individuales refleja la dependencia que existe a la hora de validar
los datos de una fecha: el atributo dı́a depende del mes y el año para poder saber si su valor es correcto.
Por eso primero modificamos estos dos últimos, y una vez fijados sus valores, cambiamos el campo dı́a
a través de su método setDı́a() asociado. Este método ya considerará los nuevos valores de mes y año

76
y podrá comprobar si el número del dı́a asignado es correcto o no. Si lo hiciéramos en otro orden, por
ejemplo llamando primero a setDı́a() y luego a setMes(), podrı́a darse el caso de que diéramos como
válido el valor 30 para el atributo dı́a siendo el nuevo mes Febrero.

Ejemplo2Fecha/Fecha.java
38 public void setFecha(short d, short m, short a) {
39 //Fijamos las fechas en este orden, para que al fijar el
40 //dı́a, sepamos si es correcto de acuerdo al nuevo año y mes
41 setAño(a);
42 setMes(m);
43 setDı́a(d);
44 }

La modificación del los métodos setMes() y setAño() es totalmente análoga a la que realizamos en la
clase Cı́rculo con el método setRadio(). Simplemente necesitamos usar sentencias if simples en las que
comprobemos que el valor de parámetro que recibe el método es correcto antes de modificar el atributo.
En el caso de setAño() la condición solamente tiene un término (año > 0), pero para setMes() necesitamos
una condición con dos términos ya que el atributo mes debe estar comprendido entre dos valores. Estas
situaciones se resuelven con dos condiciones simples unidas por un Y-lógico: el nuevo valor debe ser mayor
o igual que 1 Y menor o igual que 12. Recuérdese que nunca hay que escribir estas condiciones como si
fuera una condición matemática: son dos condiciones simples unidas por el operador &&.

86 public void setMes(short m) {


87 if ( m>=1 && m<=12 ) mes=m;
88 }

99 public void setAño(short a) {


100 if ( a > 0 ) año=a;
101 }

El caso del método setDı́a() es mucho más complicado ya que un valor válido para el campo dı́a
depende no solamente del mes, sino también del año: cuando un año es bisiesto Febrero tiene 29 dı́as. Es
decir, en este método los lı́mites para el atributo dı́a son variables: tenemos distintos casos, los meses de
31 dı́as, los de 30 dı́as y Febrero que puede tener 28 o 29 dı́as. Además, se debe tener en cuenta que en
total hay 12 casos diferentes, uno por cada mes. Para hacer el programa mucho más claro, de forma que
sea más fácil de leer, lo podemos escribir con switch. Una posible solución serı́a la siguiente:

55 public void setDı́a(short d) {


56 if ( ( d >= 1 ) && ( d <=31) ) {
57 //Sólo cambiamos el dı́a si d está entre 1 y 31
58 short dı́as_mes; //variable para calcular los dı́as que tiene el mes
59 switch ( getMes() ) {
60 case FEBRERO: //28 ó 29 dı́as
61 dı́as_mes = (short) (esBisiesto() ? 29 : 28);
62 break;
63 case ABRIL:
64 case JUNIO:
65 case SEPTIEMBRE:
66 case NOVIEMBRE: //meses de 30 dı́as
67 dı́as_mes=30;
68 break;
69 default: //Es un mes de 31 dı́as
70 dı́as_mes=31;
71 }
72 //cambiamos el dı́a si es menor o igual que el dı́a máximo del mes
73 if ( d <= dı́as_mes ) dı́a=d;
74 }
75 }

Hay muchas cosas nuevas en ese trozo de programa. Vamos a estudiarlas paso a paso. Comenzaremos
analizando las caracterı́sticas formales de la sentencia switch, empezando por su sintaxis:

77
sintaxis Sentencia switch:
switch ( expresión ) {
case valor-1 : acción-1 ; break;
case valor-2 : acción-2 ; break;
...
case valor-n: acción-n; break;
default: acción-por-defecto;
}

Algunas consideraciones importantes:

1. La expresión debe ser de un tipo básico discreto (tipos enteros y carácter), de la clase String
(Sección 5.8.1), o también se pueden utilizar los tipos enumerados que veremos en el Tema 6.

2. Los valores de cada caso deben ser expresiones del mismo tipo que la expresión del switch.

3. No hace falta usar llaves aunque la acción asociada con un caso sea una secuencia de instrucciones.

Es especialmente importante la primera de las restricciones. Como comentamos cuando hablamos del
operador de igualdad (==), no tiene sentido comparar si una expresión real es igual a un cierto valor
ya que es muy difı́cil que la igualdad total se dé. Por ello no se pueden utilizar tipos continuos, como
los reales, en los que entre cada par de valores siempre podemos encontrar otro y en los que es muy
difı́cil que la igualdad exacta se produzca. En cambio en los tipos discretos, en los que entre un par
de valor consecutivos no existe ningún otro entre ellos, la igualdad es una propiedad habitual. Por este
motivo son los tipos que pueden emplearse en una sentencia switch. Como estudiaremos en el Tema 6,
las enumeraciones también son un tipo discreto y por tanto también pueden ser usadas en una sentencia
switch. El uso de la clase String con switch es un poco más extraño, no es posible por ejemplo en otros
lenguajes, y de hecho en Java se empezó a usar a partir de la versión 7.
La semántica del switch es bastante sencilla:

Se comprueba por orden cuál de los valores de los distintos casos coincide con el valor de la
expresión.

Cuando un caso es igual, se ejecutan todas la sentencias que estén escritas a partir de ese punto.

Por ello se suele poner una sentencia break al final de las acciones de cada caso. La sentencia break
traslada el flujo del programa a la instrucción siguiente a la sentencia switch. Con esto se evita que
se ejecuten las sentencias de los casos siguientes.

Al final puede incluirse un caso por defecto, que se aplicarı́a cuando ninguno de los otros casos son
iguales a la expresión.

El detalle más importante de su semántica es el hecho de que en el momento en que un caso es igual,
el flujo del programa continua por ese punto. Esto obliga a poner sentencias break si no queremos que
se ejecuten también las acciones de los casos que están a continuación. Claro que en ciertos programas
podemos sacar partido de esta situación para escribir una sentencia switch más corta. Hay veces en el
que las acciones de un caso tienen alguna sentencia en común con las de otros. Vamos a ver todo esto
con un pequeño ejemplo. Supongamos una variable entera n:
switch (n) {
case 3: System.out.print("*");
case 2: System.out.print("*");
case 1: System.out.print("*");
}

¿Cuántos asteriscos se imprimen en cada caso?


La respuesta es que escriben tantos asteriscos como valor tenga la variable n, cuando este valor
está comprendido entre 1 y 3. Cuando n vale 3, el flujo del programa entrará por ese caso y llamará al

78
método print(). Como tras esa instrucción no hay una sentencia break, el flujo continuará por la sentencia
asociada a la constante 2 y luego con la de 1. Con lo que finalmente se imprimirán 3 asteriscos. El ejemplo
con 2 es análogo, imprimirá dos asteriscos, y con 1 tan solo uno. De todas formas, esto no es una situación
muy común; incluso la forma habitual de resolver un problema como el anterior serı́a otra, usando un
bucle.
Para acabar con la descripción de la sentencia switch vamos a describir otros de los aspectos de
nuestro método setDı́a():

Habitualmente se usan constantes para indicar los casos. En nuestro programa hemos definido
constantes para cada uno de los meses. Esto facilita la lectura del programa ya que es más sencillo
entender qué mes tiene una fecha cuando se ve la constante ABRIL, que si viéramos en su lugar el
número 4.
13 public static final short ENERO=1;
14 public static final short FEBRERO=2;
...
24 public static final short DICIEMBRE=12;

Empleamos el caso por defecto para agrupar el caso más numeroso. En este programa podemos
hacerlo ya que estamos seguros de que el valor del atributo mes será correcto. En otras situaciones
el caso por defecto suele emplearse para detectar errores o valores inapropiados de la variable o
expresión que se usa en el switch.

No usamos llaves para delimitar las acciones de cada caso, a pesar de que los dos primeros tienen
dos acciones. Es casi inevitable que las acciones asociadas a un caso tenga más de una instrucción
ya que en general siempre se finalizan con break. Recuérdese que las llaves no son necesarias ya que
los casos definen un punto de entrada y el flujo del programa sigue a partir del caso que es igual a
la expresión del switch.

La sentencia switch puede parecer una sentencia no estructurada: el flujo salta al caso igual y
después salta a la instrucción siguiente al switch con la sentencia break. Algo de cierto hay, al fin
y al cabo se realizan saltos incondicionales cada vez que se emplea la instrucción break al final de
los casos. Hay que decir que dichos saltos son producto de la sintaxis y semántica de la sentencia,
ya que en el fondo lo único que realiza la instrucción switch es un conjunto de if-else anidados
escritos de otra forma.

4
! Cualquier switch podrı́a reescribirse usando sentencias if-else.
Para finalizar, consideremos otros detalles importantes del método setDı́a(). Primero, empleamos
una variable llamada dı́as mes para calcular con el switch el número máximo de dı́as que tiene ese mes.
Serı́a perfectamente correcto decir que la postcondición de ese switch, recuérdese que la postcondición
(Sección 1.7.3) es lo que se cumple cuando el switch se acaba, es que la variable dı́as mes tendrá pre-
cisamente eso, el número de dı́as que tiene ese mes. En segundo lugar, usamos el operador ( ? : ) para
asignar los dı́as que tiene en caso de ser el mes de Febrero. El motivo es que en ese caso el mes puede
tener 28 o 29 dı́as en función de si el año es bisiesto o no. Obsérvese como lo que hacemos es asignar a
la variable dı́as mes uno de esos dos valores en función del valor de la condición del operador ( ? : ). Por
último, para determinar si el año es bisiesto llamamos a un método de la clase llamado esBisiesto() que
nos devolverá un valor booleano: cierto si el año del objeto Fecha es bisiesto y falso en caso contrario.
Ese método, como estudiaremos en el Tema 6 podrı́a ser privado.

4.6. Bucles
Además de las sentencias simples y las condicionales, la tercera pata que compone el conjunto de
instrucciones dentro de la Programación Estructurada son las sentencias iterativas o bucles. Son sin
duda las instrucciones más difı́ciles de dominar; son menos intuitivas que las condicionales y mucho más
complejas que las simples. ¿De dónde surge la necesidad de este tipo de sentencias? Vamos a pensar en
dos ejemplos, uno más sencillo y el otro más complejo y real:

79
1. Calcular la suma de 10 números leı́dos de teclado.

2. Abonar los intereses en todas las cuentas de un banco.

Aunque los problemas son bastante diferentes, en los dos necesitamos el mismo tipo de sentencias:
los bucles. En ambos programas hay alguna instrucción que se debe repetir varias veces. Quizás hasta
resulte más evidente en el segundo caso: con cada cuenta bancaria tenemos que abonar sus intereses. La
acción “abonar intereses en una cuenta” se tiene que repetir tantas veces como cuentas tenga la entidad
bancaria. Siempre que en un programa aparezcan acciones que se deban repetir usaremos bucles.
El problema de sumar los números puede parecer menos intuitivo a primera vista. Incluso podrı́amos
pensar en resolverlo como en el Tema 2, cuando hicimos la suma de dos números. En lugar de definir dos
variables, ¿podrı́amos definir 10? ¿Y si en lugar de 10 variables fueran 1000? Obviamente el programa no
se hace declarando 10 variables, resultarı́a un programa innecesariamente largo y difı́cilmente modificable.
Es mucho mejor definir una variable, leer con ella diez valores distintos e ir sumando cada valor según
se lee. En este caso lo que se repite es “leer variable, sumar el valor”. Esas dos acciones se repetirán 10
veces. Como se verá en las siguientes secciones hacerlo de esta manera tiene dos ventajas: 1) estamos
usando menos variables, y 2) ese programa podrı́a servir para sumar cualquier cantidad de números con
una simple modificación.
Comenzaremos definiendo formalmente lo que es un bucle:

definición Bucle: Sentencia que permite repetir un conjunto de acciones un cierto número de veces.

También se puede denominar Iteración, aunque este término se suele emplear para referirse a cada
una de las veces que se repiten las acciones que contiene un bucle. En lo que sigue usaremos bucle para
referirnos al conjunto de la instrucción y repetición o iteración para cada una de las ejecuciones.
Podemos resumir las principales caracterı́sticas de los bucles en los siguientes puntos:

Permiten hacer varias veces las mismas acciones.

Las acciones internas se repiten mientras la condición del bucle sea cierta.

Las acciones del bucle deben cambiar la/s variable/s que intervenga/n en la condición de forma
que ésta sea falsa en algún momento.

Si la condición nunca es falsa, el bucle es infinito.

Uno de los mayores errores que cometen los programadores principiantes es hacer un bucle infinito.
Para evitarlo se debe tener siempre en mente la siguiente idea. En la condición de una bucle suele
aparecer o bien una expresión o bien una llamada a un método, pero en ambos casos con alguna variable
implicada. En las acciones internas del bucle esa variable o variables, debe/n cambiar de valor. Si no lo
hacen, entonces es más que probable que el bucle sea infinito, ya que la condición no cambiará nunca de
valor al no hacerlo la/s variable/s implicadas en la misma.
4
! Cuando se escribe un bucle se debe garantizar siempre que en algún momento la condición
será falsa, si no el bucle será infinito.
Hay dos tipos de bucles:

1. Mientras (while y for): la acción se repite 0 o más veces.

2. Repetir (do-while): la acción se repite 1 o más veces

En la Figura 4.6 aparece el diagrama de flujo de ambas sentencias. La diferencia es clara. En el caso
del mientras (izquierda) lo primero que se hace es comprobar la condición. Si ésta fuera falsa la primera
vez, el bucle no hará nada. Si esa primera vez la condición es cierta, entonces el bucle empezará a repetir
sus acciones hasta que en algún momento la condición se haga falsa. En cambio en el bucle repetir
(derecha), las acciones se hacen al principio, y después de ellas se evalúa la condición. Las acciones se
seguirán haciendo mientras la condición sea cierta, pero lo que es seguro es que al menos se harán una
vez.

80
mientras repetir

F
Condición

T
acción
acción

T
Condición

Figura 4.6: Diagramas de flujos de los bucles mientras y repetir

Al principio puede chocar que queramos escribir un bucle cuyas acciones se repitan 0 veces. Se podrı́a
pensar que serı́a mejor no escribir nada. La causa de que un bucle pueda no repetirse ninguna vez se
debe a que el número de veces que una sentencia iterativa se ejecuta depende de su condición, que a su
vez dependerá del valor de alguna variable. Cuando el programador está escribiendo el programa no sabe
exactamente las veces que en cada ejecución del programa se va a hacer el bucle. Será en cada ejecución
individual cuando, en función del valor de las variables, las acciones del bucle se repetirán más o menos
veces. Siguiendo con los ejemplos bancarios, imaginemos que la aplicación de gestión del banco carga una
cantidad en concepto de comisión a todos los clientes que se quedan en descubierto durante un perı́odo de
tiempo. También en este caso necesitarı́amos un bucle y la acción que se repetirı́a serı́a “cobrar comisión
en cuenta con descubierto”. Pudiera ser que en alguna ocasión no hubiera ningún cliente en descubierto,
con lo que el bucle que harı́a esos cargos no se ejecutarı́a ninguna vez.
Uno de los aspectos más importantes cuando se diseña un bucle es precisamente decidir si el bucle se
va a hacer al menos una vez o no. En el primer caso deberemos usar un bucle repetir y en el segundo un
bucle mientras. En las siguientes secciones vamos a describir los bucles mientras y repetir de que dispone
Java. Del primer tipo hay dos sentencias diferentes, while y for, mientras que como bucle repetir hay
solamente una, do-while.
Comenzaremos explicando los bucles mientras ya que se suelen emplear más que los repetir. El motivo
fundamental es que en menos situaciones es seguro que el bucle deba hacerse al menos una vez. Por otro
lado, hay una cierta costumbre por parte de los programadores a escribir bucles mientras aun en el caso
de saber que el bucle se va a hacer al menos una vez. Esa preferencia puede también estar motivada por
la sintaxis del for que permite una escritura más compacta y corta.

4.7. Bucle while


El bucle mientras más genérico es el bucle while. Vamos a estudiarlo empleándolo en un ejemplo
concreto.
enun- Hacer un programa que dado un número entero positivo calcule la suma de todos los números pares
ciado comprendidos entre 2 y ese número (ambos incluidos).

Hay varios detalles a los que se debe prestar mucha atención al leer los enunciados. El primero de
todos es la descripción de la secuencia de números pares que se hace:

va entre 2 y el número leı́do de teclado (condición)

posiblemente vacı́a, ¿qué pasa si n = 1? (tipo de bucle)

81
Si el número introducido es 1 la secuencia es vacı́a ya que no habrı́a ningún número que sumar. Por
tanto, el enunciado al decir que el número es positivo nos está diciendo que la secuencia puede ser vacı́a,
con lo que el bucle podrı́a hacerse 0 veces. Está claro que hay que usar un bucle mientras. Además, nos
dice qué tipo de elementos tiene la secuencia: números pares. Es decir, se tienen que sumar los números
pares desde 2 y hasta que no sea mayor que el número leı́do. Esto es lo que usaremos para diseñar alguna
de las acciones del bucle y la condición.
El otro elemento clave para diseñar un bucle es determinar las acciones que se van a repetir. En el
ejemplo tenemos que ir sumando un número par y calculando el siguiente número par. Con estas ideas
en mente un algoritmo para resolver el problema podrı́a ser el siguiente:

Algoritmo 4.4 Suma de números pares


Leer número de teclado
par=2;
suma = 0
mientras par<=número hacer
suma = suma + par
par = par+2
fin mientras
Imprimir suma

La implementación en Java de ese algoritmo podrı́a ser la siguiente:

SumaNúmerosPares.java
6 public class SumaNúmerosPares {

8 public static void main(String[ ] args) {


9 //Objeto Scanner asociado con el teclado
10 Scanner teclado= new Scanner(System.in);
11 //Declaramos una variable entera para leer el número
12 int número;
13 //Leemos el no entero
14 System.out.print("Introduce un entero:");
15 número=teclado.nextInt();
16 //Una variable
17 int par=2; //para ir recorriendo los pares
18 int suma=0; //para calcular la suma
19 while ( par <= número ) {
20 suma+=par;
21 par+=2;
22 }
23 //Mostramos la suma en la pantalla
24 System.out.printf("La suma es %d\n",suma);
25 }
26 }

El detalle más importante del programa es sin duda la sentencia while. Su diagrama de flujo responde
al que se muestra en la Figura 4.6 y su sintaxis es la siguiente:

sintaxis Sentencia while:


while ( condición )
acción

Las principales propiedades del bucle while son:

Semántica: la acción se repite mientras la condición sea cierta.

Número de ejecuciones: 0 o más veces.

No es seguro que la acción se haga alguna vez, ya que la condición puede ser falsa la primera vez.

82
Como siempre, si la acción está compuesta de varias instrucciones se deben poner llaves.

En nuestro ejemplo, la condición es que el par que se está sumando sea menor o igual que el número
leı́do. Mientras vayamos recorriendo pares y sean menores o iguales que número quiere decir que todavı́a
la secuencia no se ha acabado y que tenemos que seguir en el bucle sumando los pares que faltan. Como
en el bucle se repiten dos acciones, la suma y el incremento de par, están encerradas entre llaves. Es
importante darse cuenta que en este problema el número par que sumamos a suma en cada repetición
es el que se ha calculado en la iteración anterior (o antes de entrar en el bucle en el caso de la primera
repetición). Esto es ası́ porque antes de sumarlo tenemos que estar seguros que es un par menor o igual
que número. Si las acciones dentro del bucle estuvieran al revés, primero incrementar la variable par y
después la suma, entonces ocurrirı́a que cuando tuviéramos el par inmediatamente superior a número lo
sumarı́amos, con lo que estarı́amos añadiendo un par más de la cuenta. Si se quiere cambiar el orden de
las dos sentencias del bucle habrı́a que rediseñarlo, cambiando también la condición y las sentencias que
inicializan las variables utilizadas.

4.7.1. Operadores de asignación aritméticos


Otra pequeña novedad del programa es que hemos empleado un operador de un grupo que no habı́amos
utilizado todavı́a. Se trata de los operadores de asignación aritméticos. En la Tabla 4.5 se muestran todos
ellos. Sus caracterı́sticas son:

Permiten hacer dos operaciones en una, primero una operación aritmética y después una asignación.

La variable sobre la que se realiza la asignación es el primer operando de la operación aritmética.

Como en todas las asignaciones, el valor que devuelve la expresión es el valor que se asigna a la
variable de la izquierda (left-value).

En el ejemplo, con el operador + = primero se suman las variables suma y par y el resultado se asigna
a suma. Lo mismo hacemos con la expresión que suma a la variable par la cantidad constante 2.
La descripción de todos los operadores de asignación del lenguaje Java está en el Apéndice C en la
Sección C.5. No solamente existen operadores de asignación aritméticos, sino también lógicos y a nivel
de bit.

Tabla 4.5: Operadores de Asignación con operadores aritméticos. Los ejemplos suponen que la variable
entera a tiene valor 5 antes de la operación. El valor a la derecha es el resultado de la expresión y por
tanto el valor que se asigna a la variable a

Op. Uso Equivalencia Ejemplo (a= 5)


+= op1 + = op2 op1 = op1 + op2 a+ = 7 12
−= op1 − = op2 op1 = op1 − op2 a− = 7 −2
∗= op1 ∗ = op2 op1 = op1 ∗ op2 a∗ = 7 35
/= op1 / = op2 op1 = op1 / op2 a/ = 7 0
%= op1 % = op2 op1 = op1 % op2 a %= 7 5

4.7.2. Las sentencias break y continue


Dado que forman parte del lenguaje Java y están relacionadas con los bucles, vamos a explicar
brevemente la sintaxis y semántica de las sentencias break y continue. Ambas tienen una cosa en común,
provocan saltos incondicionales en el flujo de un bucle.

sintaxis Sentencia break:

break;

83
Aunque es fácil hacerse una idea de cómo funciona la sentencia break ya que se estudió al explicar
switch (ver Sección 4.5), vamos a comentar su semántica en el contexto de un bucle:

Finaliza el bucle y pasa a la siguiente instrucción. Es decir, provoca un salto a la sentencia siguiente
al bucle.

Tras ejecutar un break no se harán:

1. ni las acciones entre el break y el final del bucle,


2. ni se volverá a evaluar la condición.

Se suele poner dentro de una sentencia condicional, de forma que se sale del bucle bajo una condi-
ción.

Serı́a equivalente a incluir esa condición negada en la condición del bucle (es preferible).

4
! Si una instrucción break no se pone dentro de una sentencia condicional, el bucle harı́a una
sola iteración.

sintaxis Sentencia continue:


continue;

Esta sentencia es nueva. Su semántica es parecida a la de break, lo que cambia es el punto al que
salta el flujo:

Provoca un salto incondicional a la condición del bucle.

Es decir, tras un continue lo siguiente serı́a evaluar la condición del bucle de nuevo.

No se harı́a ninguna acción que esté entre continue y el final del bucle.

Se suele poner también dentro de una sentencia condicional.

4
! Si una instrucción continue no se pone dentro de una sentencia condicional, las acciones
entre el continue y el final del bucle nunca se ejecutarı́an.
Las sentencias break y continue no son necesarias de forma estricta; todo programa con cualquiera
de esas sentencias puede escribirse sin ellas. Vamos a verlo con dos ejemplos, uno para cada instrucción.

No emplear break: añadir la condición negada del break a la condición del bucle. El siguiente código
que usa break,
while ( n >= 0 ) {
...
if ( n %2 == 0 ) break;
}

podrı́a reemplazarse por este otro:


while (( n >= 0 ) && ( n %2 != 0 )) {
...
}

Como se observa el cambio requiere escribir una condición del bucle más compleja, pero a la vez
más descriptiva. Hemos añadido a la condición del bucle (n>=0), la condición negada ( !(n %2==0)
≡ (n %2!=0) ). Ambas condiciones están en una expresión lógica con el operador && ya que se debe
seguir en el bucle cuando se cumplan ambas, la que ya tenı́a el bucle y la contraria de la que lo
finalizaba con el break.

84
No emplear continue: añadir una sentencia condicional que incluya las acciones posteriores a
continue. Este programa,
while ( n >= 0 ) {
...
if ( n %2 == 0 ) continue;
... //acciones posteriores
}

es equivalente a este otro:


while ( n >= 0 ) {
...
if ( n %2 != 0 ) {
... //acciones posteriores
}
}

En este caso lo que hemos hecho es incluir una sentencia condicional en la que se engloban todas
las instrucciones que estaban después de la sentencia continue. De nuevo es necesario negar la
condición, ya que ası́ las acciones posteriores solamente se harán cuando la condición que ejecutaba
continue sea falsa, es decir cuando la contraria sea cierta. Cuando la condición de la versión con
continue es cierta ( (n %2==0) ≡ true ) entonces ninguna de las acciones posteriores se hará y se
volverá a evaluar la condición del bucle.
Obviamente se pueden dar casos mucho más complejos en los que evitar el uso de break o continue
es más difı́cil de lo mostrado en los dos ejemplos anteriores. No obstante, siempre que se pueda, se debe
implementar una versión alternativa sin dichas sentencias.
Desde un punto de vista didáctico, que es lo que nos ocupa en la asignatura, es preferible que cuando
se empieza a programar no se empleen este tipo de sentencias. Eso obliga a diseñar los bucles de una
manera más formal, definiendo totalmente los elementos de que constan, por ejemplo, escribiendo una
única condición de salida, tal vez más compleja pero a la larga más sencilla de entender y mantener.
Además, hay muchos lenguajes de programación que no tienen estas instrucciones. Nuestro objetivo es
que desde el principio se domine la escritura de condiciones moderadamente complejas, que permitan
escribir bucles sin emplear break y continue.

4.8. Bucle for


El otro bucle mientras presente en el lenguaje es el bucle for. Presenta la misma funcionalidad que el
while,
pero tiene una sintaxis que agiliza la escritura de ciertos bucles mientras, especialmente cuando se
sabe el número de iteraciones que tiene que hacer el bucle o cuando se recorren ciertos tipos de secuencias.
enun- Hacer un programa que lea 10 números reales de teclado y calcule su media.
ciado
Este programa podrı́a hacerse perfectamente con un bucle while, incluso con un bucle repetir como
do-while. Sin embargo tiene una particularidad que lo hace más apropiado para escribirlo con un for:
sabemos exactamente las veces que se debe ejecutar el bucle.
1. El bucle debe hacer exactamente 10 iteraciones, y
2. en cada iteración debemos leer un no y sumarlo, para luego calcular la media de todos ellos.
Una posible solución serı́a la siguiente:

Algoritmo 4.5 Media de 10 números reales


suma = 0
contador = 1
mientras contador ≤ 10 hacer
Leer número de teclado
suma = suma + número
contador = contador + 1
fin mientras
Imprimir suma/10

85
En este caso, la traducción a código fuente en Java no es tan directa ya que lo escribiremos usado
for, con lo que tenemos que adaptar el mientras del pseudocódigo a la sintaxis del for:

Media10Reales.java
5 public class Media10Reales {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Declaramos una variable real para leer los números
11 double número;
12 //y otra para ir calculando su suma
13 double suma=0;
14 System.out.print("Introduce 10 números reales:");
15 for (int i=1; i<=10; i++) {
16 número=teclado.nextDouble(); //leemos el siguiente
17 suma += número; //sumamos
18 }
19 //Mostramos la media en la pantalla
20 System.out.printf("Su media es %f\n",suma/10);
21 }
22 }

Desde un punto de vista formal, el bucle for es otra variante para escribir bucles mientras, distinta
sintácticamente de la que proporciona while. El bucle for tiene una sintaxis muy particular que ayuda
a contar fácilmente las iteraciones que el bucle realiza:

sintaxis Sentencia for:


for ( inicialización; condición; actualización )
acción

Podrı́amos resumir las propiedades del for en los siguientes aspectos:

Está pensado para bucles en los que se “cuenta” el número de repeticiones o se recorren secuencias
de números. O dicho de una forma más genérica, cuando se recorre un intervalo de valores en los
que la diferencia entre cualquier par de valores consecutivos es constante. Por ejemplo, los números
enteros entre 1 y 1000, o los números reales entre el 1.078 hasta 2.347 en incrementos de 0.003.
Tiene 3 partes:

1. inicialización: de la variable con la que se controla el número de iteraciones.


2. condición: para controlar si se ha alcanzado el número de iteraciones. Normalmente se usan
operadores relacionales o de igualdad para comprobar si el número de veces que se ha ejecutado
el bucle es el requerido o no.
3. actualización: acciones para actualizar la variable de control. Normalmente lo que se hace es
incrementar o decrementar la variable que se usa para controlar el número de iteraciones del
bucle.

Ninguna es obligatoria. Se podrı́a escribir perfectamente un bucle sin ninguna ( for(;;) ) que serı́a
infinito ya que la condición se entiende siempre cierta. Lo estudiaremos en la Sección 4.8.2.

De la sintaxis del for lo más importante es entender cuándo y cómo se ejecuta cada una de las partes
que lo componen. Su diagrama de flujo está en la Figura 4.7, en él está detallado gráficamente cuándo
se ejecuta cada una de las partes del for.
Cada una de las tres partes de que consta la sintaxis tiene su funcionalidad a la hora de controlar el
número de iteraciones que hace el bucle. Ası́, la inicialización permite declarar e inicializar la variable
que usaremos para controlar las veces que el bucle se ejecuta. Es la única parte que solamente se realiza
una vez, ya que tanto la condición como la actualización se hacen en cada iteración. Se puede ver
gráficamente en el diagrama de flujo (Figura 4.7), la inicialización está antes de lo que es el bucle en

86
inicialización

F
Condición

acción

actualización

Figura 4.7: Diagrama de flujo de la sentencia for

sı́ mismo. En el ejemplo, declaramos la variable i y la inicializamos a 1. La idea es “contar” de 1 a 10


(o lo que es lo igual, recorrer los números del 1 al 10) para hacer que el bucle se ejecute exactamente 10
veces.
No hemos llamado a la variable contador, tal como aparece en el pseudocódigo presentado anterior-
mente. El motivo es que en los bucles for es tradicional que los nombres de las variables de control que
se usen sean cortos, de forma que se agilice la escritura de la sentencia. Es además norma no escrita el
utilizar los nombres i, j, k, . . . cuando tenemos varios bucles anidados (ver Seccion 4.10.4). Este convenio
permite que cuando un programador ve de un vistazo un bucle for sepa que la variable i es la de control
del bucle. En general, en cualquier bucle, while y do-while incluidos, si se usa una variable con la que se
cuenta de alguna forma las veces que se realiza el bucle o recorre una secuencia de valores conocida, y la
variable no tiene un significado definido, lo habitual es usar como identificador i para facilitar la com-
presión del bucle. Un caso diferente es cuando los valores que toma tienen algún sentido más especı́fico
en el programa, en eso caso es preferible ponerle un nombre descriptivo. En todo caso es una cuestión
de estilo, aspecto bastante personal de cada programador.
La condición tiene exactamente las mismas implicaciones que vimos en el caso del while. Antes de
ejecutar el bucle por primera vez, justo después de hacer la inicialización, se comprueba la condición. Si
ésta fuera falsa el bucle no se harı́a ninguna vez. Si resulta cierta, entonces se hace la acción del bucle.
Como en todas las sentencias condicionales e iterativas, si la acción está formada por una secuencia de
instrucciones, entonces deben encerrarse entre llaves. La condición se comprobará justo antes de cada
iteración para decidir si debe hacerse o no.
En nuestro programa, dado que queremos hacer exactamente 10 repeticiones, nos mantendremos en
el bucle mientras la variable i no sea mayor de 10, es decir, mientras i<= 10. La condición será cierta
durante toda la cuenta de 1 a 10, y en el momento que se alcance el valor 11 será falsa y el bucle acabará.
Nótese que al comprobar la condición lo que expresa la variable i es el número de la iteración en la que
estamos. Cuando estamos en la iteración número 11, el bucle ya no se debe hacer más.
Lo último que entra en acción es la parte actualización. Es la menos intuitiva respecto al momento
en que se ejecuta. Aunque se escribe antes de la acción del bucle, las sentencias de la actualización
se realizan justo después de la acción como se muestra en la Figura 4.7. Es decir, la secuencia que se
va repitiendo es condición, acción, actualización. En la actualización se suelen poner instrucciones que
alteran, normalmente incrementan o decrementan, el valor de la variable de control del bucle que se ha
creado en la inicialización y que se utiliza en la condición. Es muy común usar en la actualización un
tipo de operadores que permiten sentencias de incremento o decremento de una forma muy corta. Son

87
los operadores de pre- y pos- incremento y decremento, que estudiaremos en la sección siguiente. En el
ejemplo la variable i se incrementa en una unidad usando el operador de posincremento (i++).
Es fundamental que las tres partes que componen el bucle for estén coordinadas desde un punto de
vista lógico. Al final lo que tienen que conseguir es que el bucle se haga el número de veces deseado y que
acabe justo en ese momento. Una descoordinación entre la lógica de las tres partes puede ocasionar con
cierta facilidad que el bucle haga un número de iteraciones incorrecto o que sea infinito. En el ejemplo
queremos contar de 1 a 10. Por ello la variable i va variando desde su valor inicial 1, de unidad en
unidad, recorriendo todos los números hasta 10 y luego 11. Es decir, con cada iteración que pasa la
diferencia entre el valor al que tiene que llegar la variable para que el bucle acabe (11) y el valor que va
teniendo (1,2,3,. . . ) se va reduciendo cada vez. Esto es lo que garantiza que por muy grande que fuera
el valor lı́mite al que se tiene que llegar en algún momento se alcanzará, ya que la diferencia entre dicha
cantidad y el valor de la variable de control se va reduciendo con cada iteración que se hace. Si en lugar
de incrementar la variable i la decrementáramos, entonces nunca llegarı́a a valer más de 10 y el bucle
serı́a infinito. En ese caso, la diferencia entre lo que vale i y lo que tiene que llegar a valer, en lugar de
disminuir con el paso de las iteraciones, aumentarı́a, por lo que el bucle serı́a infinito. En realidad no es
complicado combinar las tres partes, ya que no deja de ser una forma de “contar” desde un valor hasta
otro, pero habrá programas en los que tendremos que pensarlo con cierto cuidado para garantizar que el
bucle no es infinito y hace el número de repeticiones deseado.

4.8.1. Operadores de pre- y pos- incremento y decremento


Un elemento novedoso en el programa está en el tercer elemento del for. Para incrementar la variable
de control del bucle i, que nos permite contar cuántas veces se ejecuta éste, usamos un operador nuevo
que es un posincremento y que pertenece a un grupo que permite hacer incrementos y decrementos
escritos de una forma muy corta.
Se pueden usar con enteros y reales y producen dos cosas:

1. Un incremento o decremento en una unidad de la variable que aparezca en la expresión. Equivalen


a las asignaciones +=1 y -=1.

2. Un valor de la expresión en su conjunto. Dependerá de la posición del operador:

Si el operador va antes (pre-), devolverá el valor de la variable incrementada o decrementada.


Si va después (pos-), devolverá el valor que tenı́a la variable antes de que se haga el incremento
o decremento.

Un aspecto importante es que estos operadores solamente se puede emplear con variables (left-value)
ya que hacen una asignación al incrementar o decrementar. En la Tabla 4.6 están descritas las cuatro
posibilidades, cada una de ellas con un ejemplo que indica cómo queda la variable tras la operación y el
valor que produce la expresión en su conjunto.
Cuando se utilizan estos operadores en una sentencia for en realidad da igual si se utiliza un operador
pos- o pre- ya que el valor de la expresión no se emplea, lo único que se pretende es incrementar o
decrementar la variable de control del bucle. Se pueden usar cualquiera de las dos formas: la versión pre-
o la pos-.

Tabla 4.6: Operadores de pre- y pos- incremento y decremento. Los ejemplos suponen que la variable
a= 5 y el valor a la derecha es el resultado de la expresión. Entre paréntesis el valor de la variable a tras
la operación

Op. Uso Descripción Ejemplo (a= 5)


++ ++op Preincremento ++a 6 (a= 6)
++ op++ Posincremento a++ 5 (a= 6)
−− −−op Predecremento −−a 4 (a= 4)
−− op−− Posdecremento a−− 5 (a= 4)

88
4.8.2. Otras consideraciones sobre for
Para acabar esta sección sobre el bucle for vamos a comentar algunos detalles sobre su sintaxis que
pueden ser útiles en ciertos problemas. Pondremos cada uno de los casos y un pequeño ejemplo para que
se vea cómo se hace.

Tanto la parte de inicialización como la de actualización permiten más de una acción. Se separan
por comas.
for (int i=1, j=10; i < j; i++, j--) ...

En el ejemplo, en lugar de una variable de control, tenemos dos, una que se va incrementando
desde 1 (i) y otra que se va decrementando desde 10 (j). Tanto la parte de inicialización como la
de actualización tienen dos acciones separadas por comas. Este ejemplo es útil cuando se quiere
recorrer una secuencia de elementos en las dos direcciones simultáneamente, hacia delante y hacia
atrás.
Ninguna de las partes es obligatoria.
int i=1;
for ( ; i < j ; i++) {
...
} //i se puede usar tras el for

En este caso el objetivo es que la variable i pueda ser utilizada después del bucle. Cuando la
variable de control se declara en la parte de inicialización, la variable solamente puede ser usada
dentro del bucle, nunca después. Esto se discute en profundidad en la Sección 4.12. En el ejemplo
la variable se declara antes y la parte de inicialización se deja vacı́a.
Otro ejemplo muy conocido,
for ( ; ; ) {
...
if ( ... ) break;
}

Cuando no se incluye la condición del for ésta se toma como cierta. Es decir, el bucle en principio no
pararı́a nunca. Lo que suelen hacer los programadores que usan esta clase de algoritmo (totalmente
desaconsejable) para el que el bucle acabe alguna vez, es incluir entre las acciones del for una
instrucción break, normalmente dentro de una sentencia condicional. Es exactamente el ejemplo
que hemos puesto. Lo que se logra es que el bucle finalice cuando esa condición sea cierta. Se podrı́a
lograr lo mismo poniendo esa expresión negada como condición del for (para que éste continúe
mientras la expresión sin negar sea falsa) y reescribiendo apropiadamente las acciones del bucle. En
esta asignatura no haremos nunca un ejemplo como éste ya que resultan programas menos claros y
más difı́ciles de entender. Como ya hemos dicho anteriormente, como norma general no usaremos
nunca la instrucción break salvo en la sentencia switch.
Lo que no tiene sentido es dejar las partes de inicialización y actualización vacı́as simultáneamente,
ya que en ese caso es preferible usar un bucle while: el for solamente tendrı́a la condición y eso es
precisamente lo único que tiene un while.
Una sentencia for se podrı́a escribir con while y viceversa.
Con todas las consideraciones que hemos hecho sobre la sintaxis del for, y teniendo siempre en
mente que ambas instrucciones son semánticamente sentencias mientras, es claro que son intercam-
biables en un programa poniendo cada elemento en la posición adecuada. Lo que nunca hará falta
cambiar es la condición ya que, una vez más, ambas son sentencias mientras.
Por ejemplo, podrı́amos escribir el programa que suma una secuencia de positivos con for de la
siguiente manera:
//Suma de números pares con for
int suma=0;
for (int par=2; par<=número; par+=2 )
suma+=par;

89
La inicialización consiste en declarar la variable par con el valor 2. La condición naturalmente
sigue siendo la misma, y la actualización es pasar al siguiente número par, es decir, incrementar en
2 la variable par. El for también se adapta bien a este problema ya que se recorre una secuencia de
números enteros, en los que la acción para pasar de un número entero al siguiente es simplemente
sumarle 2. En otros programas, donde la actualización requerirı́a de acciones más complejas, como
leer un nuevo número de teclado, el bucle while produce programas más sencillos de entender.
Por estudiar la situación contraria, podemos hacer el programa que calcula la media de 10 números
reales con while. Resultarı́a algo como esto:
//Media de 10 reales con while
int i=1, suma=0;
while ( i <= 10 ) {
número=teclado.nextDouble();
suma+=número;
i++;
}
System.out.printf("Media %f",suma/10);

Teniendo en mente la semántica del for, lo que allı́ era la inicialización (int i=1;), en la versión
con while va justo antes del bucle. La condición, como antes, no cambia, y la actualización va al
final de las acciones del bucle, ya que en un bucle for se ejecutan después de las acciones y antes
de la siguiente evaluación de la condición. Las otras dos acciones dentro del bucle son las mismas
que tenı́amos en el for, primero leemos y luego sumamos.
Aunque los bucles con while siempre son fáciles de entender ya que tiene una sintaxis más simple
que la de for, en el caso particular de un bucle en el que se cuenten las iteraciones que hace es
más sencillo y corto escribirlo con for. Además, en la propia sentencia, con los tres elementos de
que constan los for en una sola lı́nea se ve claramente el rango de valores que recorre la variable
de control del bucle.
Como resumen, podrı́amos decir que aunque las dos sentencias son intercambiables, dependiendo
del problema una de ellas suele resultar más apropiada. En todo caso, la más clara es while y salvo
el caso particular de bucles que se ejecutan un número de veces conocido o en secuencias de enteros
concretas, será la que más empleemos en el resto de situaciones.

4.9. Bucle do-while


Como comentamos en la Sección 4.6, la diferencia fundamental entre los bucles mientras y repetir es
que estos últimos al menos hacen una iteración, mientras los primeros pueden no hacer ninguna. Vamos
a plantear un problema que requiere de una sentencia iterativa que se ejecute al menos una vez. Lo
resolveremos con do-while y explicaremos cómo funcionan este tipo de bucles.
enun- Hacer un programa que dado un número entero leı́do de teclado, imprima el número de dı́gitos que tiene.
ciado
Idea: cada vez que dividimos un número entero entre 10, el cociente tiene un dı́gito menos. Dado
que el número es entero, realizaremos una división entera, es decir, sin decimales. Si vamos dividiendo
sucesivamente llegará un momento en el que el cociente valga 0. Simplemente lo que debemos hacer es
contar las iteraciones que hace el bucle, ese será el número de dı́gitos.
Lo que más nos interesa del problema es que, a diferencia de programas anteriores, en este caso
tenemos:

1. Un bucle que al menos se repite una vez, y

2. que hace un número de iteraciones que desconocemos.

El bucle se repetirá al menos una vez ya que todos los números tienen al menos un dı́gito. En definitiva,
el problema plantea un escenario en que se puede emplear un bucle repetir. Por ejemplo, con el siguiente
pseudocódigo:

90
Algoritmo 4.6 Contar los dı́gitos de un no entero
Leer número de teclado
dı́gitos = 0
hacer
dı́gitos = dı́gitos + 1
número = número / 10
mientras número > 0
Imprimir dı́gitos

La traducción a lenguaje Java bien pudiera ser esta:

ContarDı́gitos.java
5 public class ContarDı́gitos {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Declaramos una variable entera para leer el número
11 int número;
12 //Leemos el no entero
13 System.out.print("Introduce un entero:");
14 número=teclado.nextInt();
15 //Una variable para ir contando sus dı́gitos
16 int dı́gitos=0;
17 do {
18 dı́gitos++;
19 número/=10;
20 }
21 while ( número != 0 );
22 //Mostramos el no de dı́gitos la pantalla
23 System.out.printf("Tiene %d dı́gitos\n",dı́gitos);
24 }
25 }

La novedad naturalmente es la presencia del bucle do-while. Como hacemos siempre, empezaremos
exponiendo su sintaxis y luego detallaremos su funcionamiento.

sintaxis Sentencia do-while:


do
acción
while ( condición );

Su diagrama de flujo aparece en la Figura 4.8. La diferencia con los bucles mientras es que en este
caso la condición no va al principio sino que va al final, haciendo que la acción se haga al menos una vez.
Las caracterı́sticas fundamentales del bucle do-while son:

Semántica: la acción se repite mientras la condición sea cierta. Esto es común a todos los bucles
en Java. Hay lenguajes en los que los bucles repetir se hacen mientras la condición es falsa. En
Java no existe esa diferencia. En realidad es más sencillo ası́, todos los bucles se hacen mientras su
condición sea cierta.

Número de ejecuciones: 1 ó más veces. Es su propiedad diferenciadora, el bucle hará al menos una
iteración.

Si la acción está compuesta de varias instrucciones se deben poner llaves. De nuevo ésta es una
propiedad común a todos los bucles y extensible a las sentencias condicionales como vimos.

En nuestro ejemplo, las acciones que se repiten son: hacer la división entera del número entre 10 e
incrementar en uno el número de iteraciones que se han hecho hasta el momento. Es importante destacar

91
acción

T
Condición

Figura 4.8: Diagrama de flujo de la sentencia do-while

que el resultado de cada división entera se vuelve a guardar en la propia variable número. Eso hace que
en cada iteración que se va haciendo el valor de la variable número va disminuyendo. Al hacer la división
entera entre 10, en concreto se pierde la cifra menos significativa (es el resto de la división entera).
Llegará un momento en que el resultado de la división sea 0, cuando al número le quede exactamente un
dı́gito. Luego la condición que debemos poner para seguir en el bucle es que la variable número tenga un
valor mayor que 0. Al final imprimimos el valor de la variable dı́gitos con la que hemos ido contado las
divisiones, o lo que es lo mismo, las iteraciones que el bucle ha hecho.
Si en el programa quisiéramos mantener al final del bucle el número original, simplemente tendrı́amos
que definir una variable auxiliar con la que se irı́an haciendo la divisiones sucesivas del bucle.

4.10. Esquemas iterativos tı́picos


Vamos a dedicar esta sección a clasificar las distintas categorı́as de esquemas iterativos que se van a
presentar en los programas que resolveremos en el curso, entendiendo por esquema iterativos las carac-
terı́sticas del bucle que se tiene que diseñar para resolver un problema. No nos referimos a si se trata de
utilizar un while o un do-while, sino a estudiar los distintos tipos de secuencias que se pueden resolver
usando bucles, qué diferencias tienen y cómo tratarlas con el algoritmo adecuado.
Básicamente existen dos tipos de problemas que se resuelven usando bucles:
1. Tratamientos de secuencias de elementos
Se caracterizan por hacer el mismo tratamiento con todos los elementos de una secuencia.
Hay que identificar la secuencia:
• Secuencias descritas por enumeración.
• Secuencias de longitud conocida.
• Secuencias delimitadas por un valor centinela.
2. Búsquedas asociativas
También tenemos una secuencia de elementos,
pero no tratamos los elementos, sino que
se busca el primer elemento de la secuencia que cumpla una propiedad.
En ambos tipos de problemas hay un elemento común, se manejan secuencias de elementos.
Vamos a estudiar qué es una secuencia y a dar varios ejemplos para aprender a identificarlas a partir del
enunciado de un problema.

92
4.10.1. Secuencias
La propia semántica de los bucles que hemos estudiado indica que sirven para repetir una acción
o un grupo de acciones varias veces. Si se analiza desde el punto de vista de los datos que maneja un
programa, implica que lo que hace un bucle es aplicar las mismas acciones a varios datos, y además lo
hace en orden. Es decir, un bucle trata un conjunto de datos y lo hace siguiendo un determinado orden
secuencial. Ese conjunto de datos es lo que denominaremos secuencia.

definición Secuencia: Sucesión ordenada de elementos que un programa procesará en ese mismo orden.

Para diseñar un bucle correctamente es imprescindible identificar la secuencia de elementos que debe
tratar. Para ello tenemos que:

1. saber qué elementos componen la secuencia,

2. en qué orden están, y

3. cuándo se acaba la secuencia.

Las secuencias a veces se identifican enumerando sus elementos y en otros casos no se conocen exac-
tamente sus elementos pero se sabe cómo empieza la secuencia y cuándo acaba. Una vez identificada
la secuencia, se podrá diseñar el bucle de forma que trate todos los elementos de la secuencia, ni uno
más, ni uno menos, y lo haga en el orden correcto. Podemos formalizar el diseño del bucle mediante la
definición de tres operaciones:

1. inicializar la secuencia (obtener el primer elemento),

2. pasar de un elemento al siguiente (obtener sgte. elemento), y

3. detectar el final de la secuencia (condición de parada).

Si definimos correctamente esas tres operaciones tenemos todos los elementos para poder diseñar co-
rrectamente el bucle que resuelve nuestro problema. Como ya hemos puesto varios ejemplos de problemas
resueltos usando bucles a lo largo del tema, vamos a volver atrás para mostrar las distintas secuencias
que cada uno de ellos trataba y definiremos las tres operaciones antes mencionadas.
Ejemplo #1: Sumar los números pares hasta número (incluido):

Secuencia: 2, 4, . . . , número.

Tipo: secuencia descrita por enumeración.

Operaciones:

1. Obtener primer elemento: i=2.


2. Obtener siguiente elemento: i=i+2.
3. Fin de la secuencia: i>número.

En este ejemplo la secuencia se define enumerando los valores, ya que sabemos exactamente qué valores
son: los números pares entre 2 y el valor de la variable número. Tal como hicimos en el programa de la
Sección 4.7, el bucle recorre exactamente esos valores en orden. La clave de este tipo de secuencias es
identificar la acción que nos permite pasar de un elemento de la secuencia al siguiente. En este caso
hay que sumar 2 para pasar al siguiente elemento de la secuencia (es el siguiente número par). El otro
elemento importante es detectar cuándo el bucle se debe acabar, en el ejemplo cuando la variable i que
recorre la secuencia de pares toma un valor mayor que el de número. La condición del bucle, dado que en
Java los bucles siempre se repiten mientras la condición sea cierta, es la negación del fin de la secuencia,
es decir, se sigue en el bucle mientras i<=número.
Ejemplo #2: Media de 10 números reales leı́dos de teclado:

93
Secuencia original: valor1, valor2, . . . , valor10.
Tipo: secuencia de longitud conocida (10 elementos)
Secuencia alternativa: la secuencia original de 10 números la podemos tratar recorriendo la secuencia
1, 2, . . . , 10 y en cada iteración leer de teclado el valor-i y sumarlo.
Operaciones:

1. Obtener primer elemento: i=1.


2. Obtener siguiente elemento: i=i+1.
3. Fin de la secuencia: i>10.

Aquı́ no podemos identificar los valores de los datos que va a tratar el bucle, ya que cuando diseñamos
el programa no sabemos los números que el usuario tecleará. Pero sin embargo conocemos que son 10,
es decir, estamos ante una secuencia de longitud conocida, en la que los elementos de la secuencia se
identifican por su orden dentro de la misma. El bucle, como hicimos en la Sección 4.8 dedicada al for,
se diseña como si la secuencia fuera 1, . . . , 10 porque lo que sabemos es su cardinalidad, no los elementos
que la componen, ni siquiera si hay un valor centinela que los delimita. La condición del bucle era de
nuevo lo contrario de la condición que define el final de la secuencia: i<= 10.
Ejemplo #3: Contar los dı́gitos de un número entero leı́do de teclado:

Secuencia: número, número/10, número/100, . . . , 0.


Ejemplo: 147539, 14753, 1475, 147, 14, 1, 0.
Tipo: secuencia delimitada por un valor centinela.
Operaciones:

1. Obtener primer elemento: número.


2. Obtener siguiente elemento: número=número/10.
3. Fin de la secuencia: número==0.

En este caso tampoco somos capaces de enumerar los elementos de la secuencia antes de ejecutar el
programa, ya que depende del valor del número entero que introduzca el usuario. Sin embargo conocemos
dos cosas: cuándo se acaba (valor centinela, 0) y cómo pasar de un elemento al siguiente (dividir entre
10). La traducción a las sentencias del bucle es sencilla conocidos esos elementos. La condición, como
en los casos anteriores, es la negación del fin de la secuencia (número! =0), esto es, seguimos en el bucle
mientras no se alcance el valor centinela, de ahı́ su nombre.
Como se puede observar ya hemos hecho varios ejemplos del primero de los dos grupos de esquemas
iterativos: los tratamientos de secuencias de elementos. Sin embargo, antes de presentar las búsquedas
asociativas, el otro tipo de esquemas, vamos a formalizar los tratamientos de secuencias y a resolver un
par de nuevos problemas.

4.10.2. Tratamientos de secuencias de elementos


Tras todos los ejemplos presentados, podemos caracterizar la forma de proceder para diseñar el
tratamiento de una secuencia de elementos. Se deben seguir los siguientes pasos:

Identificar la secuencia.
Definir sus 3 operaciones: primer-elemento, sgte-elemento, fin-secuencia.
Escribir el algoritmo eligiendo el tipo de bucle:

1. for: secuencias de longitud conocida u otras en las que la operación sgte-elemento es simple,
por ejemplo, una operación de incremento o decremento tı́pica de un bucle for.
2. while, do-while: resto de casos, especialmente cuando sgte-elemento es más compleja.

94
Una vez caracterizada la secuencia, puede ser implementada o bien con un bucle repetir o mientras
en función de si la secuencia tiene al menos un elemento o puede no tener ninguno, respectivamente.
Además, podemos diferenciar si nos encontramos en una secuencia de longitud conocida o definida por
enumeración o con un valor centinela. Veamos cada caso.

Secuencias enumeradas y con valor centinela


Los esquema generales que se aplican para cada uno de los tipos de bucles (repetir o mientras) se
puede ver en los algoritmos a continuación:

Algoritmo 4.7 Tratar secuencias (mientras) Algoritmo 4.8 Tratar secuencias (repetir)
elemento=primer-elemento elemento=primer-elemento
mientras NO fin-secuencia hacer hacer
Tratar elemento Tratar elemento
elemento=sgte-elemento elemento=sgte-elemento
fin mientras mientras NO fin-secuencia

Los esquemas anteriores deben entenderse como generales para los casos de secuencias definidas por
enumeración y para las que tienen un valor centinela. No para el caso de las secuencias de longitud
conocida, que estudiaremos posteriormente. Fijándonos en las acciones del bucle, el tratamiento del
elemento siempre precede a la obtención del siguiente elemento. Esto es ası́ ya que en los dos tipos de
secuencias (por enumeración y valor centinela) cuando se obtiene el siguiente elemento no se sabe si
pertenece a la secuencia o no. Solamente podrá ser tratado en la iteración siguiente, pero siempre y
cuando comprobemos antes que la condición sea cierta y sepamos que la secuencia no se ha acabado
todavı́a.

Secuencias de longitud conocida

En el caso de las secuencias de las que se conoce su longitud se puede aplicar un esquema más
simple, ya que la variable de control del bucle no tiene nada que ver con la secuencia. Estas secuencias
se caracterizan por lo siguiente:

No hace falta comprobar si cada elemento de la secuencia pertenece o no.

Comprobamos el número de iteraciones.

En algunos programas es más natural obtener primero el elemento y luego tratarlo. Elimina la
necesidad de obtener el primer elemento antes del bucle.

Con todo ello podrı́amos aplicar un esquema más simple:

Algoritmo 4.9 Tratar secuencias (longitud conocida N)


para i desde 1 hasta N hacer
Obtener elemento
Tratar elemento i-ésimo
fin para

Es exactamente el esquema que usamos en el programa para hacer la media de los 10 números reales.
Los programas que tratan secuencias de longitud conocida se adaptan mejor al bucle for, ya que podemos
contar fácilmente las veces que se repite el bucle. Incluso aún en el caso de que sepamos que la secuencia
tiene más de un elemento, y por tanto serı́a formalmente un bucle repetir, es habitual usar for en lugar
de do-while en aquellas situaciones en las que la sintaxis del for lo hace más sencillo. La facilidad a la
hora de escribir el bucle con for, tiene su contrapartida en términos de eficiencia en el hecho de que al
usar for la condición se comprueba una vez más que si usáramos do-while. Esa vez extra se corresponde
con la comprobación de la condición la primera vez. En muchos problemas ese detalle no es crı́tico y
por ello se utiliza for. Es el caso de nuestro programa que calcula la media de 10 números reales. La

95
secuencia tiene más de un elemento siempre, luego deberı́a ser un bucle repetir, pero escribirlo con for
es mucho más cómodo y en este programa la pérdida de eficiencia es irrelevante.
Los bucles while y do-while se adaptan mejor a aquellas situaciones en las que las operaciones para
recorrer la secuencia son más complejas que las habituales que se ponen en la parte de actualización de
un bucle for, que suelen ser simples incrementos o decrementos. Si la operación para pasar al siguiente
es muy complicada, escribirla con for es menos elegante y es preferible usar los otros bucles para crear
un código más sencillo de entender.
Una vez estudiados en profundidad los tratamientos de secuencias de elementos, vamos a poner un
par de ejemplos rápidos para mostrar que con las técnicas comentadas se pueden diseñar las soluciones
de muchos programas.
enun- Hacer un programa que dada una secuencia, posiblemente vacı́a, de números enteros positivos introducidos
ciado por teclado y que finaliza cuando se introduzca un número negativo, calcule su suma.

En este problema no podemos enumerar los elementos de la secuencia, ni tampoco conocemos la


longitud de la misma. Sin embargo, el enunciado es muy preciso al describir la secuencia de números
positivos, nos está diciendo que puede ser vacı́a (clave para decidir el tipo de bucle) y cómo finaliza (valor
centinela).

Secuencia: valor centinela negativo.


Ejemplos:
• -23
• 4 3 2 1 -1

Operaciones:

1. primer-elemento: leer número.


2. sgte-elemento: leer número.
3. fin-secuencia: número< 0.

El algoritmo serı́a una traducción directa del esquema que pusimos anteriormente usando mientras
ya que la secuencia puede ser vacı́a, como en el primero de los ejemplos.

Algoritmo 4.10 Suma de números positivos


suma = 0
Leer número de teclado
mientras número ≥ 0 hacer
suma = suma + número
Leer número de teclado
fin mientras
Imprimir suma

La implementación en Java de ese algoritmo podrı́a ser la siguiente:

SumaNúmerosPositivos.java
6 public class SumaNúmerosPositivos {

8 public static void main(String[ ] args) {


9 //Objeto Scanner asociado con el teclado
10 Scanner teclado= new Scanner(System.in);
11 //Declaramos una variable entera para leer los números
12 int número;
13 //y otra para ir calculando su suma
14 int suma=0;
15 System.out.print("Secuencia de enteros positivos:");
16 //Secuencia: números positivos... número negativo
17 //Leemos el primer entero
18 número=teclado.nextInt();

96
19 while ( número >= 0 ) {
20 suma += número; //sumamos
21 número=teclado.nextInt(); //leemos el siguiente
22 }
23 //Mostramos la suma en la pantalla
24 System.out.printf("Su suma es %d\n",suma);
25 }
26 }

enun- Hacer un programa que dado un número entero positivo leı́do de teclado, imprima todos los divisores de
ciado ese número.

Imaginemos que leemos el entero en una variable llamada número. Sabemos que los divisores de un
número entero son todos aquellos enteros que al dividir ese número producen un resto igual a cero. Para
poder ser divisores deben ser obligatoriamente menores o iguales que número: ningún entero tiene un
divisor mayor que él. Luego la definición de la secuencia será:

Secuencia: 1, 2, . . . , número (enumeración de elementos).


1. primer-elemento: i=1.
2. sgte-elemento: i=i+1.
3. fin-secuencia: i>número.

Se podrı́a argumentar con razón que para todo valor de número es seguro que 1 y el propio número son
divisores suyos. Podrı́amos sacarlos de la secuencia, e imprimirlos respectivamente antes y después del
bucle. La secuencia podrı́a quedar en ese caso entre 2 y número−1. Es más, también se sabe que no existe
ningún divisor entre número/2 + 1 y número−1, con lo que podrı́amos reducir la secuencia y dejarla entre 2
y número/2. Dado que es uno de nuestros primeros ejemplos, vamos a obviar esos detalles y recorreremos
la secuencia antes comentada.
Una vez identificada la secuencia, tenemos que decidir el bucle a utilizar. Como es un secuencia de
enumeración de elementos en la que la acción sgte-elemento es un incremento, vamos a utilizar un bucle
for:

Algoritmo 4.11 Imprimir los divisores de un no entero


Leer número de teclado
para i desde 1 hasta número hacer
si número % i == 0 entonces
Imprimir i
fin si
fin para

Los valores que va tomando la variable i son exactamente los valores de la secuencia definida. Con
cada uno de ellos comprobamos si es divisor calculando el resto de dividir número por i. Si el resto es
igual a cero, entonces el valor que tiene i en esa iteración es un divisor de número y lo imprimimos en
pantalla. Nótese que para obtener el siguiente elemento se utiliza la actualización del bucle for, que
como se ha indicado es la tercera y última parte de la sintaxis del bucle for. Es decir, este algoritmo es
semánticamente equivalente al descrito en el Algoritmo 4.7. La implementación en Java es prácticamente
directa:

Divisores.java
5 public class Divisores {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Declaramos una variable entera para leer el número
11 int número;
12 //Leemos el no entero
13 System.out.print("Introduce un entero:");
14 número=teclado.nextInt();

97
15 //Imprimimos sus divisores
16 //Secuencia: i: 1..número
17 for (int i=1; i<=número; i++)
18 //Es divisor si el resto da cero!
19 if ( número % i == 0)
20 System.out.printf(" %d ",i);
21 }
22 }

4.10.3. Búsquedas asociativas


Vamos a estudiar el otro gran grupo de esquemas iterativos tı́picos, son las búsquedas asociativas.
¿Por qué las diferenciamos de los tratamientos de secuencias de elementos? Podrı́amos resumirlo en los
siguientes aspectos:

No se pretende hacer una cierta acción sobre todos los elementos de una secuencia.

Los objetivos de las búsquedas asociativas son:

1. Determinar si en la secuencia hay o no algún elemento que cumpla una cierta propiedad.
2. En caso de existir, encontrar el primer elemento que cumple dicha propiedad.

No siempre se recorren todos los elementos de la secuencia. El bucle puede finalizar por dos motivos:

1. se ha recorrido toda la secuencia y ninguno de sus elementos cumple la propiedad buscada, o


2. se ha encontrado el primer elemento que la cumple, luego debe finalizar.

Esto hace que el bucle tenga una condición formada por dos términos.

Además, obliga a que después del bucle comprobemos por cuál de los dos términos ha acabado el
bucle.

A diferencia de los tratamientos de secuencias, donde siempre se tratan todos los elementos, es decir,
se hacen sobre todos ellos las acciones internas del bucle, en las búsquedas habitualmente no se hace
ninguna acción, sino que simplemente se comprueba para cada uno de los elementos si cumple o no la
propiedad buscada. En el momento que uno de ellos la cumpla, el bucle debe finalizar ya que habremos
verificado (y localizado) que sı́ existe un elemento que cumple la propiedad buscada.
Para escribir formalmente un algoritmo de búsqueda deben definirse dos cosas:

1. La secuencia y sus 3 operaciones: primer-elemento, sgte-elemento, fin-secuencia.

2. La propiedad que se quiere encontrar entre los elementos de la secuencia: elemento-encontrado

Una vez definidos esos elementos, el algoritmo genérico para resolver una búsqueda es el siguiente:

Algoritmo 4.12 Búsquedas asociativas


elemento=primer-elemento
mientras NO fin-secuencia Y NO elemento-encontrado hacer
elemento=sgte-elemento
fin mientras
si NO fin-secuencia entonces
se ha encontrado el elemento
sino
NO se ha encontrado el elemento
fin si

98
El detalle más novedoso es que la condición del bucle está formada por dos términos. El bucle continúa
siempre que la secuencia no se haya acabado Y no se haya encontrado ningún elemento que cumpla la
propiedad que se está buscando. Normalmente ese segundo término se limita a comprobar si el elemento
actual cumple o no la propiedad. El motivo es que previamente ya se comprobó que ninguno de los
elementos anteriores la cumplı́an; en caso de que alguno lo hiciera el bucle ya habrı́a acabado, ası́ que
solamente hay que comprobar el elemento actual. A diferencia de los tratamientos de secuencias, en
las búsquedas asociativas es muy tı́pico que las acciones del bucle suelan limitarse a pasar al elemento
siguiente.
Como el bucle tiene una condición con dos términos unidos por un Y, cuando finaliza será porque
una de las dos partes de la condición es falsa. Es decir, o bien se ha llegado al final de la secuencia, o
bien hemos encontrado el primer elemento que cumple la propiedad buscada. Por ese motivo, después
del bucle es necesario verificar cuál de las dos expresiones ha ocasionado que el bucle acabe, para ello
usamos una sentencia if. Lo más seguro es siempre comprobar si la secuencia se ha acabado o no. Si no
se ha acabado, quiere decir que entonces el elemento se ha encontrado ya que el término NO elemento-
encontrado será falso. En caso contrario, si la secuencia se ha acabado, evidentemente es porque no se
ha encontrado ningún elemento que cumpla lo que se busca.
Puede pensarse que en ese if también se podrı́a comprobar el segundo término de la condición del
bucle en lugar del primero, es decir, si el elemento actual cumple o no la propiedad de la búsqueda que
estamos haciendo. En algunos casos es ası́, en el if posterior podrı́an emplearse cualquiera de las dos
condiciones. Sin embargo, hay muchas situaciones en las que eso es desaconsejable. El motivo principal
es porque puede ser que estemos fuera del rango que define la secuencia, con lo que el elemento que se
comprobarı́a en el if serı́a un elemento que no pertenece a la secuencia. En otros casos, por ejemplo
en las búsquedas en vectores, incluso podrı́amos estar tratando el valor de un elemento que ni siquiera
existe, lo que ocasionarı́a un error en tiempo de ejecución. Esto lo veremos con detalle en el Tema 5. Lo
más seguro y lo que nunca falla es comprobar si al finalizar el bucle se ha llegado al final de la secuencia
o no.
Vamos a aplicar este algoritmo genérico sobre un problema.

enun- Hacer un programa que lea un número entero positivo de teclado e imprima si el número es primo o no.
ciado
Para saber si un número es primo tenemos que determinar si tiene algún divisor distinto de 1 y de
él mismo. Si tiene algún divisor el número no es primo, y si no lo tiene el número es primo. Luego
es un problema que se basa en trabajar con los posibles divisores de un número. Se parecerá en algo
al programa que hicimos anteriormente para imprimir los divisores de un número. Sin embargo existe
una diferencia fundamental. En aquel programa lo que querı́amos era imprimir todos sus divisores y
ahora lo único que necesitamos es saber si tiene alguno. Por eso el anterior lo resolvimos tratando todos
los elementos de la secuencia y éste lo resolveremos con una búsqueda: cuando encontremos un divisor
finalizaremos el bucle.
Los elementos que hay que definir son la secuencia y la propiedad buscada:

Secuencia: 2, . . . , número/2 (enumeración de elementos).

Propiedad: número % i==0

La primera cosa que cambia con respecto al programa de los divisores es que hemos variado la
secuencia de búsqueda. No empezamos en 1, ni acabamos en el número, ya que lo que necesitamos es
precisamente encontrar un divisor distinto de ambos valores. Podrı́a decirse que la secuencia deberı́a ser
entonces entre 2 y número-1, pero como ya discutimos anteriormente, para ningún número existen divisores
mayores que número/2+1, salvo el propio número. En este caso hemos optado por hacer un bucle más
eficiente reduciendo la secuencia de búsqueda2 .
Usando esa definición el algoritmo quedarı́a ası́:

2 Podrı́a

reducirse aún más haciendo que vaya de 2 a número.

99
Algoritmo 4.13 Número primo
Leer número de teclado
i=2
mientras (i<=número/2) Y (número % i!=0) hacer
i=i+1
fin mientras
si (i<=número/2) entonces Imprimir NO ES primo
sino Imprimir ES primo
fin si

Como indicaba el algoritmo genérico para búsquedas asociativas que antes se presentó, en la condición
del bucle se comprueba que no se ha acabado la secuencia y que no hemos encontrado el elemento que
buscamos. Es decir, se niega el fin de la secuencia (!(i>número/2) → (i<=número/2)) y también se niega
la propiedad buscada (!(número % i==0) → (número % i!=0)). Ambas se unen por el operador Y. Al final
del bucle comprobamos si estamos o no en la secuencia. Si es ası́ quiere decir que hemos salido del bucle
por encontrar un divisor y el número no es primo. Si la secuencia se ha acabado implica que no hemos
encontrado un divisor y por tanto el número es primo.
La traducción del algoritmo a código Java es la siguiente:

Primo.java
5 public class Primo {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Declaramos una variable entera para leer el número
11 int número;
12 //Leemos el no entero
13 System.out.print("Introduce un entero:");
14 número=teclado.nextInt();
15 //Hacemos una búsqueda asociativa
16 //Secuencia: i: 2..número/2
17 //Propiedad: i divisor de número
18 int i=2; //para recorrer la secuencia
19 while ( ( i <= número/2 ) && ( número % i !=0 ) )
20 i++;
21 //Mostramos si es primo o no
22 if ( i <= número/2 )
23 System.out.printf(" %d NO es primo\n",número);
24 else System.out.printf(" %d SÍ es primo\n",número);
25 }
26 }

A partir de ahora, en todos los programas que precisen de un bucle lo primero que haremos es deter-
minar es si el algoritmo se resuelve tratando una secuencia o haciendo una búsqueda. Una vez decidido
el esquema apropiado, hay que definir sus elementos, aplicar el algoritmo genérico correspondiente e
implementarlo en Java.

4.10.4. Bucles anidados


A medida que la complejidad de los problemas se incrementa, para llegar a resolverlos no basta con
un solo bucle que haga un tratamiento de una secuencia o una búsqueda. Muchas veces se requiere de
varios bucles y en ocasiones dentro de las acciones de uno de ellos tenemos que hacer el tratamiento de
una secuencia o una búsqueda. Es decir, tenemos bucles dentro de otros, o lo que es lo mismo, bucles
anidados.
Vamos a analizarlo con un problema que necesita de la búsqueda realizada en la sección anterior.
enun- Hacer un programa que imprima los números primos entre 1 y 1000.
ciado
Para saber si un número es primo podemos emplear el bucle del programa realizado antes. Además,
el enunciado indica que tenemos que imprimir todos los primos entre 1 y 1000. Es evidente que para

100
lograrlo hay que tratar esa secuencia de números enteros. Luego tenemos dos secuencias y dos esquemas
iterativos anidados:

Esquema iterativo #1 (tratamiento)

1. Secuencia. j: 1 .. 1000
2. Acción: saber si el número es primo o no.

Esquema iterativo #2 (búsqueda)

• Secuencia. i: 2 .. j/2
• Propiedad: j % i == 0.

Por tanto, plantearemos un algoritmo con dos bucles anidados:

Algoritmo 4.14 Números primos entre 1 y 1000


j=1;
mientras j<=1000 hacer
i=2
mientras (i<=j/2) Y (j % i!=0) hacer
i=i+1
fin mientras
si (i>j/2) entonces Imprimir j
fin si
j=j+1
fin mientras

En código en Java podrı́a ser el que sigue a continuación:

NúmerosPrimos.java
3 public class NúmerosPrimos {

5 public static void main(String[ ] args) {


6 //Hacemos un tratamiento de una secuencia
7 //Secuencia: 1..1000
8 for (int j=1; j<=1000; j++) {
9 //Hacemos una búsqueda asociativa
10 //Secuencia: i: 2..j/2
11 //Propiedad: i divisor de j
12 int i=2; //para recorrer la secuencia
13 while ( ( i <= j/2 ) && ( j % i !=0 ) )
14 i++;
15 //Mostramos si es primo o no
16 if ( i > j/2 ) System.out.printf(" %d ",j);
17 }
18 }
19 }

Usamos un bucle for para el primer bucle ya que facilita el recorrido de la secuencia de 1 a 1000.
El bucle interior es exactamente el código de la búsqueda asociativa mostrado anteriormente, salvo que
ahora en lugar de determinar si un número leı́do de teclado es primo o no, tenemos que hacerlo con el
valor actual de la variable de control del for, llamada j.
Como se puede ver, no es tan difı́cil diseñar bucles anidados si se analiza bien el problema:

Se deben abstraer ciertas operaciones complejas (serán los bucles internos). Por ejemplo en este
programa de los números primos, lo primero es centrarse en la secuencia de números del 1 al 1000.
Eso define el primer bucle. Una vez planteado ese bucle, es cuando se tiene que diseñar la búsqueda
para determinar si un número es primo y hacer el bucle anidado.
Se deben inicializar siempre las variables de control justo antes. Es decir, deben hacerse siempre
declaraciones e inicializaciones como ésta:

101
i = ...
while ( i ... ) {
j = ...
while ( j ... ) {
...
}
}

en lugar de esto otro:


i = ...
j = ...
while ( i ... ) {
//OJO! j NO se inicializa cada vez
while ( j ... ) {
...
}
}

Es muy frecuente entre los programadores que empiezan cometer el error contenido en este segundo
ejemplo. En primer lugar, recalcar que una variable puede declararse en cualquier punto del programa,
no es obligatorio declararlas al principio, ni a la vez. Es decir, si se tienen dos bucles anidados no es
obligatorio declarar las dos variables de control al principio. Si se hiciera ası́, la variable del segundo bucle
solamente se inicializarı́a la primera vez, lo que constituye un error muy grave de diseño. La regla de oro
es inicializar las variables de control de un bucle justo antes de que ese bucle comience. Eso garantiza
que en el supuesto de que dicho bucle esté dentro de otro, su variable de control se inicializará cada vez
y no solamente una vez, al principio del bucle exterior.
4
! Las variables de control de un bucle se deben inicializar justo antes de su inicio. Es espe-
cialmente crucial en los bucles anidados.

4.11. Pruebas Funcionales


Después de haber estudiado con detalle los esquemas condicionales e iterativos, que son las instruc-
ciones básicas con las que se programan las funcionalidades de las aplicaciones, vamos a estudiar cómo
probar un programa sencillo que se base en sentencias condicionales y bucles.
En la Sección 1.7 del Tema 1 describimos qué son las pruebas en general, en qué consisten, los tipos
que existen y dimos una breve introducción a las pruebas funcionales. El objetivo en esta sección va a ser
estudiar cómo realizar pruebas funcionales en pequeños programas que tengan sentencias condicionales
y bucles. Cómo probarlos de forma que podamos estar seguros que funcionan correctamente.
Las pruebas funcionales se basan en la ejecución o en la revisión de las funcionalidades que debe tener
el programa. Se realizan fundamentalmente diseñando casos de prueba que sirven para verificar dichas
funcionalidades. Hay dos enfoques fundamentales a la hora de realizar pruebas funcionales:

1. Caja blanca: consiste en examinar el propio código, comprobando los caminos lógicos del progra-
ma, los bucles y las condiciones, y analizando el estado del programa en diversos puntos.

2. Caja negra: consiste en tratar el código como una caja negra que recibe una entradas y produce
una salida. Tı́picamente se diseñan una baterı́a de casos de prueba y el programa debe producir en
todos ellos la salida correcta.

La diferencia entre ambos enfoques es que las pruebas de caja blanca requieren tener un conocimiento
del código, por lo que suelen ser realizadas por el propio equipo que ha desarrollado la aplicación. En
cambio, las pruebas de caja negra no requieren conocer ningún detalle de cómo está implementado. Esto
hace que puedan ser realizadas por un equipo distinto al equipo desarrollador. De hecho, es preferible
que las pruebas de caja negra no las realicen las mismas personas que escribieron el programa.

102
4.11.1. Pruebas de caja blanca
Como se ha dicho, las pruebas de caja blanca analizan el código tratando de comprobar que está bien.
En el contexto de este tema, nuestro objetivo será verificar que las sentencias condicionales e iterativas
que hemos escrito en un programa sean correctas, no ya sintácticamente, que eso lo evalúa el propio
compilador, sino de acuerdo a la lógica del programa y a las funcionalidades que debe cumplir.
Un aspecto muy importante para lograr ese objetivo es garantizar que todas las partes del código están
bien. Esto no es tan sencillo cuando se combinan muchas sentencias condicionales y bucles. Por ejemplo,
cuando escribimos varias sentencias if-else anidadas, o una sentencia switch, hay que garantizar que
las pruebas que realicemos comprueban todos los casos. Es muy frecuente que, si se realizan unas pocas
pruebas sin diseñarlas apropiadamente, alguno de los casos nunca haya sido realmente probado.
4
! Hay que tratar de probar TODAS las sentencias del programa.
Solamente probando todo el programa, es decir, todas sus sentencias, podemos garantizar que todo
él puede ser correcto. Este objetivo nos lleva directamente a definir el concepto de cobertura.

definición Cobertura: Es una medida porcentual que indica la cantidad de código que ha sido cubierto (o
probado) durante las pruebas realizadas.

Es decir, si decimos que un programa se ha alcanzado una cobertura del 70% quiere decir que en las
pruebas el 70% del código ha sido probado. Hay diferentes formas de medir la cobertura:

Cobertura de segmentos. Ejecutar todos los segmentos del programa. Se entiende por segmento un
conjunto de sentencias simples consecutivas si ningún punto de decisión, es decir, sin condiciones.
Cobertura de ramas. Ejecutar todos los caminos posibles. Tener una cobertura del 100% de seg-
mentos no garantiza que se hayan ejecutado todos los trozos de código del programa, ya que si
tenemos una sentencia condicional puede que no hayamos pasado por todas sus ramas. En las
sentencias if-else hay que probar tanto el bloque if, cuando la condición es verdadera, como el
bloque else cuando es falsa. Lo mismo con las sentencias switch; habrı́a que pasar por todos sus
casos. Además, hay que tener en cuenta los posibles anidamientos. A medida que el anidamiento se
incrementa, también lo hacen los caminos posibles, con lo que es más laborioso probar todos ellos.
Hay que tratar de probar todas las ramas del programa.
Cobertura de condiciones. Cuando tenemos condiciones complejas, hay que probar todas las com-
binaciones posibles. Por ejemplo, si tenemos una condición formada por dos condiciones simples
unidas por un operador (&& ó ||) hay que probar todas sus combinaciones. Como cada condición
simple tiene dos valores posibles (cierto y falso), hay 4 combinaciones. Hay que probar las 4.
Cobertura de bucles. Hay que probar casos en los que no se entre en el bucle (si eso es posible),
casos en los que se ejecute varias veces y comprobar que no sea infinito.

Los bucles son especialmente delicados a la hora de probar un programa. Es muy fácil cometer un
error al diseñar un bucle, por lo que son una fuente tı́pica de errores. Es muy habitual, por ejemplo, que
un bucle se ejecute una vez más o menos de las que deberı́a. Es importante hacer las pruebas necesarias
para que el bucle se pruebe en distintas situaciones. No puede ocurrir que al probar el programa, en
un bucle concreto, por el motivo que sea, nunca se ejecuten sus sentencias. Por ejemplo, si tenemos un
bucle while y en todas las pruebas que hacemos la condición siempre ha sido falsa, entonces el bucle en
realidad no se ha probado. Hay que probar cada bucle en todos los casos: que no se entre en el bucle,
pero también que el bucle se ejecute varias veces. Ası́, un bucle while debe probarse al menos para que
haga ninguna iteración, una iteración y más de una iteración. Un bucle do-while con una iteración y
más de una iteración. Los bucles for son ligeramente más sencillos de probar, ya que los lı́mites suelen
ser más claros. El error más frecuente con los bucles for es que se ejecuten una vez más o una vez menos
de la cuenta.
El otro aspecto fundamental con los bucles en general es comprobar también que no sean infinitos,
es decir, que cuando se entra en un bucle porque la condición ha sido cierta, que en algún momento el
bucle finalice. Normalmente la forma de hacer todas estas comprobaciones es hacer pruebas que ejecuten
los bucles hasta sus lı́mites.

103
El grado de cobertura cuando se prueba un programa grande es muy difı́cil que alcance el 100%,
ya que el programa tiene muchas sentencias y serı́a muy laborioso alcanzar ese grado de cobertura. El
grado que se considera aceptable para un programa depende de lo delicado que sea para sus usuarios.
No es lo mismo el grado de cobertura que se alcanza cuando se prueba una aplicación que se usa en un
entorno sanitario y que podrı́a afectar a la vida de personas, que una aplicación para jugar. Aunque un
juego falle alguna vez, lo cuál no serı́a bueno ni deseable para la empresa que lo vende, no afecta tanto a
sus usuarios. En estos últimos casos, los grados de cobertura son menores, mientras que en aplicaciones
sanitarias o militares requieren de grados de cobertura del 100% o muy próximos. Hay que recordar que
algún accidente espacial tuvo su origen en un error software. El código de ese tipo de aplicaciones tiene
que ser estudiado y probado con una rigurosidad máxima.
Una herramienta para realizar las pruebas de caja blanca son los depuradores. Todos los entornos de
desarrollo, como el que usamos en prácticas, suele incluir un depurador.

definición Depurador: Programa integrado en los entornos de desarrollo que permite localizar y corregir los
errores que contienen los programas.

El depurador permite:

Ejecutar paso a paso las sentencias del programa.

Ejecutar el programa hasta una cierta sentencia o hasta que se cumpla una cierta condición. Estos
puntos donde se detiene la ejecución se denominan puntos de ruptura o breakpoints.

Ir viendo la evolución de la variables del programa a medida que sus sentencias se ejecutan.

Es por ello que los depuradores permiten analizar el código, comprenderlo mejor y con ello detectar
los errores que puede tener. Además, cuando se está aprendiendo a programar, los depuradores son
una herramienta ideal para aprender a visualizar mentalmente los flujos de los programas. A muchos
estudiantes les cuesta entender cómo se ejecuta realmente un bucle y cómo va cambiando el valor de
cada variable. Todo eso se puede ver en vivo si el bucle se ejecuta en un depurador paso a paso.
4
! La ejecución paso a paso de los bucles ayuda a comprender mejor cómo funcionan realmente
y cómo cambian sus variables.

4.11.2. Pruebas de caja negra


Las pruebas de caja negra consisten en probar la funcionalidad sin tener en cuenta, ni siquiera hace
falta conocer, la estructura ni las sentencias del programa. Partiendo de la especificación de requisitos
funcionales (ver Sección 1.7.2) se determinan cada una de las funcionalidades del programa y los casos
de prueba que se deben realizar. El probador introduce unos datos de entrada y espera que la salida sea
la correcta. Cada una de esas pruebas es un caso de prueba. El objetivo es encontrar casos en los que
el programa no funciona. Para que el programa pueda ser correcto debe hacer todos los casos de prueba
correctamente.
El problema de las pruebas de caja negra es que probar todas las posibles entradas de un programa es
casi siempre imposible. Dependiendo del número de parámetros de entrada y de los valores posibles que
éstos puedan tomar, el número de combinaciones resultantes es muy grande, incluso infinito. Pensemos
simplemente en un método que reciba dos parámetros, un entero y un real. ¿Cuántas combinaciones
posibles hay para dicho par de parámetros? Las buenas noticias son que tampoco es necesario probar
todas las combinaciones.
4
! No hace falta probar todas las posible entradas, probando ciertas entradas (clases de equi-
valencia) puede bastar para verificar que un programa funciona.
Efectivamente, aunque el número de valores de entrada posibles sea muy grande en muchas ocasiones
simplemente hace falta probar unos pocos casos. Esto se basa en un concepto que tiene que ver con
álgebra, las clases de equivalencia. La idea es partir el conjunto posible de valores de entrada en varios
subconjuntos que serán las clases de equivalencia. Para cada subconjunto se cumple que si el programa
funciona para uno de sus valores, entonces también funciona para todos los demás. Solamente hará falta

104
probar unos de los valores de cada subconjunto para verificar que el programa funciona para todos los
valores posibles. Esto que parece complicado es sencillo en muchos casos. Vamos a verlo con el siguiente
ejemplo.
Ejemplo: Tenemos una entrada que representa el valor del mes en una fecha. Sin necesidad siquiera
de ver el código en donde se utiliza ese valor, hay tres clases de equivalencia:

1. Menos de 1 (valores incorrectos menores).

2. Entre 1 y 12 (valores correctos).

3. Más de 12 (valores incorrectos mayores).

Habrı́a que probar al menos un valor de cada una de las clases de equivalencia. Además, es sabido
que muchos errores se dan en torno a los valores que están en las fronteras de las clases de equivalencia.
Se denominan valores lı́mite y conviene probar dos valores por cada valor lı́mite, uno justo en el lı́mite y
otro fuera de ese lı́mite. Es decir, en resumen deberı́amos hacer las siguientes pruebas: un valor aleatorio
menor que 1, un valor aleatorio entre 1 y 12, un valor aleatorio mayor que 12, y además, los valores
lı́mite. Dado que los lı́mites son 1 y 12, probarı́amos con 0, 1, 12 y 13.

4.11.3. Ejemplos de casos de pruebas


En esta sección vamos a ver cómo podrı́amos diseñar casos de prueba para los programas de ejemplo
que se han hecho a lo largo del tema.
Ejemplo de casos de pruebas: sentencias condicionales. En el programa de ejemplo de la
Sección 4.4.2, en el que calculábamos el precio del billete en función del destino y la edad del viajero,
tenı́amos una tabla de valores en las que se especificaba el precio en cada caso. Aplicando el concepto de
la clases de equivalencia y los valores lı́mite, habrı́a que:

Probar al menos una vez cada uno de los casos de la tabla. Por ejemplo, para Coruña habrı́a que
probar: C 12, C 37 y C 80.

Probar además, para cada caso, los valores lı́mite de la edad (18 y 64). Siguiendo con Coruña: C
17, C 18, C 64 y C 65.

Probar valores incorrectos para la ciudad destino (una letra que no sea ni C, ni G, ni B, ni S).

Ejemplo de casos de prueba: tratamientos de secuencias. En los programas de tratamientos


de secuencias, los casos de prueba tienen que diseñarse teniendo en cuenta que el objetivo primordial
es comprobar que el bucle es correcto. Dependerán no solamente del problema sino también del tipo de
bucle que se usó.
En la Sección 4.7 resolvimos un programa para sumar los números pares entre 2 y un número entero
que se introducı́a por teclado. Usamos un bucle while, por tanto debemos probar que se hace cero
iteraciones, una iteración y más de una iteración. Resultando en los siguientes casos de prueba:

número=1 : el bucle no hace iteraciones, resultado 0

número=2 : el bucle hace una iteración, resultado 2

número=8 : el bucle hace varias iteraciones, resultado 20

Serı́a conveniente probar con valores impares. Por ejemplo, con 9 debe dar lo mismo que con 8
porque se suman los mismos pares (2+4+6+8).

En la Sección 4.9 describimos un programa para contar el número de dı́gitos que tenı́a un número
entero que se introducı́a por teclado. En este caso empleamos un bucle do-while, por tanto debemos
probar al menos que hace una iteración y más de una. Podrı́amos hacer los siguientes casos de prueba:

número=7 : el bucle hace una iteración, resultado: 1 dı́gito

número=65 : el bucle hace dos iteraciones, 2 dı́gitos

105
número=5341291 : el bucle hace varias iteraciones, 7 dı́gitos
Además, probar ciertos valores lı́mite: 0, 9, 10, 99, 100, etc.

Ejemplo de casos de prueba: búsquedas asociativas. En los programas que se basan en una
búsqueda asociativa lo fundamental es probar dos tipos de entradas:

Datos de entrada que hacen que NO haya ningún elemento que cumpla la propiedad buscada.
Datos para los que SÍ se encuentra lo que se busca.

En la Sección 4.10.3 se discute un programa para determinar si un número es primo o no. Para saber
si un número es primo lo que hacı́amos era buscar un divisor distinto de 1 y del propio número, es decir,
reducı́amos el problema a buscar un divisor. Hay que diseñar casos de prueba para ambas situaciones,
no encontrar un divisor o encontrarlo:

Divisor NO encontrado (número primo). Por ejemplo, bastarı́a con probar los primeros números
primos (2, 3, 5, 7) y luego algún otro mayor (p.e. 29, 83).
Divisor encontrado (número no primo). Probar alguno de los primeros números no primos (4, 6,
14) y luego algunos otros mayores, especialmente impares (p.e. 15, 27, 33).

La idea común en todos los casos es probar el bucle en todas las situaciones posibles diferentes. No
basta con hacer solamente un tipo de prueba, por ejemplo, tratamientos de secuencias en las que en todas
las pruebas la secuencia tiene un elemento, ni probar una búsqueda cuando siempre hay un elemento que
cumple la propiedad buscada. Hay que hacer casos de prueba para todas las situaciones posibles.
En los programas que realizaremos en los temas siguientes, introduciremos siempre al final los casos
de prueba que deberı́an hacerse. Como en nuestros programas siempre suponemos que la entrada de
datos será correcta, casi nunca se incluirán casos de prueba con datos de entrada incorrectos (salvo
en los métodos set() de una clase). Por ejemplo, si el programa necesita un número entero positivo,
nunca se probaran con valores negativos dado que la precondición del programa indica que el valor debe
ser positivo. Si permitiésemos la entrada de datos incorrectos, habrı́a que escribir más código en los
programas para detectar esas situaciones, y los casos de prueba deberı́an probar los trozos de código
añadidos para detectar si funcionan incorrectamente. No lo hacemos porque es algo bastante simple y
repetitivo, y harı́a que los programas fuesen más largos, lo que desviarı́a la atención de la parte esencial
del programa.

4.12. Ámbito y tiempo de vida de una variable


En el final del tema anterior estudiamos la representación en memoria de las variables y objetos,
tanto en la Pila como en el Montón (ver Sección 3.9). Para cerrar este tema vamos a explicar más
profundamente cómo se gestionan internamente y, desde la lógica de los programas, cuál es el ciclo de
vida de las variables. Para entenderlo hay que comprender dos conceptos: lo que es el ámbito de una
variable y su tiempo de vida. Una vez definidos, pondremos ejemplos para que se entienda más claramente
cómo y dónde se puede usar cada variable que se declare.

definición Ámbito: Conjunto de instrucciones en las que una variable puede ser usada.

Una variable no puede usarse en cualquier punto de un programa. Solamente se puede utilizar en su
ámbito. Por ejemplo, como es natural no se puede usar una variable antes de declararla. Pero no es ésa
la única limitación. Si se define un parámetro en un método, no puede usarse fuera de ese método. Es
decir, existen ciertas limitaciones que viene dadas por la semántica de los elementos del lenguaje.
Durante este tema, tanto en las sentencias condicionales como en las iterativas, hemos definido muchas
veces bloques de sentencias encerradas entre llaves. En el Tema 3 escribimos métodos cuyas sentencias
también iban entre llaves. En ambos casos se crean conjuntos de instrucciones delimitados. Esto tiene
una especial importancia a la hora de usar las variables que se van declarando en cada uno de esos trozos
de código. De una forma general, las reglas que controlan el ámbito de una variable son las siguientes:

106
Una variable solamente puede ser usada en el bloque de código en el que se declara, desde el punto
en el que se declara. Es decir, si se declara en un método, en cualquiera de las partes de una
sentencia if, o dentro de un bucle, la variable solamente podrá ser usada en ese bloque, desde el
punto en el que se declara, y nunca fuera.

Por ejemplo, la variable de control que se declara en un bucle for no se puede usar fuera del for.
Por ese motivo, si en un bucle for se necesita usar la variable después del bucle, se debe declarar
antes del for, en el bloque que englobe al for y a las sentencias siguientes en las que se desea usar
la variable.

Naturalmente, una variable declarada dentro de un bloque que tiene otros bloques anidados, sı́ se
puede usar en esos bloques anidados.

Por ejemplo, si se declara una variable al principio de un método, se puede usar en todos sus
bloques anidados, tanto en los de los if’s o en los de los bucles que contenga.

Vamos a verlo con un ejemplo de un método main():


1 public static void main(String [ ] args ) {
2 ...
3 int a;
4 if ( ... ) {
5 double b;
6 for (int i=...;...;...) {
7 for (int j=...;...;...) {
8 ...
9 } //fin for#2
10 ...
11 } //fin for#1
12 } //fin parte then
13 else {
14 ...
15 }
16 }

Cada una de las variables que aparece en este método main() tiene su propio ámbito, es decir, unas
instrucciones donde puede usarse. La primera regla es que las variables no pueden usarse antes de su
declaración. Por ejemplo, la variable a no puede emplearse en las sentencias anteriores a su declaración,
ni la b antes del if. Las únicas variables de un método que pueden usarse en todo el método son los
parámetros (args en el ejemplo) o las variables que se declaren al principio del todo. Esto no quiere decir
que deban declararse todas las variables al principio del método para que su ámbito sea el mayor. Todo
lo contrario, limitar el ámbito de las variables es la mejor forma de poder controlar con más facilidad los
posibles errores en los datos que contienen: la zona del programa que habrá que comprobar en caso de
un posible error será más pequeña, solamente habrá que revisar las sentencias de su ámbito.
4
! Hay que limitar todo lo que se pueda el ámbito de las variables, ası́ será más sencillo seguir
la lógica de los valores que van tomando y depurar los errores de los programas.
En el ejemplo, aplicando la regla comentada de que una variable solamente puede usarse en el bloque
en el que se declara y en los que contiene, tendrı́amos que la variable b podrı́a emplearse en el bloque
de la parte entonces del if, es decir, en ambos bucles for. La variable i, como está definida dentro del
primer for, solamente podrá usarse en ese for. Lo que ocurre es que ese primer for contiene al segundo,
luego la variable i también puede ser usada por las instrucciones del segundo for. No es el caso de la
variable j que, como está declarada dentro del segundo for, solamente puede emplearse allı́. Finalmente,
y dado que todas ellas, b, i y j, están declaradas en la parte entonces del if, ninguna podrá usarse en
la parte else ya que ése es un bloque de código distinto.
Resumiendo:

1. args: accesible en todo el método,

2. a: en todo el método, menos las sentencias anteriores a su declaración.

3. b: en la parte entonces del if.

107
4. i: en el primer for (que incluye al segundo).

5. j: solamente en el segundo for.

El ámbito de las variables, o lo que es lo mismo, cuándo y dónde pueden usarse, está ı́ntimamente
relacionado con su tiempo de vida. En realidad una variable no puede emplearse fuera de su ámbito
porque en ese momento de la ejecución no existe, bien porque no ha sido creada (sentencias anteriores a
su declaración), bien porque se ha liberado (sentencias fuera de su ámbito). Es decir, el tiempo de vida
de una variable es el que marca el ámbito de la misma.

definición Tiempo de vida de una variable: Perı́odo de tiempo en el que la variable existe, desde que se
crea y se reserva espacio en memoria para almacenarla, hasta que se destruye y se libera el espacio
que ocupa.

Luego para comprender mejor el ámbito de una variable necesitamos saber cómo y cuándo se crea y
cómo y cuándo se destruye. Las reglas que rigen la creación y liberación de una variable son simples:

1. Creación: cuando se declara. Se reserva espacio en la Pila de forma que la variable se guarda en la
cima de la Pila, apilada sobre todas las variables creadas anteriormente.

2. Liberación: cuando se acaba su ámbito. Se libera el espacio de la Pila que ocupaba la variable.

Como se podrá comprender con un ejemplo que pondremos a continuación, para el programador la
gestión interna de las variables en la Pila es un proceso transparente:

La gestión de la Pila NO la realiza el programador.

Sólo decide la creación de variables mediante su declaración.

Pero no se tiene que ocupar de la liberación, es automática.

Veámoslo con un ejemplo. Imaginemos el siguiente método f():


1 public int f (int x) { //Reservamos memoria para el parámetro x
2 int a; //Reservamos memoria para a
3 ...
4 for (int i=0;i<10;i++) { //Reservamos memoria para i
5 ...
6 } //i deja de tener validez, se libera el espacio que ocupa
7 ...
8 } //a y x dejan de tener validez, se libera su espacio

a a a

x x x x
... ... ... ... ...

al llamar a f() al declarar a al empezar el for al acabar el for al acabar f()

Figura 4.9: Ejemplo de creación y liberación de variables en la Pila

La Figura 4.9 muestra el estado de la Pila en los distintos estados por los que pasa en función de la
creación y liberación de variables locales del método f(). Las variables se van apilando en la cima de

108
la Pila a medida que se declaran. Primero se apila el parámetro x, luego la variable a, y por último la
variable de control del for, i. La liberación se produce justo en orden inverso, a medida que se acaba
el ámbito de cada variable. Ası́, primero se libera la variable i cuando el for se acaba y luego la a y el
parámetro x cuando el método finaliza. Es decir, toda variable se libera cuando se acaba su ámbito, de
ahı́ que no pueda seguir usándose fuera de su ámbito.
La siguiente pregunta que deberı́amos hacernos es ¿qué ocurre con los objetos y en general con las
variables referenciadas? ¿Funciona todo igual? Desde el punto de vista de la Pila, la respuesta es sı́:

Las variables de tipos referenciados (p.e. objetos) se liberan usando el mismo mecanismo, se elimi-
nan de la Pila al acabar su ámbito.
¿Pero qué pasa con el objeto al que apuntan? Esa memoria dinámica es liberada por el Recolector
de basura.

Se encarga de liberar la memoria del Montón de todos aquellos objetos que ya no están referenciados
por ninguna variable.

Es decir, la variable en sı́ funciona igual que si fuera de un tipo básico, se libera automáticamente. Y
el objeto al que apunta no se libera automáticamente, pero se hace también de forma transparente para
el programador.

Antes de liberarse f de la Pila Después de liberarse f de la Pila


Pila Montón Pila Montón

f día 31 día 31
mes 12 mes 12
c 2.0 c 2.0
año 2010 año 2010
i 1 i 1

Figura 4.10: Liberación de variables referenciadas en la Pila

Pensemos ahora en el ejemplo de la Figura 4.10. Tenemos tres objetos apilados en la Pila, una variable
entera i, una variable real c y un objeto de la clase Fecha f. La variable referenciada f está apuntando a
un objeto Fecha en el Montón. Cuando acaba el ámbito de f se desapila de la Pila liberándose su espacio.
En ese momento el objeto al que apuntaba sigue en memoria, no se libera. La liberación del objeto
se producirá cuando el Recolector de basura detecte que ese objeto no está referenciado por ninguna
variable, es decir, que ya no se está usando. Por tanto, la liberación no se produce de forma inmediata,
pero sı́ de forma transparente para el programador que no tiene que hacer nada para liberarla. En otros
lenguajes el programador debe liberar explı́citamente la memoria dinámica del Montón que ocupa su
programa mediante instrucciones definidas en el lenguaje. En el Tema 6 se discutirá más profundamente
todo el proceso de creación y liberación de objetos, haciendo especial hincapié en la creación de objetos
usando new, que aquı́ no hemos tocado.

109
Tema 5

Vectores
Objetivos

Saber emplear los vectores como estructura para la representación de una colección de datos.

Entender la representación y utilización de vectores multidimensionales (matrices).

Saber utilizar la clase String para el manejo de cadenas de caracteres.

Ser capaz de diseñar programas simples que manejen vectores, cadenas de caracteres y matrices.

5.1. Introducción
A poco que la complejidad de los programas empieza a aumentar, resulta evidente que las variables
simples de los tipos básicos que hemos utilizado en nuestros ejemplos hasta ahora no son suficientes para
representar toda la información que requieren dichos programas. Ni tampoco basta con los objetos de
las clases, que mirados desde el punto de vista de la representación de información permiten crear tipos
de datos más complejos, por ejemplo, podemos definir una clase para crear objetos que representen toda
la información de una persona: nombre, apellidos, DNI, etc. Necesitamos algo más:

Tanto una variable simple como un objeto de una clase nos permiten representar un elemento
de información, ya sea un entero, o un cı́rculo, o una fecha, si usamos las clases que creamos en
el Tema 3.

¿Qué pasa si un programa necesita representar cientos de esos elementos de información?¿Declaramos


tantas variables simples u objetos como datos tengamos? Obviamente eso no tiene mucho sentido.

Los programas necesitan representar colecciones grandes de datos, lo que invalida usar una va-
riable individual por cada dato.

Son necesarias estructuras para organizar esas colecciones de datos. El objetivo último es facilitar
el tratamiento del conjunto de datos dentro de los programas.

Ejemplos:

1. Almacenar la información sobre los clientes de una empresa.

2. Cotización del cambio Euro/Dólar todos los dı́as de un año.

Efectivamente, cuando queremos representar un conjunto más o menos grande de datos, ya sean
objetos de una clase como en el primer ejemplo, o una colección de datos de un tipo básico como en el
segundo caso, es necesario emplear una estructura de datos.

111
5.1.1. Estructuras de datos
El objetivo básico de las estructuras de datos es permitir el tratamiento de un conjunto de datos, rela-
cionados de algún modo entre sı́, a través de una única variable, y además lograr que dicho procesamiento
resulte eficiente. Una posible definición de las estructuras de datos podrı́a ser la siguiente:

definición Estructura de datos: Forma de organizar un conjunto de datos en la memoria con el objetivo de
facilitar su tratamiento.

La Figura 5.1 muestra un ejemplo de tres estructuras de datos distintas: un vector, un árbol binario
y una lista. Las tres guardan la misma colección de datos enteros ordenados. Es importante observar
que aunque todas ellas guardan los mismos datos, la forma de representarlos es muy diferente. Eso
condicionará la eficiencia de sus operaciones.

1 3 4 5 6 8 9 5

3 8

1 4 6 9

1 3 4 5 6 8 9

Figura 5.1: Ejemplo de tres estructuras de datos. Arriba a la izquierda, un vector, a la derecha, un árbol
binario, y abajo una lista.

Volviendo a la discusión inicialmente planteada, tenemos la necesidad de guardar un grupo de datos


con los que va a trabajar nuestro programa y existen distintas posibilidades a la hora de decidir cómo
guardarlos en la memoria. Cada una de esas formas diferentes de almacenar los datos es lo que se denomina
una estructura de datos. Hay bastantes estructuras de datos distintas, además de las representadas en la
Figura 5.1. Cada una tiene unas caracterı́sticas definidas que implicarán ventajas y desventajas para su
uso en un programa en concreto. La labor del programador es escoger la estructura de datos más adecuada
de acuerdo con las necesidades de la aplicación que está realizando. La elección depende principalmente
del tipo de las operaciones más frecuentes que necesite hacer el programa. Las operaciones tı́picas que
los programas realizan sobre las estructuras son:

1. Leer/Modificar: obtener o cambiar el valor de uno de los elementos de la estructura.

2. Insertar/Borrar: añadir un nuevo elemento o eliminar de la estructura un elemento ya existente.

3. Buscar: encontrar un determinado elemento.

4. Ordenar: todos los elementos de acuerdo a un criterio.

5. Mezclar: dadas dos estructuras, combinar sus elementos ordenadamente para crear una nueva.

Cada estructura ofrece ventajas y desventajas en relación a la simplicidad y eficiencia para la rea-
lización de esas operaciones. Por ejemplo, las listas son muy eficientes a la hora de añadir o borrar

112
elementos; los vectores son los mejores a la hora de acceder a los datos, bien para obtener su valor o
para modificarlo; los árboles son ideales para buscar un determinado elemento dentro del conjunto. Y al
contrario, las listas son ineficientes a la hora de acceder a los elementos, los vectores son la peor opción
si necesitamos añadir o borrar datos muchas veces, y los árboles también pueden resultar ineficientes a
la hora de insertar o borrar elementos. Es decir, cada estructura es mejor para un tipo de operaciones
que para otras. Por ello, la elección de la estructura de datos apropiada para cada programa depende de
la frecuencia con que se realiza cada operación sobre los datos. Se escogerá la estructura más eficiente
para las operaciones que se realicen más veces en el programa.
En este tema vamos a presentar la estructura de datos más simple que existe. Son los vectores. A
pesar de su simplicidad, o quizá por ello, es la estructura de datos que más se emplea en Programación,
de hecho todos los lenguajes incluyen en su sintaxis la forma de crear y manipular vectores. El resto
de estructuras no son elementos “de serie” de los lenguajes, y normalmente se incluyen en librerı́as o
paquetes de clases adicionales pero sin formar parte de la sintaxis básica del lenguaje. En el segundo
curso hay una asignatura dedicada al estudio de otras estructuras de datos más complejas, como las listas
o los árboles antes citados. Aquı́ nos centraremos en estudiar las caracterı́sticas básicas de los vectores
y, sobre todo, en analizar los algoritmos más habituales que se suelen hacer con ellos.

5.2. Vectores
5.2.1. ¿Qué es un vector?
Antes de definir qué son los vectores como sugiere el tı́tulo de esta sección, vamos a plantear un
sencillo ejemplo para demostrar cómo surge la necesidad de utilizarlos en los programas.
enun- Realizar un programa que permita al usuario introducir las temperaturas registradas durante las 24 horas
ciado del dı́a, desde las 0 horas hasta las 23 horas, y calcule diversos parámetros como la temperatura máxima
y media de ese dı́a.
¿Qué es nuevo en este programa?

Necesitamos representar 24 valores distintos, la temperatura de cada hora.

¿Serı́a lógico usar 24 variables diferentes?

Obviamente NO, serı́a muy complicado hacer ciertas operaciones con todas ellas, por ejemplo
calcular el máximo. Si recordamos el programa para calcular el máximo de tres números (ver
Sección 4.4.2), necesitamos de un par de sentencias condicionales anidadas para realizarlo. ¿Cuántas
sentencias if necesitarı́amos para buscar el máximo entre esos 24 valores individuales? Serı́an
muchı́simas, y todo para hacer una operación que será relativamente sencilla si, en lugar de variables
simples, usamos una estructura de datos que nos permita tratarlos de forma conjunta.

Necesitamos guardar esos datos en una estructura más compleja que las variables simples, de
manera que nos facilite realizar tratamientos secuenciales de todos esos datos. Es decir, lo que
queremos es guardar los datos en memoria de tal forma que podamos aplicar fácil y eficientemente
los algoritmos de tratamiento secuencial estudiados en la Sección 4.10. Usaremos una única variable,
un vector.

Analizando la necesidad que estamos describiendo, no es difı́cil llegar a la idea intuitiva de lo que
puede ser un vector. Básicamente lo que queremos es poder guardar varios datos en lugar de uno solo,
que es lo que logramos al declarar una variable simple. Es decir, queremos variables que guarden varios
datos. Eso cuadra perfectamente con la definición de vector en matemáticas:

definición Vector (Matemáticas): Un vector de dimension n es una tupla de n números reales.

Geométricamente, un vector representa un punto en un espacio n-dimensional, donde cada uno de los
elementos de esa tupla es el valor de ese punto para una de las dimensiones. Los espacios n-dimensionales
más naturales, al poder representarse gráficamente, son el espacio de dos dimensiones (2D) y el de tres
(3D), es decir, R2 y R3 respectivamente. La Figura 5.2 muestra un ejemplo de un vector en R3 . El punto,

113
z

(5,3,4)

Figura 5.2: Representación geométrica de un vector en un espacio de tres dimensiones

(5,3,4), que representa ese vector tiene el valor 5 para la dimensión x, el valor 3 para la dimensión y, y
el valor 4 para la dimensión z. Un valor por cada dimensión del espacio.
Igual que somos capaces de representar mediante un vector cualquier punto en R3 , podemos también
trabajar con un punto de más dimensiones, Rn en general, aunque luego no seamos capaces de dibujarlo
como hemos hecho con el ejemplo en 3D. Un vector nos permitirá representar un punto de cualquier
espacio, por ejemplo, el punto (5,3,4,9,0,7) de R6 . En nuestro ejemplo de las temperaturas diarias, el
espacio en el que se representan las temperaturas de las 24 horas del dı́as será R24 . La primera dimensión
representará la temperatura a las 0 horas, la segunda a la 1, y ası́ sucesivamente hasta la vigésima cuarta
dimensión que representará la temperatura a las 23 horas.
Los vectores en Programación son una extensión de los vectores en Matemáticas de manera que no
solamente se puedan representar puntos en el espacio de los números reales, sino en cualquier tipo de
espacio, no necesariamente numérico.

definición Vector (Programación): Un vector de tamaño n es una tupla de n variables (llamadas elementos
o componentes) del mismo tipo.

5 3 4 9 0 7

Figura 5.3: Representación conceptual de un vector en Programación

Los vectores en Programación pueden guardar elementos de cualquier tipo de dato. Eso incluye,
naturalmente, objetos de cualquier clase que definamos. En la Figura 5.3 hemos representado un
vector de enteros para extender el ejemplo de la Figura 5.2, pero perfectamente podrı́amos haber
dibujado un vector de objetos Fecha.
Los elementos tienen que ser todos del mismo tipo. No puede ocurrir que un elemento sea entero,
otro un valor booleano y el siguiente un real. Si declaramos un vector de objetos de la clase Cı́rculo,

114
todos los elementos de ese vector serán objetos Cı́rculo, no podrá haber un objeto Fecha. Para
asegurar las propiedades diferenciadoras de los vectores como estructura de datos, ver Sección 5.5,
es obligatorio que las componentes sean del mismo tipo.

Además, los elementos tienen que estar relacionados entre sı́. Es decir, no se trata de agrupar
variables simples sin relación.

Volviendo al ejemplo de las temperaturas diarias. El vector que usaremos guardará las temperaturas
de las 24 horas de un mismo dı́a. Todos los datos son del mismo tipo (son todos enteros, temperaturas)
y además están relacionados entre sı́ (son del mismo dı́a). Esa es la clase de relación que suele darse
entre las componentes de un vector. Si no hay relación entre los datos, no debe declararse un vector. Por
ejemplo, en el programa para sumar dos números enteros, no tendrı́a ningún sentido declarar un vector
de tres componentes, dos para los números leı́dos y la tercera para su suma. Por poder, podrı́a hacerse,
pero en general se utilizan los vectores cuando los datos tienen una relación entre ellos y al agruparlos
hacemos su tratamiento más sencillo. En el programa de la suma, si usáramos un vector para guardar
todos los datos no se obtendrı́a ninguna ventaja ni en términos de eficiencia, ni de claridad del programa,
antes al contrario.

5.2.2. Declaración, creación e inicialización de un vector


Ahora que se ha descrito qué es un vector, vamos a presentar cómo se declaran, se crean y se utilizan.

sintaxis Declaración y creación de un vector:


tipo[ ] nombre [= new tipo [tamaño] ];

Varios detalles importantes:

El tipo de un vector es tipo[ ], los [ ] representan vector. Por ejemplo, int[ ] es el tipo “vector
de enteros”. Como se puede apreciar, es como si leyéramos la declaración del tipo de derecha a
izquierda: vector ([ ]) de enteros (int). Y como siempre, el tipo puede ser cualquiera, es decir, se
pueden crear vectores con valores de un tipo básico o con objetos de una clase.

Los vectores son objetos: una cosa es la creación de la variable y otra la creación del objeto vector
usando new. Es decir, como los vectores son objetos, por tanto tipos referenciados, su forma de
declararlos, crearlos y representarlos en memoria siguen el mismo funcionamiento que los objetos
de una clase. Aunque un poco más adelante estudiaremos la representación en memoria de un
vector, es aplicable todo lo que se explicó sobre la representación de objetos y su creación con el
operador new en la Sección 3.9.2.

Al crear el objeto vector usando new se debe indicar el tipo de las componentes y su tamaño o
número de elementos. El tamaño debe ser una expresión int entre corchetes. En la sintaxis descrita
para la creación con new, los primeros corchetes indican que es opcional hacerlo, es decir, no es
obligatorio, además de declararlo, crear el objeto y reservar espacio en memoria para el vector. Los
corchetes del tamaño no son opcionales, hay que ponerlos obligatoriamente (por eso están escritos
en negrita).

Las componentes en un vector de un tipo básico se inicializan a 0 (false para boolean) y si son
objetos de una clase a null. Esto es, siguen exactamente el mismo comportamiento que cuando se
inicializan por defecto los atributos de un objeto (ver Sección 3.5).

Veamos algunos ejemplos de declaración y creación de vectores:


1 int[ ] v = new int [6]; //Vector de 6 enteros
2 double[ ] t; //Declaración vector de reales
3 t= new double [24]; //Creación vector de 24 reales
4 Fecha[ ] f= new Fecha [365]; //Vector de 365 objetos Fecha

115
En el primer ejemplo se declara un vector de tipo int y se inicializa con un objeto vector de 6
elementos. Es importante darse cuenta cómo el tipo int se pone justo detrás de new para indicar que
las nuevas componentes del vector que se está creando son de tipo entero. En el ejemplo de la lı́nea 2
declaramos simplemente un vector llamado t de tipo double, pero no lo asociamos con un nuevo objeto
vector. Es decir, como es un tipo referenciado, en ese momento t valdrá null, ya que no se ha creado el
vector todavı́a. Es en la tercera lı́nea cuando se reserva espacio para el vector de componentes double,
concretamente 24 elementos. En el último ejemplo se declara un vector de objetos Fecha y se reserva
espacio para almacenar 365 fechas.
Como puede apreciarse, en todos los casos se reserva espacio para los vectores usando el operador
new e indicándole dos cosas: el tipo que tendrás las componentes del vector (tiene que coincidir con el
utilizado en la declaración del vector), y el número de componentes, es decir, su tamaño (entre corchetes).
4
! El tamaño de un vector se indica entre corchetes, no entre paréntesis.
Otra opción, a la hora de declarar un vector y crear el objeto vector con el espacio necesario, es usar
una lista con los valores iniciales de sus componentes. La sintaxis es la siguiente:

sintaxis Declaración, creación e inicialización de un vector:


tipo[ ] nombre [= { lista de valores }];

Aspectos a tener en cuenta:

La lista de valores será una lista de expresiones del mismo tipo del vector separadas por comas y
encerradas entre llaves. Es decir, si por ejemplo queremos dar valores iniciales a un vector de int,
tiene que ser una lista de expresiones de tipo int, entre llaves y separadas por comas.

Automáticamente se reserva espacio en memoria para tantos elementos como valores contenga la
lista de valores y el vector se referirá a ese espacio. Como se ha indicando anteriormente, los vectores
son objetos, elementos referenciados, lo que guarda una variable vector es una referencia a la zona
donde está el objeto vector en memoria.

Ejemplos:
1 int[ ] v = { 5, 3, 4, 9, 0 ,7}; //Vector 6 enteros
2 char[ ] vocales= {’a’,’e’,’i’,’o’,’u’}; //Vector 5 vocales
3 Fecha[ ] f= {new Fecha(), new Fecha()}; //Vector 2 Fechas

En el primer ejemplo se crea el vector de enteros de las Figuras 5.3, 5.4 y 5.5, con los seis valores
separados por comas. En el ejemplo de la lı́nea 2 creamos un vector de caracteres inicializado con las
cinco vocales del alfabeto español. En este caso, dado que el vector es de tipo char, las expresiones usadas
en la lista tienen que ser de tipo char, y lo son, son constantes char ya que van entre comillas simples.
En el último de los ejemplos el vector se inicializa con dos objetos de la clase Fecha creados por defecto.
Debe observarse como en ninguno de los ejemplos hemos indicado el tamaño que debe tener el vector.
El espacio necesario se calcula en función del número de expresiones que tiene la lista con la que lo
inicializamos.
4
! El tamaño del vector será el mismo que el tamaño de la lista de valores indicada.
Por último, vamos a mostrar otra forma de declarar un vector. Es una sintaxis que puede resultar un
poco confusa y que no usaremos en los programas de la asignatura. En cualquier caso, dado que existe
en el lenguaje y por tanto puede aparecer en cualquier programa Java, es conveniente conocerla. Es la
siguiente:

sintaxis Otra forma de declarar un vector:


tipo nombre[ ] [= new tipo [tamaño] ];
tipo nombre[ ] [= { lista de valores }];

116
Es decir:

Los corchetes se pueden situar después del nombre del vector. En las dos sintaxis mostradas pre-
viamente, los corchetes para indicar que lo que se declaraba iba a ser un vector se ponı́an justo
detrás del tipo de las componentes. También pueden ponerse después del nombre.

Es útil para declarar a la vez variables simples y vectores. Usando la sintaxis indicada, la declaración
en esos casos se puede hacer en una sola lı́nea como en el siguiente ejemplo:
int a=5, b[ ] = new int [4], c=7;

Las variables a y c son de tipo int, que es el tipo que está indicado al principio de la declaración de
las tres variables. Para indicar que b va a ser un vector en lugar de una variable simple, se ponen
los corchetes después del identificador (b[ ]). Aunque esto es sintácticamente correcto, preferimos
poner las declaraciones en lı́neas separadas, añadir un comentario apropiado y usar la sintaxis
inicialmente descrita usando como tipo tipo[ ].

Si se quieren declarar varios vectores habrı́a que poner los corchetes después de cada nombre.
double d[ ]= new double[10], e[ ]= { 3.3 , 4.5 , 5.7};

En este caso se crean dos vectores double, el primero de ellos (d) de 10 elementos, y el segundo (e)
además inicializa sus tres componentes. De nuevo aunque sea sintácticamente correcto, nos parece
más apropiado hacerlo usando la sintaxis previa:
double[ ] d= new double[10]; //Vector d para ...
double[ ] e= { 3.3 , 4.5 , 5.7}; //Vector e para ...

ya que en ella se indica desde el principio que la declaración que sigue va a ser de un vector.

4
! Es preferible declarar cada vector en un declaración y usar la sintaxis tipo[ ].
La preferencia por una sintaxis u otra a la hora de declarar vectores en Java es una cuestión de gusto
personal de cada programador. Sin embargo, nosotros preferimos la primera de ellas ya que indica desde
el principio que la declaración es de un vector y no hace falta repetir los corchetes si se declaran varios
vectores.

5.3. Representación de un vector en memoria


Antes de explicar cómo se usan los vectores en un programa, vamos a detenernos un momento en
mostrar cómo se representan en memoria. Esto ayuda a entender mejor, tanto algunos detalles de su
uso, por ejemplo por qué la primera componente de un vector es la componente 0, como algunas de sus
caracterı́sticas principales como estructuras de datos, especialmente por qué el tiempo de acceso a las
componentes de un vector es constante.
Lo más importante que se debe comprender es que los vectores son objetos (tipos referenciados),
lo cual implica dos cosas de cara a su representación:

1. Por un lado está la variable que representa todo el vector y que se guarda en la Pila. Se reserva
cuando declaramos la variable. El vector en su conjunto es la variable que declaramos y se almacena
en la Pila siguiendo el funcionamiento descrito en la Sección 3.9 cuando comentamos la represen-
tación de los objetos de las clases. Guardará una referencia al objeto vector con las componentes
propiamente dichas.

2. Por otro lado está el objeto vector con sus componentes y se guarda en el Montón. Se reserva al
hacer new o al inicializar el vector con una lista de valores. Si no hacemos una de las dos cosas,
las componentes del vector no se crearán, del mismo modo que nos ocurrı́a con los objetos si no
hacı́amos un new. El funcionamiento de todos los tipos referenciados es el mismo en ese sentido, da
igual que sean objetos de una clase o vectores.

117
Pila Montón

v 5 3 4 9 0 7
...

Figura 5.4: Representación en memoria de un vector

La Figura 5.4 muestra la representación en memoria del vector de enteros v declarado en los ejemplos
de la sección anterior. La variable v al declararla se guardará en la cima de la Pila, encima de todas las
variables declaradas con anterioridad, y quedará debajo de las variables que se declaren a continuación.
Si solamente declarásemos la variable v, sin hacer un new ni inicializarla, v contendrı́a el valor null,
indicando que todavı́a no se refiere a ningún objeto vector en memoria. Para crear las componentes del
vector es necesario, o bien hacer un new indicando el número de enteros que se quieren reservar para el
vector, o bien inicializar el vector en la misma declaración con una lista de valores enteros. Ambos casos
están contemplados en los ejemplos de la sección previa. En la figura, el objeto vector creado tiene 6
componentes enteras.
El otro aspecto clave en la representación de los vectores en memoria es que las componentes del
vector se guardan en posiciones consecutivas de la memoria. Es decir,

No puede haber otras variables entre medias, tienen que estar todas las componentes seguidas. En
el caso de otras estructuras de datos, como las listas y los árboles mostrados con anterioridad, es
habitual que cada elemento de información, representado con una caja en la Figura 5.1, pueda
estar en un zona de la memoria no necesariamente pegada al resto de elementos que almacena la
estructura. En los vectores esto no es ası́.

Es clave que los elementos estén consecutivos porque es la manera de agilizar los accesos a esos
datos. Sin esa propiedad, sencillamente un vector no serı́a un vector.

Dado que todas las componentes son del mismo tipo, es decir, ocupan el mismo espacio en memoria1 ,
y además sabemos dónde empieza el vector (es la referencia que guarda la variable del vector en la
Pila), se puede determinar la posición exacta de cada dato en la memoria.

El hecho de que los elementos de un vector estén seguidos en la memoria también tiene implicaciones
en cuanto a la gestión de memoria por parte del Gestor de Memoria.
4
! El espacio necesario para un objeto vector solo podrá reservarse si hay suficiente memoria
consecutiva libre.

Efectivamente, cuando se hace un new y se indica un cierto número de elementos, el Gestor de Memoria
debe encontrar un bloque dentro del Montón en el que pueda situar todas las componentes de forma
consecutiva. El espacio de memoria necesario para el objeto vector se obtiene multiplicando el número
de componentes, por el número de bytes que ocupa en memoria cada una de ellas, lo cuál depende de su
tipo de dato. A veces puede ocurrir que aunque un vector necesite menos espacio que la memoria libre
disponible, el Gestor de Memoria no puede encontrar suficiente memoria libre consecutiva. En ese caso
el objeto vector no se creará ya que no hay un hueco en la memoria lo bastante grande2 .

1 Debe recordarse que una de las caracterı́sticas que define un tipo de dato es el espacio que ocupa cada uno de sus datos
en memoria, lo que además determina el rango de valores posibles que puede tomar una variable de ese tipo.
2 Naturalmente, esto puede ocurrir cuando se reservan vectores muy grandes, para vectores de unas pocas componentes

como los que manejaremos en nuestros programas lo normal es que no haya problemas para que el gestor encuentre un
bloque de memoria consecutiva libre.

118
5.4. Uso de un vector
En esta sección vamos a dedicarnos a estudiar los distintos elementos que se requieren para usar un
vector. Solamente son dos: el operador corchete ([ ]) para acceder a las componentes y el atributo length
para saber el tamaño de un vector dado. Finalizaremos la sección exponiendo un programa para guardar
las temperaturas diarias que servirá para mostrar cómo se emplean ambos elementos.

5.4.1. Operador corchete [ ]


Si se entiende correctamente lo expuesto en la sección anterior y la representación de los vectores
como tipos referenciados, es más sencillo comprender que el nombre del vector sirve para referirse al
vector en su conjunto. Hay varias operaciones para las que necesitamos referirnos a todo el vector, por
ejemplo cuando queremos asignar un vector a otro vector, siendo ambos del mismo tipo. Dado que son
variables referenciadas, las asignaciones presentan las mismas particularidades que las asignaciones entre
objetos, véase la Sección 3.9.2. Usar el vector de esa manera no plantea ninguna novedad, lo novedoso
es cómo podemos acceder a las componentes individuales.
Para acceder a las componentes individuales de un vector se usa el operador corchete ([ ]). Tiene dos
operandos:

1. el nombre del vector, y

2. la posición de la componente a la que queremos acceder.

La sintaxis concreta del operador [ ] es:

sintaxis Operador [ ]:

vector[ı́ndice]

Debe tenerse en cuenta que:

El ı́ndice debe ser una expresión de tipo int, con un valor no negativo. Obviamente también
será válida cualquier expresión de un tipo que pueda convertirse automáticamente a int, es decir,
byte, short o char, pero no valdrı́a que fuera de tipo long, float, double o boolean ya que en esos
casos no se produce la conversión automática a int (ver Sección 2.11.2).

Si el ı́ndice está fuera del rango del vector se produce un error en tiempo de ejecución. Por ejemplo,
si un vector tiene 10 componentes y se trata de acceder a la componente 20, obviamente el acceso
está fuera del vector y se producirá la excepción ArrayIndexOutOfBoundsException que si no se
trata hace que el programa se detenga. El tratamiento de excepciones se estudiará en la asignatura
Metodologı́a de la Programación del segundo cuatrimestre.

Es un left-value, es decir, se puede colocar a la izquierda en una asignación. A pesar de tratarse


de una expresión con operadores, lo que devuelve es una referencia a la componente del vector, por
lo que se puede usar en una asignación para cambiar el valor de esa componente de un vector.

Lo que resulta más extraño a los programadores noveles es cómo se numeran las componentes del
vector. Lo más natural para todo el mundo es empezar a contar por 1. Parecerı́a lógico pensar que el
ı́ndice del primer elemento fuera 1 y el del último n, siendo n el número de componentes del vector. Sin
embargo no es ası́.
En un vector de n elementos:

La primera componentes tiene ı́ndice 0, y

por tanto, la última tendrá ı́ndice n − 1.

119
v[0] v[1] v[2] v[3] v[4] v[5]

v 5 3 4 9 0 7
...

Figura 5.5: Acceso a las componentes de un vector con el operador corchete

Por ejemplo, en la Figura 5.5 aparece la representación del vector v de 6 elementos. La primera
componente es la 0 (v[0]), y la última será la 5 (v[5]).
Todo esto tiene un objetivo, hacer los accesos a las componentes más rápidos. Como comenta-
mos cuando definimos lo que era una variable (Sección 2.1), el nombre de una variable sirve en realidad
para referirnos simbólicamente a una dirección en la memoria. Del mismo modo, para acceder a una
componente concreta de un vector tenemos que saber en qué dirección de la memoria se encuentra. La
variable vector, si se observan con detalle las figuras anteriores, se refiere a la posición de la memoria
donde se encuentra la primera componente del vector. Eso nos podrı́a servir para acceder a esa primera
componente, pero ¿cómo accedemos al resto? La clave está en dos factores: a) las componentes están
seguidas en la memoria, y b) ocupan el mismo espacio (ya que son del mismo tipo). Luego se puede
calcular de forma sencilla la posición de cualquier componente usando esos tres datos: i) la dirección
inicial del vector, ii) el número de bytes que ocupa cada componente, y iii) la posición del elemento al
que se quiere acceder.
La expresión que permite calcular la posición de cada componente del vector es la siguiente:

dir. elemento i-ésimo = principio vector + i ∗ no bytes tipo


donde i representa el ı́ndice de la componente. El segundo término de esa ecuación sirve para calcular
cuántos bytes está desplazada la componente a la que se quiere acceder respecto del principio del vector.
Para eso es fundamental que la primera componente tenga ı́ndice 0, de esa forma el segundo término de
la ecuación anterior dará cero, reflejando que la primera componente está desplazada 0 bytes respecto del
principio del vector. Si se hubiera establecido el convenio de numerar la primera componente como 1, en
cada acceso vec[ı́ndice] habrı́a que restarle 1 al ı́ndice i para calcular su posición. Empezando a numerar
en 0 nos ahorramos esa resta en todos los accesos. Y eso es muchı́simo tiempo de cálculo ahorrado, ya
que los programas realizan muchı́simos accesos a componentes de vectores.
Por ejemplo, supongamos que tenemos un objeto vector de elementos int que está situado en la direc-
ción 0x3000, expresada en hexadecimal como se indican las direcciones. La variable vector contendrá esa
dirección, refiriéndose al objeto vector. Es evidente que la componente 0 estará en la dirección 0x3000,
justo al principio del vector. La siguiente componente, la 1, estará en la dirección 0x3004 ya que los int
ocupan 4 bytes y las componentes del vector están seguidas en memoria. La expresión anterior funciona:
0x3000 + 1 ∗ 4 = 0x3004. Y la componente 2 obviamente estará en la dirección 0x3008 = 0x3000 + 2 ∗ 4,
y ası́ sucesivamente con el resto de elementos del vector. Naturalmente, la ecuación también funciona
para la componente 0, 0x3000 + 0 ∗ 4 = 0x3000, que es la dirección inicial del objeto vector.

5.4.2. El atributo length


El otro elemento que necesitamos para trabajar con los vectores es saber el número de elementos que
tienen. Antes hemos dicho que en un vector de n componentes la primera es la 0 y la última la n − 1. Si
nos dan un vector cualquiera, por ejemplo en un método de una clase, y queremos acceder a su último
elemento, ¿cómo sabemos cuántas componentes tiene? La respuesta está en el atributo length.
Hemos dicho que los vectores eran objetos, luego tienen atributos y métodos como los objetos de
cualquier clase. El atributo más importante es el que nos permite conocer el tamaño de un vector:

El atributo length guarda el tamaño del vector.

120
v[0] ... v[v.length-1]

v 5 3 4 9 0 7
...

Figura 5.6: Acceso a las componentes usando el atributo length

Es una constante pública (public y final), es decir, es accesible desde fuera de la clase y no cambia
de valor. Se puede pensar que esto contradice lo que dijimos en el Tema 3, que los atributos eran
siempre privados. Las constantes de las clases sı́ suelen ser públicas, por ejemplo las constantes PI
ó E de la clase Math. El motivo fundamental para poder hacer las constantes públicas es que aunque
un programador quisiera cambiarlas inapropiadamente no podrı́a. Por eso no importa que length
sea público, ya que es constante.

Se le da valor cuando se crea el vector. Ya sea con new o inicializándolo con una lista de valores.
Desde ese momento y hasta que se libere, el vector no cambiará de tamaño y por tanto tampoco
hace falta cambiar el valor del atributo length.

Usando el operador corchete y el atributo length podremos recorrer todas las componentes de cual-
quier vector. Sabremos que la primera componente tiene ı́ndice 0 y la última el valor que tenga el atributo
length menos 1 (por empezar en 0). Véase la Figura 5.6.

4
! Los vectores nunca cambian de tamaño, por eso el atributo length es constante.
Este es un detalle muy importante que debe tenerse siempre en mente. Los vectores no cambian de
tamaño, no se puede añadir una componente o eliminarla. No hay métodos que lo permitan. La única
forma de realizar esas operaciones es creando un nuevo objeto vector con new, con una componente más
si lo queremos es añadir un nuevo elemento, o con una menos si lo que necesitamos es borrarla. Pero
ambas operaciones debe realizarlas el programador, ya que no hay métodos que las realicen. Hay algunas
clases en las librerı́as de Java que permiten representar vectores más sofisticados, y que incluyen métodos
para realizar esas operaciones. Como el objetivo de esta asignatura es aprender los fundamentos de la
Programación preferimos trabajar con los vectores estándar, que son los tı́picos que aparecen en cualquier
lenguaje de programación. El alumno puede investigar por su cuenta las clases de los paquetes de Java
que permiten representar vectores con esas funcionalidades adicionales, pero eso está fuera del ámbito
de la asignatura.

5.4.3. Clase Temperaturas


Con todo lo que hemos visto ya sobre vectores estamos en disposición de hacer casi cualquier programa
que necesite uno. Vamos a empezar por el problema que habı́amos planteado sobre las temperaturas
diarias. Lo primero que debemos hacer es el análisis de esa clase, qué atributos y métodos necesita:

Atributos:

1. Un vector con temperaturas (enteros)

Métodos:

1. Calcular la temperatura máxima() y media() (enunciado).


2. Métodos set() y get() para poder cambiar las componentes del vector.

Este análisis inicial nos permite dar nuestra especificación de la clase Temperaturas, su representación
en UML está en la Figura 5.7. Hemos tomado varias decisiones. Primero representar las temperaturas

121
con enteros, en segundo lugar, los métodos setGrados() y getGrados() creados permiten la manipulación
de una de las componentes, por eso ambos tienen un parámetro entero para indicarles el valor de la com-
ponente que queremos cambiar u obtener. Será el ı́ndice que utilizaremos para acceder a la componente.
La otra opción serı́a que trabajaran con todo el vector, bien para devolverlo o para modificarlo. Hemos
optado por la primera opción. En algunos programas de las siguientes secciones veremos métodos que
manipulan y devuelven vectores completos.

Temperaturas
- grados: int[]

+ setGrados(int, int)
+ getGrados(int): int
+ Media(): float
+ Máxima(): int

Figura 5.7: UML - Clase Temperaturas

Dada esa especificación, ya podemos programar la clase Temperaturas:

Ejemplo1Temperaturas/Temperaturas.java
1 /** Representa objetos con las temperaturas de las 24 horas de un dı́a
2 * @author los profesores de IP
3 * @version 1.0 */
4 public class Temperaturas {
5 /**Valor de la temperatura de cada hora del dı́a*/
6 private int[ ] grados = new int[24];

8 /**Fija la temperatura de una hora del dı́a


9 * @param h indica la hora que se va a fijar
10 * @param t indica el valor de la temperatura*/
11 public void setGrados(int h, int t) {
12 if ( h>=0 && h<=23) grados[h]=t;
13 }

15 /**Devuelve el valor de la temperatura de una hora


16 * @param h indica la hora que se quiere obtener
17 * @return el valor de temperatura de esa hora */
18 public int getGrados(int h) {
19 return grados[h];
20 }

22 /**Calcula la temperatura media del dı́a


23 * @return la temperatura media*/
24 public double media() {
25 int suma=0;
26 for (int hora=0; hora<grados.length; hora++)
27 suma+=grados[hora];
28 return (double)suma/24;
29 }

31 /**Calcula la temperatura máxima del dı́a


32 * @return la temperatura máxima*/
33 public int máxima() {
34 int max=grados[0];
35 for (int hora=1; hora<grados.length; hora++)
36 if (grados[hora]>max) max=grados[hora];
37 return max;
38 }
49 }

122
Como en toda clase, es importante comenzar representando los atributos. En este caso hemos decla-
rado un vector de enteros privado que hemos llamado grados, dado que va a guardar temperaturas. En
la lı́nea 5 le damos como valor inicial un objeto vector creado con new con 24 elementos enteros. Esa
declaración implica que todos los objetos de la clase Temperaturas que se declaren, crearán inicialmente
para el atributo grados un vector de enteros de 24 componentes. En el Tema 6, cuando discutamos con
precisión cómo se produce la creación de los objetos de una clase, se entenderá mejor por qué cada objeto
de la clase tendrá asociado un nuevo objeto vector.
Lo siguiente es programar los métodos set() y get() necesarios para la clase. Como hemos comentado
previamente, hemos decidido hacer esos métodos de manera que afecten a las temperaturas individuales.
Esta es una de las decisiones de diseño que debe tomar el programador que crea una nueva clase. La
otra opción hubiera sido que dichos métodos sirvieran para obtener o cambiar, respectivamente, todo el
vector de temperaturas.
Como ambos necesitan saber sobre la temperatura de qué hora deben trabajar, los dos reciben como
primer parámetro (único en el caso de getGrados()) el valor de dicha hora. El método setGrados()
recibe además la nueva temperatura que se debe asignar a la hora recibida como primer parámetro.
Como es habitual en los métodos set() de las clases, antes de cambiar el valor del atributo se hace una
comprobación para verificar que la hora que se pasa a través del parámetro h es correcta, esto es, que
está entre 0 y 23 (forma de representar las horas según el enunciado). Si el valor de h es correcto entonces
se procede a cambiar el valor de la temperatura de esa hora dentro del vector de acuerdo con lo que valga
el segundo parámetro t. En este problema la representación entre horas y posiciones del vector grados
es directa, ya que las horas se representan también en el rango [0..23], que son justamente los ı́ndices de
las componentes de un vector de 24 elementos. Luego la hora h está precisamente en la posición h del
vector grados, es decir, usando el operador corchete, grados[h].
El método getGrados() es obviamente simétrico, esto es, devuelve el valor de la componente h-ésima
de acuerdo con el valor de dicho parámetro. En este método no hemos incluido la misma sentencia
condicional que en setGrados(), ya que aunque podrı́amos hacerlo, en la parte else el método no podrı́a
devolver ningún valor lo cuál es marcado como error por entornos como Eclipse. En realidad, la forma
de tratar correctamente los errores en las clases es usando excepciones, y dado que no forman parte de
los objetivos de la asignatura (pertenecen a la del segundo cuatrimestre), algunas veces como en este
caso, no incluimos ningún código para evitarlos y dejamos la responsabilidad de usar bien el método en
manos del programador que utilice la clase.
Aunque en los métodos setGrados() y getGrados() ya hemos trabajado con el vector, es cierto que
solamente accedemos a una componente individual. Los dos métodos restantes, media() y máxima(),
permiten ver cómo se trata el conjunto del vector cuando se quiere hacer algún tipo de cálculo con todas
sus componentes. Analicemos su implementación.
Empecemos por el método media(). Lo que necesitamos es sumar todos los elementos del vector y
después dividir el resultado de esa suma entre 24. Para sumar las componentes del vector tenemos que ir
recorriéndolas y acumulando su valor en una variable, lo cual se puede programar aplicando los esquemas
de tratamiento iterativos que estudiamos en la Sección 4.10.2. Estamos ante una secuencia descrita
por enumeración de sus elementos: grados[0], . . . , grados[23]. Para pasar por todos ellos simplemente
tenemos que hacer que el ı́ndice vaya variando desde 0 hasta 23. Eso lo conseguimos con la sentencia
for de la lı́nea 26. Recuérdese que el vector grados tenı́a 24 componentes, que será precisamente el valor
del atributo length. Como se mantiene en el bucle mientras la variable de control hora sea menor que
dicho atributo, y dado que se inicializa en 0 y se va incrementando en cada iteración en una unidad, la
variable hora pasará por todos los valores entre 0 y 23 y el bucle finalizará cuando valga 24 y estemos
fuera del vector grados. En cada iteración se va acumulando en la variable local suma el valor de la
componente que se está tratando, grados[hora]. Cuando el bucle acaba simplemente hay que retornar la
media, dividiendo el valor sumado entre 24. Podrı́amos haber hecho el método de forma que devolviera
un int, en lugar de un double, pero hemos preferido hacerlo ası́ para que se vea una vez más el uso del
operador de casting (Sección 2.11.1).
El método máxima(), aunque parecido, tiene alguna particularidad interesante. La idea del método
es mantener en la variable local max el valor máximo entre todas las componentes visitadas hasta ese
momento. En todos los algoritmos que calculan el máximo de algo3 hay que tener siempre en cuenta
un detalle importante: inicializar esa variable en la que vamos calculando el máximo de forma que no
se le dé nunca un valor inicial mayor que el del conjunto del que queremos calcular el máximo. Si se
3 Todo lo dicho es aplicable para algoritmos que calculen el mı́nimo.

123
le asignará inicialmente a max un valor mayor que el de todas las temperaturas, entonces el método no
calcuları́a correctamente la temperatura máxima. En este caso lo tenemos bastante fácil para inicializar
la variable max: le asignamos el valor de la primera componente. Esto podemos hacerlo ya que sabemos
que el vector grados no es vacı́o, tiene 24 componentes siempre. Si el vector pudiera ser vacı́o deberı́amos
seguir otra estrategia de inicialización. Una vez fijado el valor inicial, simplemente nos resta recorrer el
resto de componentes. Nótese que en este caso el bucle empieza en la componente 1 en lugar de en la 0,
y comprueba para todas ellas si su valor es mayor que el registrado hasta esa iteración. Caso de serlo, se
actualiza el valor de la variable local max. Al finalizar el bucle simplemente debemos retornar su valor.
Como se puede apreciar, el método máxima() es más corto que los métodos para calcular el máximo de
dos y tres números que hicimos en el tema anterior. Hemos podido programarlo de esta manera gracias
a que tenı́amos los datos en un vector, lo cuál nos permite tratar todos ellos de forma secuencial. La
ejecución del programa no resultará más rápida que si hubiéramos usado 23 sentencias if, ya que tal
como está hecho al final se tendrán que hacer 23 comparaciones también, pero la facilidad para escribir
el programa y para que otros programadores lo entiendan es muchı́simo mayor.
Vamos a ver cómo se podrı́a usar la clase Temperaturas en un programa, por ejemplo:
Ejemplo1Temperaturas.java
1 import java.util.Scanner;

3 /** Ejemplo 1 de uso de la clase Temperaturas


4 * @author los profesores de IP */
5 public class Ejemplo1Temperaturas {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Objeto de la clase Temperaturas
11 Temperaturas t = new Temperaturas();
12 //Leemos las temperaturas
13 System.out.print("Introduce las temperaturas: ");
14 int valor;
15 for (int hora=0;hora<24;hora++) {
16 valor=teclado.nextInt();
17 t.setGrados(hora, valor);
18 }
19 //Mostramos el máximo y la media en la pantalla
20 System.out.printf("La máxima ha sido %d\n",t.máxima());
21 System.out.printf("La media ha sido %f\n",t.media());

23 }
24 }

El programa crea un objeto Temperaturas por defecto, de forma análoga a como hemos hecho con
clases anteriores. Una vez creado el objeto t se procede a leer de teclado las 24 temperaturas y a asignarlas
a cada una de las horas. Aquı́ se emplea un tratamiento secuencial sabiendo que es una secuencia de
longitud conocida (Sección 4.10.2), tenemos que leer de teclado y asignar 24 temperaturas usando el
método setGrados(). En lugar de contar de 1 a 24, contamos de 0 a 23 y ası́ podemos pasar directamente
ese valor como primer parámetro a setGrados(). El programa acaba imprimiendo en pantalla el resultado
que devuelven los métodos máxima() y media().
Para completar la comprensión del programa, es interesante estudiar cómo se representa el objeto t
de la clase Temperaturas que usamos en el main(). La descripción gráfica aparece en la Figura 5.8. Como
vimos en el Tema 3, las variables objetos guardan en la Pila la referencia a la posición del memoria
donde está el objeto real. En el caso de t es una referencia a un objeto Temperaturas que solamente
tiene un atributo, el vector grados. A su vez, dicho vector es también un objeto referenciado, por lo que
guarda la referencia a la posición de la memoria donde se encuentra el vector. Es decir, en el ejemplo
tenemos dos referencias, una la del objeto y otra la del vector. Pueden darse incluso situaciones más
complejas, por ejemplo, podrı́amos crear otra clase que tuviera como atributo un objeto Temperaturas
con lo que tendrı́amos una referencia más. Lo importante del ejemplo es darse cuenta que a medida
que se emplean objetos y vectores el grado de complejidad en la representación va aumentando, aunque
no resulta complicado de dibujar ya que basta con tener presente que en ambos casos son elementos
referenciados y lo que contienen son referencias a las posiciones de memoria donde se guardan realmente
los datos, tanto si son de un objeto o como si son de un vector.

124
hora 23
Objeto Vector
valor 11 0 1 2 23

t grados 10 9 8 ... 11
...

Figura 5.8: Representación en memoria de un objeto Temperaturas

pruebas Para probar la clase Temperaturas tendrı́amos que introducir valores para las temperaturas de un
dı́a en el que el máximo fuera unas veces a las 0 horas, otras a las 23 horas y otras veces en los
valores intermedios del dı́a. Se tendrı́a que probar también con temperaturas positivas y negativas.
Por ejemplo, que el máximo sea un valor negativo. También que sea 0.

5.5. Propiedades de los vectores


Para finalizar la parte más teórica sobre vectores, antes de hacer más ejercicios prácticos, conviene
resumir las propiedades o caracterı́sticas fundamentales de los vectores:

1. Los elementos de un vector se sitúan contiguos en memoria.

2. Las componentes deben ser todas del mismo tipo.

3. Tiempo de acceso constante a cualquier elemento, no importa su posición. Se tarda lo mismo


en acceder a cualquier componente.

4. Uso: se emplean para guardar colecciones de datos de un mismo tipo a los que necesitemos acceder
en un tiempo constante.

Las caracterı́sticas de los vectores están todas orientadas a conseguir el uso que se describe en el
último de los puntos. El hecho de que todas las componentes estén contiguas en la memoria y que sean
del mismo tipo, es lo que permite que los accesos se puedan hacer en un tiempo constante. En este
contexto, tiempo constante significa que se tarda lo mismo en acceder a cualquier componente del vector,
da igual la posición en la que se encuentre. Tal como se describió en la Sección 5.4.1, siempre que se
quiere acceder a una componente de un vector se debe calcular su posición en la memoria, lo que se logra
mediante el uso del operador corchete. Da igual que esa componente sea la primera o la última, para
todas ellas se tiene que hacer exactamente el mismo cálculo, que obviamente tarda el mismo tiempo.
Se podrı́a pensar que, en ejemplos como el método media() de la clase Temperaturas, se tarda menos
en llegar a la componente 0 que a la componente 23. Eso no es que se tarde menos, es que se recorre
primero. El bucle de ese método podrı́amos haberlo hecho al revés, comenzando por la componente 23
hasta llegar a la componente 0. Eso tampoco harı́a que se tardara menos en acceder a la componente
23 que a la 0. No hay que confundir el orden en que se hace un tratamiento de todas las componentes
de un vector, del hecho de que el tiempo de acceso a cualquier componente i es el mismo al emplear el
operador corchete, grados[i].
4
! Con los vectores es muy importante garantizar que se accede a una posición existente.
Al realizar tratamientos secuenciales sobre las componentes de cualquier vector es fundamental con-
trolar que se tratan las componentes que se desean. Es un error muy frecuente diseñar bucles que se
salen del rango del vector, tratando de acceder a componentes que realmente no existen. Por ejemplo, en

125
el vector grados no se puede acceder a la componente grados[24] ya que el vector tiene 24 componentes
y la última es la de ı́ndice 23. Estos errores suelen venir casi siempre propiciados por olvidar que la
primera componente es la 0, y por la tendencia a pensar que el ı́ndice de la última componente coincide
con el tamaño del vector. En la mayorı́a de los programas se hacen tratamientos sobre todo el vector,
con lo que para no cometer este tipo de errores basta con escribir siempre en esas situaciones la misma
condición, i<v.length, siendo i la variable de control del bucle (convenientemente inicializada a cero) y
v el vector cuyas componentes desean tratarse. Vamos a recalcar esto último con los dos ejemplos de la
siguiente sección.

5.6. Algoritmos simples con vectores


Dada su estructura puramente secuencial, los vectores son la estructura de datos perfecta para aplicar
los esquemas de tratamiento secuencial que se estudiaron en la Sección 4.10. Vamos a repasarlos con un
par de ejemplos: uno de un tratamiento de una secuencia y otro de búsqueda.

5.6.1. Tratamientos secuenciales de vectores


En realidad tanto los métodos media() como máxima() de la clase Temperaturas realizan tratamientos
secuenciales sobre los elementos de todo el vector grados. Hay una pequeña diferencia entre ambos que
afecta a la secuencia, mientras media() trata todas las componentes, desde la 0 a la 23, el método máxima
hace un recorrido desde la componente 1 a la 23 debido a la inicialización de la variable para calcular la
temperatura máxima. Vamos a poner otro ejemplo más, en este caso con una dificultad añadida dado que
el tratamiento de cada elemento implicará realizar otro bucle, es decir, tendremos dos bucles anidados.
enun- Añadir un método a la clase Temperaturas que pinte un gráfico de barras horizontal (usando asteriscos)
ciado con la temperatura de cada hora.
Por ejemplo,
0 : *************
1 : ***********
2 : **********
...
23: **************

La secuencia que debe tratar es trivial:


Secuencia: 0,. . . ,23, recorriendo grados[0],. . . ,grados[23] (enumeración)
1. primer-elemento: hora=0
2. sgte-elemento: hora=hora+1
3. fin-secuencia: hora>=grados.length
La pequeña dificultad añadida de este método es que el tratamiento de cada una de las componentes
requiere a su vez de un bucle. Queremos imprimir tantos asteriscos como grados de temperatura haya
hecho en cada una de las horas. Es un bucle para tratar una secuencia de longitud conocida, para cada
hora debemos imprimir grados[i] asteriscos. Haremos un bucle que se repita grados[i] veces y en cada
una de las iteraciones se imprimirá un asterisco.
El código fuente podrı́a ser el siguiente:
4 public class Temperaturas {
...
40 /**Pinta un gráfico de barras con la temperatura de cada hora*/
41 public void pintaGráfico() {
42 for (int hora=0; hora<grados.length; hora++) {
43 System.out.printf(" %d:",hora);
44 for (int i=1;i<=grados[hora];i++)
45 System.out.print(’*’);
46 System.out.println();
47 }
48 }
49 }

126
En el bucle externo recorremos todo el vector. Para ello hacemos que la variable de control hora recorra
el rango desde 0 hasta 23, o mejor dicho, desde 0 hasta grados.length-1. Para lograrlo se inicializa a 0 y
la condición es que sea estrictamente menor que grados.length, es decir que el último elemento tratado
será grados[grados.length-1]. Nótese que le hemos dado el nombre hora para indicar que el bucle recorre
las distintas horas del dı́a.
Para cada una de las temperaturas del dı́a tenemos que imprimir una lı́nea con tantos asteriscos como
valor tenga dicha temperatura. El bucle interno necesario es un bucle que se repite un número de veces
conocido (grados[hora] veces), por lo que contamos ese número de veces, empezando en 1 y mientras el
número de ejecuciones realizado sea menor o igual que la temperatura de grados[hora].
En ambos casos hemos empleado bucles for ya que se adaptan a la perfección, tanto para hacer el
recorrido de todo el vector, como para hacer el bucle interno que simplemente requiere contar el número
de iteraciones que realiza.

pruebas En este caso solamente consideramos temperaturas no negativas y se deberı́an probar con distintos
valores para cada hora, incluyendo especialmente el valor 0.

5.6.2. Búsqueda lineal en vectores


Uno de las acciones más tı́picas que se realizan con vectores es tratar de encontrar una componente
que cumpla una cierta propiedad. El algoritmo más simple para realizar este tipo de acciones es emplear
una búsqueda lineal. Se basa en recorrer secuencialmente todos los elementos de un vector hasta que
alguno de ellos cumpla la propiedad buscada. No es más que una aplicación directa de las búsquedas
asociativas estudiadas en la Sección 4.10.3.
enun- Realizar un programa que lea de teclado primero un vector de enteros y luego un valor entero y determine
ciado si dicho valor aparece o no en el vector.

Aplicando los esquemas estudiados, para resolver el problema debemos determinar dos cosas: i) la
secuencia de elementos que vamos a tratar y ii) la propiedad que estamos buscando.

Secuencia: v[0], . . . , v[v.length-1] (enumeración)

Propiedad: número == v[i]

Una vez determinados ambos elementos, solamente queda por aplicar el esquema de las búsquedas
asociativas, resultando un algoritmo como el que sigue:

Algoritmo 5.1 Búsqueda lineal en un vector


Leer número y v de teclado
i=0
mientras (i<v.length) Y (número != v[i]) hacer
i=i+1
fin mientras
si (i<v.length) entonces Imprimir SÍ está
sino Imprimir NO está
fin si

Como apuntamos cuando explicamos las búsquedas asociativas, en las búsquedas en vectores es
esencial mantener el orden de la condición del bucle: NO fin-secuencia Y NO elemento-encontrado. El
error más habitual en este algoritmo es cambiar el orden de esas dos expresiones. El peligro está en
que en elemento-encontrado, número == v[i], estamos accediendo a una posición de un vector. Antes
de cualquier acceso a una componente de un vector debemos estar seguros de que el valor del ı́ndice
está dentro del rango del vector. Por ello, primero se debe poner SIEMPRE la condición que nos garantiza
que seguimos dentro de la secuencia de búsqueda, esto es, del vector. Si esa primera condición fuera falsa,
entonces no se realiza la comprobación de si es el elemento que buscamos, gracias a la optimización por
cortocircuito de las expresiones condicionales con el operador && (ver Sección 4.3.2).
Vamos a analizar el programa completo:

127
BúsquedaLineal1.java
1 import java.util.Scanner;

3 /** Búsqueda lineal de una componente en un vector


4 * @author los profesores de IP */
5 public class BúsquedaLineal1 {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Leemos el vector de enteros de teclado
11 int[ ] v = leeVector(teclado);
12 //Leemos el número
13 System.out.print("\nIntroduce un número: ");
14 int número = teclado.nextInt();
15 //Hacemos una búsqueda asociativa, buscando v[i]==número
16 int i=0;
17 while ( ( i < v.length ) && ( v[i] != número ) )
18 i++;
19 if ( i < v.length )
20 System.out.printf("\n %d SÍ está en el vector",número);
21 else
22 System.out.printf("\n %d NO está en el vector",número);
23 }

25 /** Lee un vector del objeto Scanner pasado


26 * @param t objeto Scanner del que leer los datos
27 * @return el vector de enteros leı́do */
28 public static int[ ] leeVector(Scanner t) {
29 System.out.print("Introduce el tamaño del vector: ");
30 int tamaño=t.nextInt();
31 //Reservamos memoria para el vector
32 int[ ] vec = new int[tamaño];
33 //Leemos las componentes
34 System.out.print("Introduce las componentes: ");
35 for (int i=0; i<vec.length; i++)
36 vec[i]=t.nextInt();
37 return vec;
38 }
39 }

El primer detalle novedoso del programa es cómo leemos el vector de teclado. Hemos hecho un método
llamado leeVector() que recibe un objeto de la clase Scanner y retorna un vector de enteros, incluyendo
obviamente las componentes leı́das de teclado. El método puede ser útil ya que lo podrı́amos usar para
leer muchos vectores. Para leer los datos es imprescindible que use un objeto de la clase Scanner. Dado
que el método main() tiene definido el objeto teclado y por tanto leeVector() no tendrá acceso a él ya
que es un objeto local al main(), necesitamos pasárselo como parámetro para que leeVector() pueda
usarlo y leer los datos que necesita. El método solicita primero el número de componentes y una vez
leı́do el número introducido por el usuario, reserva el espacio necesario para el vector. Es decir, permite
leer vectores de cualquier tamaño. Después de crear el vector con new, se leen las distintas componentes
usando un bucle for que va recorriendo todo el vector. Finalmente se retorna el vector.
En el programa principal leemos tanto el vector, usando el método leeVector() y asignando lo que
devuelve al vector local v, como el número entero que guardamos en la variable número. Una vez hechas
ambas cosas, tan solo nos resta por aplicar el Algoritmo 5.1. En cada iteración se comprueba si la
componente i-ésima es la que estamos buscando. Cuando finaliza el bucle hay que comprobar por cuál
de las dos condiciones ha finalizado. De nuevo es fundamental no usar en ese if la condición (número !=
v[i]), o su contraria (número == v[i]), ya que el valor de i puede estar fuera del rango del vector lo
que ocasionarı́a una excepción ArrayIndexOutOfBoundsException. Por ese motivo el if final usa la otra
condición, (i<v.length), preguntando si seguimos dentro del rango de búsqueda. Si es ası́, es porque
hemos encontrado el elemento, y si no, es porque hemos recorrido todas las componentes y ninguna ha
sido igual al número buscado.
Como en todo algoritmo de búsqueda, es primordial entender que cuando se está comprobando si
el elemento i-ésimo es el que buscamos o no, lo que es seguro es que en ninguna de las componentes
anteriores estaba el elemento buscado, dado que de haber estado el bucle hubiera finalizado sin alcanzar

128
la componente i-ésima. Por ello cuando lo encontramos, si lo encontramos, es seguro que el bucle se
detiene en la primera aparición, luego es eficiente.

pruebas Dado que es un algoritmo basado en búsqueda tenemos que considerar las dos situaciones tı́picas, que
el elemento se encuentre en el vector y que no. Dentro de los casos en los que encuentre deberı́amos
probar que el elemento estuviera en la primera posición, en la última y en una del medio del vector.
Para los casos en los que el valor no está en el vector, habrı́a que probar que estuviera el valor anterior,
el siguiente, un múltiplo, un divisor, etc. Ambas pruebas habrı́a que repetirlas para distintos valores,
no siempre utilizar el mismo número como el valor que se busca en el vector. Además, hay que probar
con vectores de distinta longitud, con una sola componente, con dos componentes y un par de casos
con más de dos componentes, por ejemplo, con 5 y 6 para incluir un caso con longitud impar y otro
par. Esos cuatro casos ya serı́an suficientes para probar que el programa funciona con vectores de
distinta longitud.
Para ver otra forma de realizar el programa, en lugar de incluir la búsqueda como parte del programa
principal, la hemos implementado en un método aparte, que nos servirı́a para realizar cualquier búsqueda
en vectores de enteros. El código de ese método y su uso en el programa principal serı́a el siguiente:
BúsquedaLineal2.java
1 import java.util.Scanner;

3 /** Búsqueda lineal de una componente en un vector (Método)


4 * @author los profesores de IP */
5 public class BúsquedaLineal2 {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Leemos el vector de enteros de teclado
11 int[ ] v = leeVector(teclado);
12 //Leemos el número
13 System.out.print("\nIntroduce un número: ");
14 int número = teclado.nextInt();
15 //Comprobamos si está en el vector
16 if ( búsquedaLineal(v,número) )
17 System.out.printf("\n %d SÍ está en el vector",número);
18 else
19 System.out.printf("\n %d NO está en el vector",número);
20 }

22 /** Lee un vector del objeto Scanner pasado


23 * @param t objeto Scanner del que leer los datos
24 * @return el vector de enteros leı́do */
25 public static int[ ] leeVector(Scanner t) {
26 System.out.print("Introduce el tamaño del vector: ");
27 int tamaño=t.nextInt();
28 //Reservamos memoria para el vector
29 int[ ] vec = new int[tamaño];
30 //Leemos las componentes
31 System.out.print("Introduce las componentes: ");
32 for (int i=0; i<vec.length; i++)
33 vec[i]=t.nextInt();
34 return vec;
35 }

37 /** Busca un elemento en un vector


38 * @param vec un vector de enteros
39 * @param n un número enteros
40 * @return true si n está en vec, false en caso contrario */
41 public static boolean búsquedaLineal(int[ ] vec, int n) {
42 //Hacemos una búsqueda asociativa, buscando vec[i]==n
43 int i=0;
44 while ( ( i < vec.length ) && ( vec[i] != n ) )
45 i++;
46 return ( i < vec.length );
47 }
48 }

129
El método, al que hemos llamado búsquedaLineal(), retorna un valor de tipo boolean y recibe dos
parámetros, el vector de enteros y el número entero que deseamos buscar. El bucle de la búsqueda
es exactamente igual que el que tenı́amos en el programa principal anterior, y la diferencia está en la
sentencia posterior para comprobar por cuál de las dos condiciones finalizó el bucle. En este caso ni
siquiera hace falta una sentencia if ya que lo único que queremos saber es si el número está o no.
Retornamos directamente el valor de la condición (i<vec.length), es decir, si esa condición es cierta
quiere decir que no hemos recorrido todo el vector y por tanto el valor de n sı́ está en el vector. Si es
falsa, es que hemos llegado al final y por tanto no está. Es un algoritmo sencillo y eficiente.
Una posible modificación del método consistirı́a en cambiarlo para que en lugar de devolvernos si
el valor del parámetro n está en vec o no, nos devolviera la posición en la que se encuentra, caso de
estar. Siguiendo esa especificación, el valor de retorno debe ser int ya que los ı́ndices son siempre de
ese tipo. Toda vez que los ı́ndices son siempre mayores o iguales a cero, para indicar que el elemento
no se encuentra en el vector se podrı́a retornar un valor negativo, por ejemplo −1. Programándolo con
esa funcionalidad, la sentencia return serı́a algo más compleja, por ejemplo, usando el operador ( ? : )
quedarı́a, return ( i < vec.length ? i : -1 ).

pruebas Serı́an los mismos del programa anterior, ya que resuelven el mismo problema aunque de una forma
distinta.

5.7. Vectores de objetos


Si en la clase Temperaturas, mostrada en la Sección 5.4.3, aparece un ejemplo de objetos de una clase
que entre los atributos que los describen tienen un vector, también se pueden tener vectores de objetos
de una clase. Vamos a verlo con un ejemplo, precisamente con objetos de la clase Temperaturas.
enun- Realizar un programa que lea todas las temperaturas horarias de los dı́as de un mes y calcule e imprima
ciado la temperatura máxima de ese mes.

Aunque hay otras formas de realizarlo, un posible enfoque se basa en emplear un vector de objetos
Temperaturas:

En lugar de crear un vector de elementos de un tipo básico, vamos a utilizar un vector cuyo tipo
será una clase. Nada en la sintaxis de declaración de un vector, revı́sese la Sección 5.2.2, impide
crear un vector de objetos de una clase, dado que las clases son tipos válidos.
Pero hay que tener en cuenta que crear y reservar espacio para el vector (usando new) no implica
que se creen sus componentes.
Es decir:
1. se debe crear el vector, y
2. cada uno de los objetos que contiene.

Lo mejor es ver el código fuente de una posible solución:

TemperaturasUnMes.java
1 import java.io.File;
2 import java.io.FileNotFoundException;
3 import java.util.Scanner;

5 /** Temperaturas de un mes, ejemplo de un vector de objetos


6 * @author los profesores de IP */
7 public class TemperaturasUnMes {

9 public static void main(String[] args) throws FileNotFoundException {


10 //Objeto File para acceder a un fichero
11 File f = new File ( "temperaturas.txt" );
12 //Objeto Scanner asociado con el objeto f
13 Scanner fichero= new Scanner(f);
14 //Leemos el número de dı́as para el que tiene el fichero
15 int dı́as = fichero.nextInt();
16 //Vector con tantos objetos Temperaturas como dı́as

130
17 Temperaturas[ ] t = new Temperaturas[dı́as];
18 //Leemos las temperaturas de cada dı́a
19 for (int dı́a=1; dı́a <= dı́as; dı́a++) {
20 t[dı́a-1]=new Temperaturas(); //creamos el objeto!
21 for (int hora=0;hora<24;hora++)
22 t[dı́a-1].setGrados( hora, fichero.nextInt() );
23 }
24 //Calculamos la temperatura máxima del mes
25 int máxima=t[0].máxima(); //temp máxima primer dı́a mes
26 //Recorremos el resto de dı́as, hasta t[t.length-1]
27 for (int dı́a=1; dı́a<t.length; dı́a++) {
28 int m = t[dı́a].máxima(); //para no llamarlo 2 veces
29 if ( máxima < m ) máxima = m;
30 }
31 //Mostramos la temperatura máxima en la pantalla
32 System.out.printf("La temperatura máxima ha sido %d\n",máxima);
33 }
34 }

Dado que el programa requiere la introducción de muchos datos, 24 temperaturas por cada dı́a del
mes, en lugar de leer los datos de teclado vamos a leerlos de un fichero de texto. No está entre los objetivos
de la asignatura trabajar con ficheros. Esa competencia, y la serialización en general, será cubierta en
la asignatura del segundo cuatrimestre Metodologı́a de la Programación4 . En este ejemplo lo usamos
por dos motivos: i) por no hacer tediosa la introducción de datos y que el programa se pueda probar de
forma rápida y ii) para mostrar que la lectura de datos de un fichero puede realizarse prácticamente de
forma análoga a la lectura de teclado, usando en ambos casos los mismos métodos de la clase Scanner.
Las únicas diferencias existentes, con respecto a la lectura por teclado, son cuatro:

1. Hay que incluir los paquetes java.io.File y java.io.FileNotFoundException.

2. El método main() debe poder lanzar una excepción FileNotFoundException. Eso se añade después
de la declaración del parámetro args del main(), lı́nea 9.

3. Se debe crear un objeto de la clase File asociado con el fichero del que se van a leer los datos. En
el ejemplo es el objeto f. El fichero se incluye en el mismo directorio donde tenemos el proyecto.

4. El objeto de la clase Scanner con el que se van a leer los datos debe asociarse al objeto File creado.
Es decir, en este caso no se asocia con el teclado (System.in) sino con el objeto f de la clase File.

Después de todas esas acciones podremos leer los datos del fichero usando los mismo métodos que se
han empleado en el resto de programas cuando se utilizaba el teclado como entrada.
Lo más interesante del programa es estudiar cómo se crea el vector de objetos. Lo primero es de-
terminar el tamaño que debe tener, es decir, el número de dı́as para el que tenemos que guardar las
temperaturas. Ese es el primer número que contiene el fichero y lo leeremos sobre la variable entera dı́as.
Una vez leı́do ese dato, reservamos espacio para el vector de objetos Temperaturas que hemos llamado
t. Como siempre después de new se indica la clase de las componentes, en este caso Temperaturas, y el
número entre corchetes de objetos (valor de la variable dı́as) que tendrá el vector. Es fundamental tener
presente que crear el vector no hace que se creen las componentes.
Una vez reservado el espacio necesario para el vector, tenemos que ir creando los objetos Temperaturas
individuales que almacenarán las temperaturas horarias de cada dı́a del mes. Para lograrlo hacemos un
bucle que itera sobre todos los dı́as del mes, teniendo en cuenta que el número de dı́as es lo que vale
la variable dı́as. Usamos un bucle for cuya variable de control dı́a recorre la secuencia 1..dı́as, ambos
incluidos5 . En cada iteración lo primero es crear el objeto para luego poder leer los valores de sus
temperaturas. Es importante recordar que el dı́a 1 se guardará en la componente 0, por ello al crear el
objeto con new lo guardamos en la posición t[dı́a-1]6 . Dado que t es un vector de tipo Temperaturas,
cada una de sus componentes será un objeto Temperaturas. Es decir, t[dı́a-1] es a todos los efectos
un objeto de esa clase y por tanto podremos invocar todos sus métodos usando el operador punto y
4 Es decir, no se pedirá en ningún examen o control de Introducción a la Programación.
5 También se podrı́a haber hecho un recorrido desde 0 hasta dı́as-1 que resultarı́a más eficiente pero que perderı́a
el significado de la variable dı́a.
6 Usando el recorrido 0..dı́as-1 se guardarı́a en la posición t[dı́a].

131
llamando al método que necesitemos. Es lo que hacemos para guardar las temperaturas horarias de ese
dı́a. Hacemos un bucle que pase por las 24 horas del dı́a y para cada hora fijamos su temperatura usando
el método setGrados() y pasándole la temperatura leı́da del fichero. Es decir, la lectura de datos requiere
de dos bucles anidados, el externo que va pasando por cada dı́a de mes, y el interno que recorre cada
hora de ese dı́a.
Lo último del programa es calcular la temperatura máxima. Siguiendo la misma estrategia del método
máxima() de la clase Temperaturas, ver Sección 5.4.3, inicializamos la variable máxima a la temperatura
máxima del primer dı́a del mes, la cual se obtiene llamando al método máxima() con el objeto t[0].
Solamente resta recorrer el resto de dı́as del mes e ir actualizando la variable máxima siempre que la
temperatura máxima de un dı́a supere la registrada hasta ese momento. Para evitar dos llamadas al
método máxima(), guardamos su valor en la variable local al bucle m. Si el valor guardado en m es mayor
que el que tiene máxima, se lo asignamos a esta última variable sin necesidad de volver a invocar el método
máxima().

pruebas Para probar este programa habrı́a que usar ficheros con meses con distinto número de dı́as: 28, 29, 30
y 31. Cada uno de esos ficheros tendrı́a que tener dı́as diferentes en cuanto a la posición donde se da
la temperatura máxima: que se de a las 0 horas, a las 23 horas y en horas del dı́a intermedias. Además
habrı́a que probar con temperaturas negativas, que todo un dı́a tuviera valores negativos (máximo
negativo) y otro caso en el que el máximo sea 0. Dado que en cada fichero hay las temperaturas de
varios dı́as, si se diseñan cuidadosamente dichos ficheros, con pocos ficheros es posible hacer todas los
casos de prueba necesarios. Además en este caso, las pruebas están hasta cierto punto automatizadas
ya que no hay que teclear los valores si tuviésemos que repetir las pruebas.

Volviendo al programa, el aspecto más interesante del mismo es comprender cómo se debe crear un
vector de objetos, ya que están involucradas varias variables referenciadas: el vector y los objetos que
este contiene.

23 1 0

13 ... 11 10 grados
hora 23

día 30
0 1 30
t ...

...
0 1 23

grados 10 9 ... 11

0 1 23

grados 9 8 ... 10

Figura 5.9: Vector de objetos Temperaturas para un mes con 30 dı́as

4
! Un vector de objetos implica que se debe hacer un new para el vector y un new por cada
objeto que contenga el vector.

La Figura 5.9 muestra la representación en memoria del vector t del programa realizado. Dado que
un vector es un objeto referenciado, la variable t guarda en la Pila la referencia del objeto vector que
se creó con new en la lı́nea 17. Las componentes de t son a su vez objetos de la clase Temperaturas, por
lo que cada elemento de t guarda una referencia al objeto de la clase Temperaturas reservado con el
operador new en la lı́nea 20.

132
5.8. Cadenas de caracteres
Además de los tipos básicos, otro elemento esencial en cualquier lenguaje de programación para
representar la información que se suele necesitar guardar son las cadenas de caracteres.

definición Cadena de caracteres: Es una secuencia de caracteres, donde cada uno de los caracteres pertenece
al alfabeto que define el esquema de codificación empleado.

En el caso del Java, por tanto, las cadenas de caracteres serán secuencias de caracteres Unicode, con
lo que permiten representar cualquier texto escrito en prácticamente cualquier idioma. Las cadenas de
caracteres tienen mucha utilidad ya que:

Permiten guardar la información de carácter textual. Usando los tipos básicos no podrı́amos hacerlo.
El tipo char es el único relacionado con información de texto, pero solamente puede almacenar un
carácter.
El ejemplo más habitual es guardar el nombre y el apellido de una persona, o la dirección y ciudad
en la que vive. Muchos de los datos que guardan los sistemas de información son de carácter textual.

Por todo ello, es habitual dotar a los lenguajes de programación de mecanismos para representar las
cadenas de caracteres. El primero de ellos es mediante constantes de tipo cadena:

Las constantes cadena de caracteres se representan como secuencias de caracteres entre comillas
dobles. Es lo que hemos utilizado para imprimir mensajes de texto con los métodos print(),
println() y printf().

Ejemplos: "Hola, Mundo" o "Introduce un valor: "

4
! No confundir las constantes cadena, entre comillas dobles, con las constantes char, entre
comillas simples.
Pero además de mediante constantes, es lógico pensar que los programas necesitan variables que
guarden cadenas. Si volvemos a su definición, el hecho de ser una secuencia de caracteres encaja con la
definición de vector, en el sentido de ser una tupla de varios elementos, en este caso caracteres. Luego las
cadenas son vectores donde cada uno de los elementos representa una carácter. Eso nos llevarı́a a pensar
en manejar las cadenas de caracteres del mismo modo en que se manejan los vectores de otros tipos, por
ejemplo, los vectores de enteros. Sin embargo, todos los lenguajes disponen de mecanismos especiales que
permiten tratar las cadenas de caracteres de una forma distinta que el resto de vectores. Java no es una
excepción.
4
! En Java las cadenas no se suelen representar con char[ ].
Java permite manejar las cadenas de caracteres mediante algunas clases que proporcionan unas funcio-
nalidades mucho más amplias que las ofrecidas por los vectores básicos vistos en los apartados anteriores.
Los motivos por los que las cadenas no se manejan como el resto de vectores son varios:

Con las cadenas se deben poder hacer operaciones de entrada y salida de manera estándar. Es
mucho más habitual necesitar leer una cadena de caracteres o imprimirla, que hacer lo mismo con
vectores de otros tipos. Estos últimos pueden requerir formateos particulares de acuerdo con la
aplicación en que se usen, pero las cadenas, como en realidad representan palabras o un texto,
tienen un forma estándar de escribirse y leerse. De hecho, el parámetro que requieren los métodos
print() y println() es una cadena, en concreto un objeto de la clase String que se estudia en la
sección siguiente. Cuando a esos métodos se les pasa un dato de otro tipo, se convierte previamente
a tipo String y luego se imprime. Esto indica que las cadenas son la forma natural de representar
la información para las personas.
Suelen utilizarse para representar datos, pero no se hacen operaciones con ellas. Se emplean más
para guardar un dato textual, que para luego hacer algo con él, más allá de tenerlo en memoria y
tal vez escribirlo en pantalla o en un fichero. No se hacen tantas operaciones con las cadenas como
se hacen con los vectores de enteros o de reales.

133
Requieren realizar otros tipos de operaciones, como convertir un cadena en mayúsculas o comparar
si dos cadenas son iguales. Una aplicación tı́pica es buscar una cadena dentro de un texto (otra
cadena) o en los datos que tenemos guardados sobre algo. Para ello se necesita comparar la cadena
que se busca con la información almacenada que también será de carácter textual.

Por todo ello, se suelen manipular con clases que incluye Java, por ejemplo:

1. String: cadenas inmutables (no cambian).


2. StringBuffer: cadenas mutables (sı́ cambian).

5.8.1. Clase String


La clase que vamos a utilizar para manejar cadenas es la clase String. Tiene algunas peculiaridades
que la hacen diferente de otras clases, por ejemplo, no requiere hacer uso del operador new para crear un
objeto de la clase y permite concatenar dos cadenas usando los operadores + ó +=. Vamos a verlo con
varios ejemplos:
1 String s1 = "Hola, "; //s1 creado e inic. sin new
2 String s2 = new String("Mundo"); //Ahora con new
3 s1 += s2; //s1 contendrá "Hola, Mundo"
4 System.out.print(s2+" Java" ); //Imprime "Mundo Java"
5 String s3 = teclado.next(); //Lee una cadena con Scanner

En la primera declaración se crea el objeto s1 y se inicializa con la cadena “Hola”. Normalmente


para crear un objeto hace falta usar el operador new; con la clase String puede crearse e inicializarse
directamente con una constante cadena. Eso hace que la declaración del siguiente objeto, s2, sea más
infrecuente: requiere escribir más para hacer lo mismo. No se suele usar new con los objetos String. Los
operadores + y + = aplicados sobre dos objetos String significan que se concatenan las cadenas que
aparecen como operandos generándose un nuevo objeto String con el resultado de esa concatenación.
Es lo que ocurre en las dos siguientes instrucciones. En la tercera lı́nea se asigna al objeto s1 el resul-
tado de concatenar la propia cadena s1 con la cadena s2, resultando en este ejemplo la cadena “Hola
Mundo”. También se concatena la cadenas s2 con la constante cadena “ Java” en la lı́nea 4, pero en este
caso la cadena resultante se imprime usado el método print(). Los objetos String se puede imprimir
directamente usando los métodos print() o println(), y en el caso de usarlos con printf() la letra para
convertir el formato es la s, es decir, hay que poner %s (ver Apéndice E). La lectura de teclado se hace
usando el método next() de la clase Scanner.
Pero sin duda lo que da más funcionalidad a los objetos String son los métodos de que dispone la
clase. En la Tabla 5.1 se muestra una lista con los métodos más significativos de la misma. La clase tiene
más métodos. La lista de los mismos y su especificación se puede consultar en la documentación de la
clase, disponible por ejemplo a través de Internet.
4
! En la clase String para determinar la longitud de la cadena se emplea el método length().
Es un método, no un atributo como en los vectores, luego deben ponerse los paréntesis.
Por último, recalcar un aspecto importante de la clase String. La cadena que guarda un objeto
String es constante, es decir, no se puede cambiar. Si con un vector de enteros, por ejemplo, podı́amos
acceder a cualquier de sus componentes con el operador corchete y cambiar su valor, con un objeto String
eso es imposible, no se puede cambiar ninguno de sus caracteres. En la Sección 5.8.2 veremos algunas
alternativas que existen en el caso de que nuestro programa necesite guardar cadenas susceptibles de ser
modificadas.
4
! Los objetos String son inmutables, es decir, pueden devolver otros objetos String distintos
pero no se puede cambiar ningún carácter de ellos mismos.
Para practicar con el uso de los objetos String, vamos a hacer un programa que es un clásico dentro
de los ejercicios con cadenas de caracteres. Nos servirá para repasar las búsquedas y para ver cómo se
puede recorrer una secuencia con dos ı́ndices.
enun- Realizar un programa que lea de teclado una cadena c y diga si es un palı́ndromo o no.
ciado
Lo primero es saber qué es un palı́ndromo.

134
Tabla 5.1: Algunos métodos de la clase String (ordenados alfabéticamente). Para una mejor descripción,
incluyendo la referencia de otros métodos de la clase, consúltese la documentación de la clase String

Método Descripción
char charAt(int i) Devuelve el carácter en la posición i.
int compareTo(String s) Compara alfabéticamente el objeto String con el objeto String que se
pasa como argumento. Si el objeto va antes alfabéticamente devuelve
un valor negativo, positivo si el parámetro va antes y 0 si son iguales
(cuando equals() devuelve true).
boolean equals(Object s) Devuelve true si el objeto String contiene la misma secuencia de ca-
racteres que s.
int indexOf(char c) Devuelve el ı́ndice donde se produce la primera aparición de c. De-
vuelve -1 si c no está en el String.
int indexOf(String s) Igual que el anterior, pero busca la primera aparición de la subcadena
s.
int length() Devuelve la longitud del String.
String valueOf(int i) Devuelve un String que es la representación del entero i. Es un método
estático. Hay métodos equivalentes donde el parámetro es otro tipo
básico, como float, double, long, e incluso char[ ].
char[ ] toCharArray() Devuelve un vector de caracteres estándar con el mismo contenido
que el objeto String.
String toLowerCase() Devuelve un String equivalente al objeto pero convertido a minúscu-
las.
String toUpperCase() Lo mismo que el anterior, pero convertido a mayúsculas.

Palı́ndromo (R.A.E.): Palabra o frase que se lee igual de izquierda a derecha, que de derecha a
izquierda
Ejemplos: anilina; dábale arroz a la zorra el abad.
En un palı́ndromo el primer carácter es igual al último, el segundo al penúltimo, y ası́ sucesivamente
con todos los demás. El problema se debe resolver como una búsqueda. Estamos buscando un carácter
que no sea igual a su simétrico. La dificultad está en cómo, dado un carácter calculamos su simétrico.
Hay dos alternativas: o bien hacemos un recorrido con dos ı́ndices, uno que se mueva hacia delante y
otro hacia atrás, o bien buscamos un relación entre el ı́ndice de un carácter y el de su simétrico.
La primera opción puede ser la más intuitiva. La idea es hacer un bucle de búsqueda con dos ı́ndices,
uno (i) empieza en 0 y va creciendo, y el otro (j) se inicializa con la posición del último carácter del
String (c.length()-1) y va decreciendo. En cada iteración comprobamos si son iguales los caracteres de
las posiciones i y j. En el momento que uno de esos pares de caracteres sean distintos es que la cadena
no es un palı́ndromo. La secuencia finaliza cuando se cruzan esos dos ı́ndices, es decir, cuando el valor
de i es mayor o igual que el de j (cuando i y j son iguales no hace falta comprobar si el carácter al que
se refieren es el mismo, ya que obviamente siempre lo va a ser).
Secuencia (opción 1): usando dos ı́ndices, uno se va incrementando y el otro decrementando.

Primer elto.: i=0, j=c.length()-1

Sgte. elemento: i++, j--

Fin secuencia: i>=j

Buscar: c.charAt(i)!=c.charAt(j)

La segunda opción es menos intuitiva. Hay que descubrir una relación entre los ı́ndices de los elementos
simétricos, el primero con el último, el segundo con el antepenúltimo y ası́ sucesivamente. Si usamos un
ı́ndice i que se va incrementando desde 0, según se va incrementado la i el simétrico se va decrementando.
Luego la relación tiene que incluir el valor de la i restado de alguna forma. Si observamos la primera

135
pareja, el primer y el último carácter, i=0 y c.length()-1, si le restamos la i a la última expresión, es
decir, c.length()-1-i vemos que queda el mismo valor. Cuando i pase a valer 1, el simétrico valdrá uno
menos, porque la i aparece restada. No es sencillo descubrir este tipo de relaciones, requiere ver cómo
se mueven los valores y tratar de hallar un relación entre las variables que usemos para describir los
elementos de nuestra secuencia. Usando la relación descrita podemos definir la secuencia de búsqueda
del siguiente modo:
Secuencia (opción 2): usando un solo ı́ndice y empleando una relación entre un elemento y su
simétrico.

Primer elemento: i=0

Sgte. elemento: i++

Fin secuencia: i>=c.length()/2

Buscar: c.charAt(i)!=c.charAt(c.length()-1-i)

El programa principal crea el objeto c de la clase String y lo lee de teclado usando el método next()
de la clase Scanner. A continuación llama a los dos métodos estáticos que hemos realizado con cada uno
de los algoritmos de búsqueda descritos. Ambos métodos devuelven un valor booleano que será true si
la cadena es un palı́ndromo. Dependiendo del valor que retornen se escribe un mensaje u otro.
Los dos métodos están programados exactamente según las secuencias descritas. Respecto al uso de
la clase String, que es lo que nos ocupa en esta sección, simplemente necesitan usar el método length()
para saber el tamaño de la cadena, y el método charAt() para acceder a los caracteres individuales de
la misma. Con los objetos de la clase String no se puede usar el operador corchete. En ambos casos
programamos la búsqueda utilizando el algoritmo genérico presentado en la Sección 4.10.3. Se sigue en
el bucle de búsqueda mientras no se acabe la secuencia y no encontremos lo que buscamos, es decir,
mientras las parejas de caracteres simétricos sean iguales. Al final de los bucles comprobamos si estamos
dentro de la secuencia de búsqueda para saber si se encontró un par de caracteres distintos o no.

pruebas Es de nuevo un problema de búsqueda luego tenemos que probar con casos de palı́ndromos y otros
con palabras que no lo sean. Por ejemplo, entre los palı́ndromos, podrı́amos probar con ana, anilina o
dabalearrozalazorraelabad. Podrı́amos introducir variaciones de esas palabras poniendo alguna letra
en mayúsculas. Para los casos de palabras que no sean palı́ndromos, deberı́amos probar casos en los
que difieran en la primera letra, en las letras centrales de la palabra y en otras posiciones aleatorias
intermedias. Es importante repetir las pruebas de los palı́ndromos y no palı́ndromos con palabras de
longitud par o impar.

Palı́ndromo.java
1 import java.util.Scanner;

3 /** Dada una cadena de caracteres indica si es un palı́ndromo


4 * @author los profesores de IP */
5 public class Palı́ndromo {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 System.out.print("Cadena: ");
11 //Leemos la cadena de caracteres
12 String c = teclado.next();
13 //Con dos métodos
14 if (esPalı́ndromoV1(c))
15 System.out.println("Método1: ES palı́ndromo");
16 else System.out.println("Método1: NO ES palı́ndromo");
17 if (esPalı́ndromoV2(c))
18 System.out.println("Método2: ES palı́ndromo");
19 else System.out.println("Método2: NO ES palı́ndromo");

21 }

136
23 /** Indica si una cadena es una palı́ndromo o no
24 * @param c la cadena
25 * @return true si c es una palı́ndromo, false si no */
26 public static boolean esPalı́ndromoV1(String c) {
27 //Dos ı́ndices. Buscamos: c.charAt(i)!=c.charAt(j)
28 int i=0;
29 int j=c.length()-1;
30 while ( (i<j) && (c.charAt(i)==c.charAt(j)) ) {
31 i++;
32 j--;
33 }
34 return ( i >= j );
35 }

37 /** Indica si una cadena es una palı́ndromo o no


38 * @param c la cadena
39 * @return true si c es una palı́ndromo, false si no */
40 public static boolean esPalı́ndromoV2(String c) {
41 //Un ı́ndice. Buscamos: c.charAt(i)!=c.charAt(c.length()-1-i)
42 int i=0;
43 while ( (i < c.length()/2) &&
44 (c.charAt(i)==c.charAt(c.length()-1-i)) )
45 i++;
46 if ( i < c.length()/2 ) return false;
47 else return true;
48 }
49 }

5.8.2. Modificar cadenas de caracteres


A pesar de la gran funcionalidad que proporciona la clase String hay situaciones en las que no nos
sirve para realizar ciertas tareas. Es el caso del siguiente programa:
enun- Realizar un programa que lea de teclado una palabra en inglés y la convierta a mayúsculas.
ciado
Una primera solución trivial serı́a guardar la cadena en un objeto String y emplear el método toUp-
perCase(). Dado que estamos aprendiendo a programar, lo que pide el enunciado es hacer un algoritmo
capaz de resolver el problema sin necesidad de utilizar el método toUpperCase(). Los principales aspectos
que debemos tener en cuenta para poder realizar el programa dada esa limitación son los siguientes:

El programa necesita tratar todos los caracteres de una cadena y modificar los que están en minúscu-
las.

Al ser los objetos String inmutables no podemos usar un String si queremos modificar la cadena.

Necesitamos otra forma de hacerlo. Hay varias alternativas:

1. Usar un vector de elementos char.


2. Usar la clase StringBuffer que permite trabajar con cadenas modificables.

Vamos a optar por emplear un vector de char. Una posible implementación serı́a la siguiente:

Mayúsculas.java
1 import java.util.Scanner;

3 /** Convierte una cadena a mayúsculas


4 * @author los profesores de IP */
5 public class Mayúsculas {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 System.out.print("Cadena: ");
11 //Leemos la cadena de caracteres
12 String cadena = teclado.next();
13 char[ ] mayúsculas = cadena.toCharArray();

137
14 //La pasamos a mayúsculas
15 for (int i=0; i<mayúsculas.length; i++) {
16 if (mayúsculas[i] >= ’a’ && mayúsculas[i] <= ’z’)
17 //hay que restarle 32: ’A’ es 65 y ’a’ es 97
18 mayúsculas[i] = (char) (mayúsculas[i] - 32);
19 }
20 //Se imprime la cadena
21 System.out.println(mayúsculas);
22 }
23 }

El primer problema que tenemos al usar un vector de componentes char es que no podemos leer
directamente una cadena de teclado sobre un vector de caracteres. Necesitamos leerlo sobre un objeto
String y luego convertirlo a vector de char usando el método toCharArray(). Es algo muy similar a lo
que ocurre con la lectura de caracteres de teclado (ver el programa de la Sección 4.4.2). En este caso lo
hemos hecho creando explı́citamente un objeto String y luego invocando el método toCharArray(). Se
puede hacer también con una sola instrucción, llamando al método toCharArray() con el objeto String
que devuelve next(), es decir, teclado.next().toCharArray().
Una vez que tenemos la cadena en el vector de caracteres, llamado mayúsculas, podemos cambiar los
caracteres que contiene usando el operador corchete. El programa requiere tratar la secuencia formada por
todos los caracteres de la cadena leı́da y convertir las letras minúsculas en mayúsculas. La precondición
del programa, el hecho de que sea una palabra en inglés, está puesta para no tener que considerar
algunos caracteres particulares del castellano, como la ñ, o las vocales acentuadas, y hacer el programa
más sencillo. Solamente tendremos que convertir las letras en minúsculas de la ’a’ a la ’z’. Eso es lo que
hace la sentencia if dentro del bucle for: cuando la letra está entre ’a’ y ’z’ (recuérdese que las letras
del alfabeto inglés están en posiciones consecutivas de la codificación Unicode), entonces tenemos que
modificar la letra de esa posición y convertirla a mayúsculas. La forma de hacerlo es restándole 32 al
código entero de la letra, que es la diferencia entre cualquier letra minúscula y su correspondiente letra
mayúscula. Dado que la resta produce un int, es necesario convertirlo a char antes de guardarlo en la
posición correspondiente del vector mayúsculas.

pruebas Para probar este programa deberı́amos introducir palabras que tuvieran todos los casos de letras
minúsculas, especialmente las dos letras que están en la frontera dentro de la tabla ASCII, la ’a’ y la
’z’.

5.8.3. Los argumentos de un programa


Después de saber cómo funcionan los vectores en Java y conocidas las funcionalidades principales de
la clase String, podemos empezar a hacer uso de los parámetros que pueden recibir los programas. Todo
método main() recibe sus parámetros en un vector de objetos String. Los parámetros son las cadenas
que se pueden suministrar cuando ejecutamos un programa, por ejemplo desde la lı́nea de comandos.
Vamos a verlo con un pequeño programa:
enun- Hacer un programa que imprima por pantalla el parámetro que vaya antes alfabéticamente
ciado

Figura 5.10: Terminal con la ejecución del programa PrimeraPalabra.java. El programa imprime, de entre
todos sus parámetros, aquél que va antes alfabéticamente

La Figura 5.10 muestra un ejemplo de la ejecución desde la lı́nea de comandos del programa que
queremos hacer. Para ejecutarlo hay que invocar el intérprete de comandos de Java (que se llama java) e

138
indicarle el programa que deseamos interpretar, en este caso PrimeraPalabra. A continuación, el resto de
elementos serán los parámetros para el programa. En el ejemplo se le pasan cuatro palabras: “microsoft”,
“google”, “apple” y “yahoo”. Cada una de estas palabra será un argumento que se guardará en el vector
de objetos String que recibe el método main() de un programa de consola. El vector tendrá longitud 4
y guardará cada una de las 4 cadenas en el orden en que se suministraron, ver Figura 5.11.

args[0] args[1] args[2] args[3]

args "microsoft" "google" "apple" "yahoo"

Figura 5.11: Representación lógica del vector args con los argumentos de la ejecución de la Figura 5.10

PrimeraPalabra.java
1 /** Recibe varias cadenas como parámetros e imprime la primera alfabéticamente
2 * @author los profesores de IP */
3 public class PrimeraPalabra {
4 public static void main(String[ ] args) {
5 if ( args.length > 0 ) {
6 int primera=0;
7 for (int i=1; i<args.length; i++)
8 if (args[primera].compareTo(args[i]) > 0)
9 primera = i;
10 System.out.println(args[primera]);
11 }
12 }
13 }

La implementación del programa no es tan complicada como pudiera parecer. Debemos fijarnos en
los parámetros del método main(): recibe un vector de objetos String llamado args. Lo primero que
hace el programa es comprobar si ha recibido algún argumento. Dado que el parámetro args es un
vector como los estudiados en las primeras secciones del tema, su número de elementos viene dado por
el valor del atributo length. Si es mayor que 0 entonces es que tiene algún parámetro. Todos ellos serán
cadenas guardadas como objetos String. Lo que se hace dentro del if es descubrir cuál de esas cadenas
va antes alfabéticamente. Es un algoritmo similar a descubrir el máximo o el mı́nimo en una secuencia
de números. Inicializamos la variable primera asignándole el valor 0, el ı́ndice de la primera cadena.
Inicialmente asumimos que la cadena que va antes alfabéticamente es la primera. Luego se recorren el
resto de cadenas con un bucle for y si alguna de ellas va antes alfabéticamente, se actualiza el valor
de la variable local primera. En el bucle comparamos la cadena que en esa iteración es considerada la
primera, args[primera], con la cadena que estamos testeando si va antes o no, args[i]. Para saber si
una cadena va antes alfabéticamente que otra se usa el método compareTo() que retorna un valor mayor
que 0 cuando la cadena que se pasa como argumento va antes que el objeto String con el que se llama
al método.

pruebas La primera cosa a probar es ver si el programa funciona dependiendo de la posición donde se encuentre
la palabra que va antes alfabéticamente. Ası́ deberı́amos probar casos donde estuviera la primera
en la secuencia, la última y en una posición del medio. Deberı́amos probar además secuencias con
una palabra, con dos y con más de dos. También que en las pruebas la palabra que va a antes
alfabéticamente empiece por distintas letras: ’a’, ’z’ y otras.

5.9. Matrices y vectores multidimensionales


Hay aplicaciones, especialmente las relacionadas con Cálculo o Álgebra, que requieren representar
vectores de más de una dimensión. El caso más habitual es necesitar trabajar con una matriz. En una

139
matriz no tenemos una única “fila” de elementos como ocurre en los vectores descritos en las secciones
previas, si no que tenemos varias filas, generalmente todas ellas con el mismo número de elementos o
columnas. La Figura 5.12 muestra la representación lógica de una matriz.
Los lenguajes de programación suelen disponer, en su definición, de los mecanismos necesarios para
poder crear y trabajar con matrices. Las matrices pueden verse como vectores de dos dimensiones.
Generalizando este concepto podemos crear también vectores multidimensionales, como el que se muestra
en la Figura 5.16 que se discutirá al final de la sección.

5.9.1. Matrices
Antes de explicar cómo se trabaja con matrices en Java, definamos formalmente qué es una matriz y
veamos cuál es su representación lógica.

definición Matriz: Es una tabla rectangular o cuadrada de elementos ordenados en filas y columnas. Al
elemento que se encuentra en la fila i-ésima y la columna j-ésima se le llama elemento (i,j)-ésimo
de la matriz.

La Figura 5.12 muestra una matriz que tiene 4 filas y 6 columnas. El elemento (i,j)-ésimo de la
matriz, es decir, el que se encuentra en la fila i-ésima, columna j-ésima, es el que aparece sombreado
en la imagen. Tanto la numeración de las filas como la de las columnas sigue el mismo convenio que
hemos estudiado en el caso de los vectores, es decir, empiezan en cero. Cuando se muestre cómo se
representan las matrices en memoria y repasemos el operador corchete aplicado sobre ellas, veremos que
la justificación es exactamente la misma que la explicada para los vectores (ver Sección 5.4.1). El número
9 está en la fila 2, columna 3.

j
1 4 7 0 5 2

5 6 4 1 8 7

i 4 3 7 9 0 2
6 1 3 5 6 8

Figura 5.12: Una matriz de 4 filas y 6 columnas. El elemento de la fila i-ésima columna j-ésima, que
aparece sombreado y tiene valor 9, se encuentra en la fila 2, columna 3

4
! Como en los vectores, la numeración de las filas y las columnas de una matriz empieza en
cero.
De hecho el funcionamiento de las matrices es una simple extensión de los vectores. Si estos son de
una sola dimensión, las matrices son bidimensionales. Eso se indica en la sintaxis de su declaración del
siguiente modo:

sintaxis Declaración y creación de una matriz:


tipo[ ][ ] nombre [= new tipo [filas][columnas] ];

En lugar de poner una sola pareja de corchetes se ponen dos. Como en toda declaración, primero se
escribe el tipo y luego las dos parejas de corchetes indicando que será un vector de dos dimensiones, una
matriz. Si leemos la declaración tipo[][] de derecha a izquierda, una matriz es un vector (corchete más

140
a la derecha) donde el tipo de sus componentes es tipo[], es decir, un vector de vectores, un vector de
elementos vectores cuyas componentes son del tipo indicado. Por ejemplo, las siguientes instrucciones
declaran y crean matrices de distintos tipos, incluyendo tipos básicos y objetos de la clase Fecha.
1 int[ ][ ] m = new int [4][6]; //Matriz 4 filas y 6 col.
2 double[ ][ ] t; //Declaración de la matriz
3 t = new double [31][24]; //Creación: 31x24 reales
4 Fecha p[ ][ ] = new Fecha [3][2]; //Matriz 3x2 objetos Fecha

A la hora de reservar el espacio para la matriz usando el operador new también hay que indicar el
tamaño de las dos dimensiones. Comienza con la palabra reservada new, después el tipo de los elementos
de la matriz y a continuación entre corchetes las dimensiones, primero el número de filas y luego el número
de columnas. En el primer ejemplo la matriz m tiene 4 filas y 6 columnas. Podrı́a servir para guardar la
matriz de la Figura 5.12. No es obligatorio en la declaración crear la matriz con new, es perfectamente
válido y lógico en muchos programas, declarar primero la variable para la matriz y en una sentencia
posterior crear el objeto matriz reservando el espacio con new (como en el ejemplo) o realizando una
asignación de una expresión que genere una matriz, por ejemplo una llamada a un método que retorne
una matriz. Finalmente, las matrices, como ocurre con los vectores, no están restringidas para los tipos
básicos, también se pueden crear matrices de objetos. Cada elemento será un objeto de esa clase, con las
implicaciones que eso tiene y que se expusieron en la Sección 5.7 (hay que crear cada objeto individual con
new). En la última declaración se muestra cómo la pareja de corchetes puede ponerse también después del
identificador de la matriz, de forma análoga a la estudiada cuando se comentó la sintaxis de los vectores.
Igual que ocurrı́a con los vectores unidimensionales, las componentes de una matriz se pueden inicia-
lizar en la propia declaración. La sintaxis es la siguiente:

sintaxis Declaración, creación e inicialización de una matriz:


tipo[ ][ ] nombre = { { lista de valores fila 0 } ,
{ lista de valores fila 1 } ,
...
{ lista de valores fila n } };

Los valores iniciales de las componentes de cada fila van entre llaves para indicar dónde empieza y
dónde acaba cada fila. Por ejemplo, para crear la matriz de la Figura 5.12 podemos usar la siguiente
declaración:
1 //Matriz de 4 filas y 6 columnas, inicializada
2 int[ ][ ] m = { { 1, 4, 7, 0, 5, 2 },
3 { 5, 6, 4, 1, 8, 7 },
4 { 4, 3, 7, 9, 0, 2 },
5 { 6, 1, 3, 5, 6, 8 } };

Primero se abre una llave para indicar el principio de los datos y a continuación los valores de cada
fila encerrados entre llaves y separados por comas. Las filas también se separan entre sı́ con comas. La
inicialización finaliza con una llave, que cierra la llave inicial, y el punto y coma.
4
! Los valores de los elementos de cada fila van separados por comas.
Aunque lo más habitual es que todas las filas tengan la mismas columnas, como ocurre en el ejem-
plo anterior, es posible crear matrices donde las filas tengan un número de elementos diferentes. Esto
puede resultar útil cuando un programa necesita trabajar con matrices triangulares, por ejemplo para
representar una matriz triangular inferior o superior. El motivo es que cada fila es en realidad un vector
independiente.

5.9.2. Representación en memoria de una matriz


Para comprender mejor las matrices, y poder explotar todo su potencial en los programas, es pri-
mordial entender cómo se representan en memoria. Como se apuntó en el apartado anterior, una matriz
es un vector donde cada componente es a su vez un vector. La Figura 5.13 muestra la representación
completa de una matriz de enteros. La variable que representa la matriz, llamada m, se guarda en la Pila

141
Pila Montón
m[0][0]m[0][1]m[0][2]m[0][3]

5 3 6 0
m m[0]
m[1][0]m[1][1]m[1][2]m[1][3]

... m[1] 2 7 1 4
m[2] m[2][0]m[2][1]m[2][2]m[2][3]

4 6 9 5

Figura 5.13: Representación en memoria de una matriz. Es un vector de vectores, cada fila es un vector

y apunta a un vector con tantas componentes como filas tenga la matriz. Cada una de esas componentes,
m[0], m[1] y m[2], son vectores de enteros. Por ejemplo, m[0] apunta al vector {5, 3, 6, 0} que es la fila
0 de la matriz. Para acceder a las componentes de esa primera fila se usará el identificador de su vector,
m[0], seguido de la componente a la que se quiere acceder, por ejemplo, la componente cero de esa fila
cero es m[0][0]. Lo mismo se puede hacer con las otras dos filas m[1] y m[2].
La matriz de la figura está compuesta por 4 vectores, el vector en el que se guardan los vectores de
cada fila (m), y los 3 vectores de las filas (m[0], m[1] y m[2]). Toda matriz tiene tantos vectores como filas,
más un vector adicional para contener todos esos vectores fila. Los distintos vectores fila pueden tener
un número diferente de elementos, por lo que podemos crear matrices no cuadradas, como las matrices
triangulares comentadas anteriormente. En uno de los programas de la Sección 5.9.4 nos aprovecharemos
de esta forma de representación para tratar individualmente los vectores de cada fila.
Es importante observar cómo todos los elementos de una fila están juntos en un vector, pero no
ası́ los elementos de las columnas. Cada componente de una columna está en un vector fila diferente. Por
ejemplo, los elementos de la columna 0, {5, 2, 4}, están en vectores diferentes, el 5 en el vector de la fila
0, m[0], el 2 en el de la 1, m[1] y el 4 en m[2]. Por ese motivo no podremos tratar todas las componentes
de una columna como un vector, hay que utilizar toda la matriz.
4
! Una matriz es un vector de filas, donde cada fila es a su vez un vector.

5.9.3. Uso de una matriz


Una vez explicada la representación en memoria de las matrices, puede resultar más sencillo com-
prender las dos operaciones básicas necesarias para manipular matrices: el operador corchete [ ] para
acceder a las componentes y el atributo length para conocer las dimensiones de la matriz.
La sintaxis del operador corchete con las matrices es la siguiente:

sintaxis Operador [ ] con matrices:


componente: matriz[fila][columna]

vector fila: matriz[fila]

142
1 4 7 0 3 2 9 1 4 6 16 1
5 6 2 1 + 8 4 5 7 = 13 10
10 7 8

9 3 4 8 2 1 3 6 11 4 7 14

Figura 5.14: Tres matrices de 3 filas y 4 columnas. La tercera matriz es la suma de las dos anteriores

Lo más frecuente es realizar accesos a las componentes de la matriz. Para ello debe usarse el operador
corchete dos veces, la primera para indicar la fila (matriz[fila ], que en realidad es un vector), y luego
la columna dentro de ese vector, [columna ]. Juntando ambas cosas, matriz[fila ][columna ]. Los ı́ndices
fila y columna tienen que ser de tipo int, como en todo vector. En la Figura 5.13 aparecen los ı́ndices
que deben emplearse para acceder a cualquier componente de la matriz representada.
Aunque menos frecuente, también se puede acceder a todo un vector fila indicando simplemente
matriz[fila ]. En el programa SumaFilas de la Sección 5.9.4 se hace uso de este tipo de accesos para
tratar individualmente cada vector fila.
Para conocer las dimensiones de una matriz hay que usar el atributo length sobre el vector adecuado.
Para conocer el número de filas y columnas debe emplearse:

No de filas: matriz.length
No de columnas: matriz[fila ].length

El número de filas coincide con el tamaño que tenga el vector que representa toda la matriz. Por
ejemplo, en la matriz m de la Figura 5.13 el número de filas será m.length, que son los vectores fila que
contiene el vector m. El tamaño de las filas, es decir, su número de columnas, se obtiene accediendo al
atributo length de cada fila, por ejemplo, el número de columnas de la fila 0 es m[0].length. Si la matriz
es cuadrada o rectangular todas las filas tienen el mismo número de elementos; podremos saber el número
de columnas de la matriz accediendo al atributo length de cualquiera de las filas, aunque normalmente
se suele utilizar el de la fila 0 ya que es la fila que siempre existe en toda matriz. En el caso de que las
filas de la matriz tengan un número de elementos diferente, habrá que acceder al atributo length de la
fila cuyos elementos queramos tratar.

5.9.4. Programas simples con matrices


Una vez estudiados los operadores y atributos que nos permiten trabajar con matrices, vamos a
realizar un par de programas sencillos que emplean matrices.

Suma de dos matrices

enun- Dadas dos matrices de enteros no vacı́as de la misma dimensión, calcular otra matriz que sea la suma
ciado de ambas e imprimirla en pantalla.

Para sumar dos matrices ambas deben tener las mismas dimensiones, como ocurre en el ejemplo de
la Figura 5.14. La matriz suma también tendrá las mismas dimensiones. Cada una de las componentes
de la matriz suma se corresponderá con la suma de las componentes que se encuentren en esa misma
posición en las dos matrices que se pretenden sumar. Por ejemplo, para obtener la componente de valor
10 que aparece sombreada en la figura, hay que sumar las componentes sombreadas de las dos matrices
que se suman, 6 + 4.
Para poder realizar el programa necesitamos leer las dos matrices de teclado e imprimir la matriz
resultante, tal como aparece descrito en el Algoritmo 5.2. En situaciones como esta, es importante
realizar estas operaciones mediante métodos para mejorar la modularidad del código y evitar duplicarlo.
Por ejemplo, como hay que leer dos matrices debe hacerse un método para leer matrices e invocarlo para

143
leer cada una de las dos matrices. Ese método podrá además reutilizarse en un otros programas que
tengan matrices, como veremos más adelante.

Algoritmo 5.2 Calcular la suma de dos matrices (alto nivel)


Leer matriz1 y matriz2 de teclado
Crear la matriz suma
Calcular las componentes de la matriz suma (bucles anidados)
Imprimir la matriz suma

El código completo del programa es el siguiente:

SumaMatrices.java
1 import java.util.Scanner;

3 /** Dadas dos matrices de enteros calcula su suma


4 * @author los profesores de IP */
5 public class SumaMatrices {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Leemos dos matrices de enteros de teclado
11 int[ ][ ] matriz1 = leeMatriz(teclado);
12 int[ ][ ] matriz2 = leeMatriz(teclado);
13 //Creamos la matriz suma
14 int[ ][ ] suma = new int[matriz1.length][matriz1[0].length];
15 //Calculamos la suma
16 for (int i=0; i<suma.length; i++)
17 for (int j=0; j<suma[i].length; j++)
18 suma[i][j]=matriz1[i][j]+matriz2[i][j];
19 //Imprimimos la matriz con la suma
20 imprimeMatriz(suma);
21 }

23 /** Lee una matriz del objeto Scanner pasado


24 * @param t objeto Scanner del que leer los datos
25 * @return la matriz de reales */
26 public static int[ ][ ] leeMatriz(Scanner t) {
27 System.out.print("No de filas y columnas: ");
28 int filas=t.nextInt();
29 int columnas=t.nextInt();
30 //Reservamos memoria para la matriz
31 int[ ][ ] m = new int[filas][columnas];
32 //Leemos las componentes, recorriendo la matriz
33 System.out.print("Componentes: ");
34 for (int i=0; i<m.length; i++)
35 for (int j=0; j<m[i].length; j++)
36 m[i][j]=t.nextInt();
37 return m;
38 }

40 /** Imprime por pantalla una matriz de enteros


41 * @param m la matriz que se desea imprimir */
42 public static void imprimeMatriz(int[ ][ ] m) {
43 //Imprimimos las componentes
44 for (int i=0; i<m.length; i++) {
45 for (int j=0; j<m[i].length; j++)
46 System.out.printf(" %d ",m[i][j]);
47 System.out.println();
48 }
49 }
50 }

En el programa principal se declaran dos matrices de enteros, llamadas matriz1 y matriz2, que se
inicializan con lo que devuelven dos llamadas consecutivas al método leeMatriz(). A continuación se

144
reserva el espacio para la matriz que guardará la suma usando el operador new. El tamaño de la matriz
suma será exactamente el mismo que el de las otras dos matrices. Dado que el enunciado indica que
ambas serán iguales y no vacı́as (precondición), tomamos las dimensiones de la matriz1, el número de
filas es matriz1.length y el número de columnas matriz1[0].length. El hecho de que las matrices leı́das
no sean vacı́as es lo que nos permite garantizar que al menos tienen una fila y por tanto podemos saber
su número de columnas con el tamaño de esa primera fila. Si la matriz1 pudiera ser vacı́a, la fila 0 no
existirı́a y la expresión matriz1[0].length producirı́a un error en tiempo de ejecución. Aunque no se
diga explı́citamente en el enunciado, cuando se hace referencia al término “matriz”, sin indicar otros
condicionantes, se sobreentiende que es una matriz cuadrada o rectangular donde todas las filas tienen
el mismo número de columnas.
Una vez creada la matriz suma ya podemos calcular sus componentes. Eso se realiza con dos bucles
anidados. El primero recorre las filas, moviéndose desde la fila 0 hasta la fila suma.length-1. En el bucle
interno se recorren las componentes de cada fila, cuyos ı́ndices de columna varı́a entre 0 y suma[i].length-
1. Cada componente suma[i][j] es la suma de las componentes de las matrices matriz1 y matriz2 que
se encuentran en esa misma posición, matriz1[i][j]+matriz2[i][j]. Al finalizar los bucles anidados se
imprime en pantalla la matriz suma.
Además de los bucles que permiten realizar la suma, es fundamental comprender cómo están progra-
mados los métodos auxiliares leeMatriz() e imprimeMatriz(). El primero de ellos recibe un parámetro
de tipo Scanner (será el objeto del que se leerán los elementos de la matriz) y devuelve una matriz de
enteros (int[ ][ ]). En las llamadas al método realizadas en main() se pasa un objeto de la clase Scanner
asociado con el teclado. El método leeMatriz() pide inicialmente el número de filas y columnas y reserva
el espacio necesario para una matriz de enteros de ese tamaño. Posteriormente se leen las componentes de
teclado haciendo un recorrido por filas y columnas mediante dos bucles anidados. Al finalizar se retorna
la matriz creada. Igual que un método puede devolver un entero o un real, también puede retornar un
vector o una matriz.
El método imprimeMatriz() es más sencillo. Recibe como parámetro una matriz de enteros, que
llamaremos m, y no retorna nada, ya que su objetivo es mostrar en consola las componentes de la matriz.
Emplea el mismo recorrido que se utilizó para sumar las matrices o para leerlas de teclado. Primero se
van recorriendo las filas de m y luego sus columnas. Después de acabar de imprimir las componentes de
una fila se escribe un salto de lı́nea para producir una visualización más comprensible.

pruebas En los programas de matrices es fundamental probar con distintos tipos de matrices. En este concreto
deberı́amos usar casos de prueba con matrices cuadradas y rectangulares. Dentro de las cuadradas,
un caso particular serı́a usar matrices con una sola componente (una fila y una columna). Dentro de
las rectangulares, con matrices que tuvieran más filas que columnas y al revés. Con todos esos casos
probaremos que el programa crea bien la matriz y la recorre sin salirse.

Suma de cada fila de una matriz


En el siguiente ejemplo se va a tratar de explotar la forma en la que están representadas las matrices
como vectores de vectores fila.
enun- Dada una matriz no vacı́a de números reales leı́da de teclado, calcular e imprimir en pantalla un vector
ciado donde cada componente represente la suma de los elementos de la fila correspondiente en la matriz.

En la Figura 5.15 aparece un ejemplo del comportamiento deseado del programa. La matriz del
ejemplo tiene tres filas y las sumas de sus componentes son respectivamente 12, 14 y 24. El programa
debe generar un vector con esas tres componentes, tal como se muestra a la derecha de la imagen.
Lo interesante del programa es que nos permite tratar individualmente cada fila de una matriz.
Antes de llegar a ese detalle del programa analicemos el método main(). De nuevo está implementado
usando métodos estáticos auxiliares que realizan cada una de las acciones primordiales. Reutilizamos el
método leeMatriz(), en este caso adaptado para lectura de matrices de reales, y añadimos el método
sumaPorFilas() que recibe la matriz leı́da y devuelve el vector con las sumas de cada fila. El vector
devuelto por el método se guarda en el vector local de elementos float llamado sumas. Finalmente se
imprimen los valores de sumas usando un bucle for sencillo.
Centrándonos en el método sumaPorFilas() es curioso ver que es más simple de lo que la complejidad
del enunciado parece sugerir. Sin embargo, llegar a diseñar esa solución tan sencilla requiere de una total
comprensión de cómo se representan internamente las matrices (Sección 5.9.2). En primer lugar, el método

145
+
1 4 7 0 = 12
5 6 2 1 = 14 12 14 24

9 3 4 8 = 24
Figura 5.15: Esquema del resultado esperado del programa para calcular la suma de cada fila de una
matriz. Dada la matriz se calcula la suma de cada una de sus filas y se guardan en un vector. La
componente 0 del vector se corresponde con la suma de la fila 0, la componente 1 con la de la fila 1, y
ası́ sucesivamente

usa el operador new para crear un vector llamado s con tantas componentes como filas tenga la matriz
m que recibe como parámetro (m.length). Posteriormente se recorren las filas de m y para cada vector
fila m[i] se calcula la suma de sus elementos. El resultado se guarda en la correspondiente componente
de s, s[i]. Para hacer la suma de los vectores fila se emplea otro método auxiliar estático llamado
sumaVector() que recibe un vector de flotantes y retorna un valor float con la suma de los elementos del
vector que recibe. El método simplemente recorre las componentes del vector y va acumulando su suma,
retornándola al finalizar el tratamiento de todos los elementos del vector.
La gracia del programa está en dos aspectos: i) es totalmente modular, cada acción con una cierta
complejidad y entidad se realiza en un método estático auxiliar, y, sobre todo, ii) en cómo se calcula
la suma de cada fila aprovechándonos del hecho de que cada fila de una matriz es un vector y puede
manejarse de manera individual.

pruebas Serı́an similares a los del programa anterior. Hay que usar matrices cuadradas y rectangulares con
distintos tamaños para las filas y las columnas.

SumaFilas.java
1 import java.util.Scanner;

3 /** Dada una matriz de reales calcula la suma de cada fila


4 * @author los profesores de IP */
5 public class SumaFilas {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Leemos la matriz de reales de teclado
11 float[ ][ ] matriz = leeMatriz(teclado);
12 float[ ] sumas= sumaPorFilas(matriz);
13 //Imprimimos la suma de cada fila
14 for (int i=0; i<sumas.length; i++)
15 System.out.printf("\nFila %d: suma %f",i,sumas[i]);
16 }

18 /** Lee una matriz del objeto Scanner pasado


19 * @param t objeto Scanner del que leer los datos
20 * @return la matriz de reales */
21 public static float[ ][ ] leeMatriz(Scanner t) {
22 System.out.print("No de filas y columnas: ");
23 int filas=t.nextInt();
24 int columnas=t.nextInt();
25 //Reservamos memoria para la matriz
26 float[ ][ ] m = new float[filas][columnas];
27 //Leemos las componentes, recorriendo la matriz
28 System.out.print("Componentes: ");

146
29 for (int i=0; i<m.length; i++)
30 for (int j=0; j<m[i].length; j++)
31 m[i][j]=t.nextFloat();
32 return m;
33 }

35 /** Calcula la suma de cada fila de una matriz


36 * @param m la matriz
37 * @return un vector con la suma de cada fila */
38 public static float[ ] sumaPorFilas(float[ ][ ] m) {
39 //Calculamos su suma, lo hacemos sobre un vector
40 float[ ] s = new float[m.length];
41 for (int i=0; i<m.length; i++)
42 s[i]=sumaVector(m[i]);
43 return s;
44 }

46 /** Calcula la suma de los elementos de vector de floats


47 * @param v el vector de float
48 * @return la suma de las componentes de v */
49 public static float sumaVector(float[ ] v) {
50 float suma = 0;
51 for (int i=0; i<v.length; i++)
52 suma+=v[i];
53 return suma;
54 }
55 }

5.9.5. Vectores multidimensionales


No solamente es posible crear vectores unidimesionales o matrices, sino que la sintaxis para declarar
vectores en Java permite definir vectores de todas las dimensiones que queramos. Basta con añadir un
par de corchetes por cada dimensión que tenga el vector multidimiensional que deseemos crear. Hay
que añadirlos tanto al principio, para indicar el tipo, como al reservar su espacio en memoria usando el
operador new. Como esto puede resultar un poco abstracto, vamos a verlo con un ejemplo.
Para definir un vector de tres dimensiones de enteros se podrı́a utilizar la siguiente declaración:
1 int[ ][ ][ ] m = new int [3][4][6];

El vector es de tres dimensiones ya que hemos puesto tres parejas de corchetes en el tipo (int[ ][
][ ]) y también después al hacer new, indicando el tamaño de cada dimensión. Como con las matrices,
si leemos de derecha a izquierda la declaración del tipo (int[ ][ ][ ]), nos indica que es un vector
(primera pareja de corchetes empezando por la derecha) donde cada elemento es una matriz de enteros
(int[ ][ ]). Para crear vectores con más dimensiones simplemente hay que añadir más corchetes, tantas
parejas como dimensiones se requieran.
Por ejemplo, la Figura 5.16 muestra el vector de tres dimensiones creado con esta declaración. Te-
nemos 3 matrices (valor de la primera dimensión en el new) y cada una de ellas es de 4 filas (segunda
dimensión) y 6 columnas (tercera dimensión). Tal como se representa también en la figura, para acceder
a una componente particular hay que utilizar tres veces el operador corchete. El primero de ellos nos da
acceso a la matriz deseada, el segundo a la fila de esa matriz y el último a la columna. Un ejemplo: si
queremos acceder al último elemento (última fila, última columna) de la segunda matriz debemos escribir
m[1][3][5]. Como la numeración de las dimensiones empieza por cero, como siempre, la segunda matriz
tiene ı́ndice 1, la última fila ı́ndice 3 ya que hay 4 filas, y la última columna ı́ndice 5 ya que las matrices
del ejemplo tienen 6 columnas.
En la práctica lo más habitual es usar vectores unidimensionales (como en todos los ejemplos del
principio del tema) o matrices (descritas en los apartados anteriores de esta sección). En alguna aplicación
puede ser necesario usar un vector de tres dimensiones como el de la Figura 5.16 y en muy raras ocasiones
algo más complejo. En esta asignatura usaremos solamente vectores (se sobreentiende, unidimensionales)
y matrices, pero no está de más saber que la sintaxis permite definiciones más complejas que en alguna
ocasión pueden resultar útiles.

147
m[2][0][1]

0 2 5 1 8 2
8 7 6 4 9 5
m[0][1][2] 3 1 8 9 6 7
9 3 7 9 6 7
2 0 6 7 1 4
1 4 7 06 51 23 5 2 3
9 3 7 2 5 0
5 6 4 1 8 7
6 1 3 5 3 9
9 3 7 9 0 2
6 1 3 5 6 8 m[1][3][5]

Figura 5.16: Vector tridimensional. Es un vector donde cada elemento es una matriz

148
Tema 6

Introducción a la Programación
Orientada a Objetos
Objetivos

Ser capaz de diseñar y programar correctamente una clase completa en lenguaje Java.
Conocer la utilidad de los constructores de una clase y saber elegir el conjunto de constructores
adecuados dada la funcionalidad esperada de una clase.
Saber crear métodos sobrecargados.
Comprender el concepto de composición y saber diseñar clases que contengan objetos de otras
clases.
Introducir el concepto de herencia a traves de la Clase Object.

6.1. Introducción
El objetivo de este tema es dar una introducción formal a la Programación Orientada a Objetos
(POO) usando Java. En el Tema 3 se expusieron dos de los conceptos más importantes del paradigma
de la Programación Orientada a Objetos: los conceptos de clase y objeto. Estudiamos cómo diseñar una
clase sencilla y crear, a partir de dicha definición, objetos que exhibieran el comportamiento definido por
la clase. Sin embargo, dado que el objetivo de ese tema era presentar de una forma informal el diseño de
las clases, no se expusieron todas las posibilidades que existen a la hora de crear una clase en Java. En
este tema vamos a tratar de hacerlo. Se presentarán todos los detalles que afectan a la construcción de
una clase.
Antes de comenzar con los contenidos del tema, resumiremos brevemente los elementos fundamentales
de la POO tratados en el Tema 3:

Cuando escribimos el código de una clase estamos definiendo el comportamiento de los objetos de
esa clase.
Las clases constan fundamentalmente de:
1. atributos: los datos que permiten representar los distintos objetos de esa clase, y
2. métodos: que implementan las acciones que los objetos pueden hacer.
Al escribir el código de una clase se ocultan los detalles de implementación (parte privada) y se
proporciona una interfaz (parte pública) que los programadores emplean en sus programas para
usar los objetos de la clase.
A partir de la definición de una o varias clases podemos hacer un programa que empleando objetos
de esas clases realice las tareas para las que está diseñado.

149
Todos esos aspectos son fundamentales para comprender cómo funciona y cuál es la filosofı́a de la
POO. Pero además, desde un punto de vista técnico, debemos tener en cuenta otros detalles para diseñar
clases útiles que puedan ser reutilizadas por nosotros o por otros programadores, y que además sean
correctas y encajen con el estilo de la POO en Java. Son bastantes los detalles técnicos que hay que
dominar y presentarlos todos la vez serı́a difı́cil de asimilar. Por ello seguiremos un enfoque incremental,
iremos introduciendo uno a uno los distintos elementos que hay que considerar e iremos mejorando
sucesivamente las clases que servirán de ejemplo. Al final del tema se describirán varios ejemplos de
clases completas que implementan los conceptos tratados a lo largo del tema.
Durante el tema vamos a ir estudiando los siguientes aspectos:
1. Cómo se inicializan los objetos de una clase (constructores).
2. Cómo se representan las clases y sus objetos, cómo se guardan los métodos y los atributos (elementos
estáticos y no estáticos).
3. Cómo se relacionan las clases entre sı́, por ejemplo, hay objetos que contienen otros objetos para
representarse (composición).
4. Cómo se organizan las clases en Java y la relación jerárquica que existe entre ellas (herencia).
En este tema prestaremos especial atención a los tres primeros elementos de la lista anterior, y muy
especialmente a las relaciones de composición. La herencia y otros conceptos más avanzados de la POO
son el objetivo de la asignatura Metodologı́a de la Programación del segundo cuatrimestre. Sin embargo,
daremos una primera noción del concepto de herencia a través de la descripción de la clase Object: todos
los objetos en Java derivan de una clase base llamada Object. Revisaremos los métodos fundamentales
que proporciona la clase Object y cómo deben ser adaptados cuando creamos una clase nueva.

6.2. Construcción e inicialización de objetos


El primer aspecto importante a la hora de diseñar una clase es dotarla de los elementos necesarios
para inicializar los objetos de la clase. La inicialización es una instrucción fundamental en programación,
tanto cuando se declara una variable de un tipo básico, como cuando se declara un objeto. Una variable
o un objeto mal inicializado es una de las fuentes más habituales de errores en los programas. Vamos a
estudiar los mecanismos para inicializar los objetos de una clase con el siguiente ejemplo:
enun- Modificar la clase Cı́rculo de forma que los objetos de la clase se puedan crear e inicializar de tres formas
ciado distintas:

1. Indicando un valor real para el atributo radio.


2. Indicando otro objeto Cı́rculo, ambos tendrán el mismo valor para el radio.
3. Por defecto: el radio debe valer 1.

Si releyéramos los programas del Tema 3, y en concreto los que empleaban objetos de la clase Cı́rculo,
verı́amos que la inicialización se producı́a mediante dos instrucciones:
9 Cı́rculo c = new Cı́rculo();
14 c.setRadio(teclado.nextDouble());

Eso no nos ocurre cuando usamos objetos de otras clases, por ejemplo la clase String estudiada en
la Sección 5.8.1. Para inicializar un objeto String podemos hacer simplemente:
String s = new String("Hola, Mundo");

y el objeto s se crea e inicializa con la cadena “Hola, Mundo”. Con nuestros objetos queremos conseguir
lo mismo, inicializarlos directamente al crearlos:
15 Cı́rculo c2 = new Cı́rculo(teclado.nextDouble());

La idea de fondo es dotar a las clases que diseñemos de los modos más adecuados para poder inicializar
fácilmente sus objetos de una forma similar a la mostrada con el objeto s de la clase String. La forma
de lograrlo es mediante unos métodos especiales denominados constructores.

150
6.2.1. Constructores
Como vamos a ver inmediatamente, crear un constructor es simplemente escribir un método dentro
de la clase siguiendo varios normas en cuanto a la manera de escribir dicho método. Comencemos con su
definición formal:

definición Constructor: Es un método de una clase cuyo objetivo es permitir la inicialización de los objetos
de esa clase.

La construcción de objetos es el proceso que sirve para garantizar la correcta inicialización de los
objetos de las clases. Es el mecanismo del que disponen los programadores para lograr que los objetos de
sus clases estén correctamente inicializados, de acuerdo con la semántica de cada clase, desde el momento
justo en que los objetos son creados.
Siempre que se declara en Java un objeto de cualquier clase se inicializa invocando un constructor.
Esta última frase es muy importante: todos los objetos que se crean a partir de una clase se inicializan
con un constructor y sólo uno, incluso, como veremos, aunque no existan constructores en la clase. Es la
forma de garantizar que los objetos se inicializan de algún modo.
Los constructores cumplen las siguientes propiedades:

1. Son métodos públicos que se llaman como la clase,

2. no devuelven ningún valor,

3. podemos tener distintas versiones, con distintos parámetros (sobrecarga),

4. su misión es inicializar correctamente los objetos de la clase, y

5. se invocan implı́citamente cuando se crea el objeto con new.

Los constructores son simplemente métodos cuyo nombre es el mismo que el de la clase y que no tienen
valor de retorno. Lo normal en la mayorı́a de las clases es que los constructores estén sobrecargados, es
decir, que tengamos varias versiones diferentes, como se pide en el ejemplo de la clase Cı́rculo. En lo que
diferirán las distintas versiones de los constructores es en el número y tipo de sus parámetros. En la clase
Cı́rculo nos piden crear tres constructores: el primero que reciba un número real, el segundo un objeto
Cı́rculo y el último que no reciba nada y que directamente inicialice el radio a 1. Cuando un método
que hace una determinada tarea, en este caso un constructor que inicializa los objetos, presenta distintas
versiones cada una con distintos parámetros se dice que está sobrecargado. La sobrecarga se estudia en
la Sección 6.7.
4
! Los constructores son los únicos métodos que no tienen tipo de retorno, no se puede poner
nada, tampoco void.
Volviendo a nuestra clase Cı́rculo, y teniendo en cuenta las reglas de lo que es un constructor citadas
con anterioridad, podrı́amos escribir los constructores que se nos piden en el problema del siguiente
modo:

Ejemplo3Cı́rculo/Cı́rculo.java
5 public class Cı́rculo {
...
15 //Constructores
16 /** Constructor por defecto, sin parámetros, radio = 1 */
17 public Cı́rculo() {
18 setRadio(1.0);
19 }

21 /** Constructor copia, un parámetro Cı́rculo, el objeto se inicializa


22 * con los mismos datos que el objeto Cı́rculo pasado como argumento
23 * @param c objeto Cı́rculo, radio=c.radio */
24 public Cı́rculo(Cı́rculo c) {
25 setRadio(c.getRadio());
26 }

151
28 /** Constructor con un parámetro double valor inicial del radio
29 * @param r valor inicial del radio, radio = r */
30 public Cı́rculo(double r) {
31 setRadio(r);
32 }
...
54 }

Como la clase Cı́rculo solamente tiene el atributo radio, la labor de los tres constructores es encargarse
de darle un valor inicial a ese atributo. El primero de ellos, el constructor por defecto, asigna al atributo
radio el valor 1, tal como indicaba el enunciado. Para lograrlo llama al propio método setRadio() de
la clase pasándole como argumento dicho valor. El constructor copia, el que recibe como argumento un
objeto Cı́rculo, inicializa el radio del nuevo objeto con el radio del objeto c. Finalmente el constructor que
tiene un parámetro double usa ese valor como valor inicial para radio. Los tres constructores reutilizan el
método setRadio() lo cuál no es un mal enfoque ya que todos ellos usan la funcionalidad de dicho método
y no duplican su código. Sin embargo, veremos en la Sección 6.6 que existe una forma más estándar de
lograr esto mismo en Java usando el objeto this.

6.2.2. Selección del constructor al usar new


Una vez definidos los constructores en la clase, ya podrı́amos crear objetos e inicializarlos haciendo
uso de ellos. Para lograrlo tenemos que emplear el operador new y pasar los argumentos adecuados con
los que se quiere inicializar cada objeto. La sintaxis completa para la creación de objetos usando new es
la que sigue:

sintaxis Creación de un objeto:


clase nombre = new clase(argumentos)

Por ejemplo, con los constructores realizados en la clase Cı́rculo podrı́amos crear los tres objetos
siguientes:
11 Cı́rculo c1 = new Cı́rculo();
15 Cı́rculo c2 = new Cı́rculo(teclado.nextDouble());
17 Cı́rculo c3 = new Cı́rculo(c2);

El constructor que se ejecutará depende de los argumentos que se indiquen al crear el objeto. En el
ejemplo, el primer objeto se crea sin incluir ningún argumento, luego el constructor que se aplicará será el
que no tiene parámetros, también conocido como constructor por defecto. En la inicialización del objeto
c2 se pasa un valor de tipo double; el constructor invocado será obviamente el que tiene un parámetro
double. Finalmente el objeto c3 se inicializa a partir de c2, con lo que el constructor que casa es el que
tiene un parámetro de la propia clase Cı́rculo. Este constructor se suele denominar constructor copia.
Aunque la regla para determinar el constructor con el que se inicializa un objeto es algo más compleja
(ver Sección 6.7.1), de momento podemos decir que el constructor que se aplica es aquél cuyos parámetros
concuerdan en número y tipo con los argumentos que se suministran al usar el operador new.
4
! Crear un objeto con unos argumentos que no casan con los de ningún constructor de la
clase es un error sintáctico.

6.2.3. Constructor por defecto y por copia


Aunque para una clase dada se pueden crear muchos constructores, cada uno de ellos con un grupo
distinto de parámetros, hay dos constructores muy tı́picos que aparecen en casi cualquier clase. Son el
constructor por defecto y el constructor copia. Los parámetros de ambos son siempre los mismos: el
primero de ellos no tiene, y el segundo tiene un único parámetro que es un objeto de la misma clase. Por
la frecuencia con la que aparecen y por su importancia, vamos a estudiar brevemente sus caracterı́sticas
y la utilidad que tiene cada uno de ellos.

152
Constructor por defecto
El papel del constructor por defecto es generar un estado inicial tı́pico en aquellos objetos para los
que no se especifica una inicialización más concreta. La idea es que si el programador no sabe todavı́a
qué estado debe tener el objeto, es decir, qué valores deben tener sus atributos, pero sin embargo necesita
crear el objeto, éste se deje en un estado inicial correcto con una serie de valores iniciales en sus atributos
que de alguna forma representan algo estándar para la semántica de la clase.
En la clase Cı́rculo hemos decidido por diseño que, cuando no se indique otra cosa, se creará un
objeto de radio unidad. Este objeto puede ser muy tı́pico en aplicaciones que manejen objetos Cı́rculo,
por ejemplo alguna que tenga que ver con Geometrı́a o Trigonometrı́a.
17 public Cı́rculo() {
18 setRadio(1.0);
19 }

Otro ejemplo: si estamos haciendo una clase Punto que represente puntos en un espacio 2D serı́a lógico
que el constructor por defecto inicializara los atributos que representan las coordenadas con el valor 0,
indicando que el objeto Punto está inicialmente situado en el origen de coordenadas.
El constructor por defecto se caracteriza por los siguientes aspectos:

1. no tiene parámetros, y

2. asigna valores iniciales tı́picos a los atributos.

La forma de usar el constructor por defecto es simplemente no indicar ningún tipo de argumento al
crear el nuevo objeto con new:
11 Cı́rculo c1 = new Cı́rculo();

4
! Si se declaran otros constructores y no se incluye el constructor por defecto, entonces no
podrán crearse objetos con la sintaxis clase objeto = new clase();.

Constructor copia
Uno de los aspectos más importantes que se discutió en el Tema 3, ver Sección 3.9.2, es cómo se
produce la asignación entre objetos cuando se usa el operador de asignación. En dicho apartado veı́amos
que la asignación no produce una copia, como ocurre en otros lenguajes, sino que el objeto asignado y el
objeto al que se realiza la asignación acaban refiriéndose tras la asignación al mismo objeto en memoria.
Eso provoca que cambiar uno de ellos implica alterar el otro también. Ese es un comportamiento válido
y deseable en ciertas ocasiones, ya que se ahorra memoria, pero inaceptable en otras. Hay veces que lo
que queremos es crear una copia de un objeto para modificarla después en algún sentido, pero mantener
a la vez el objeto original sin alterar. Una primera forma, y la más fácil y segura de realizar esto, es a
través del constructor copia.
El constructor copia sirve para crear un nuevo objeto que será una copia de otro que ya existe. El
nuevo objeto que se crea será exactamente igual, es decir, tendrá los mismos datos, pero estará en otra
zona de la memoria. Tendremos dos objetos iguales en memoria. La programación del constructor copia
que hemos hecho en la clase Cı́rculo ha sido:
24 public Cı́rculo(Cı́rculo c) {
25 setRadio(c.getRadio());
26 }

Sus caracterı́sticas principales son:

1. tiene como parámetro un objeto de la propia clase,

2. inicializa el nuevo objeto para que sea una copia del objeto pasado como argumento, y

3. tras la creación del nuevo objeto, cualquier modificación en uno de los objetos no afecta al otro.
Es decir, son objetos independientes.

153
El hecho de hacer un new implica que se está creando un nuevo objeto, en una nueva zona de la
memoria. Lo que hace el constructor copia es encargarse de inicializar ese nuevo objeto con los datos de
otro que ya existe. Sus instrucciones serán básicamente siempre las mismas, dar a los atributos del nuevo
objeto los mismos valores que tienen en el objeto que se pasa como argumento.
4
! Se debe emplear el constructor copia en lugar del operador de asignación cuando lo que se
quiere es crear una copia independiente de un objeto ya existente.
El constructor copia es la forma más segura y sencilla de lograr ese propósito. La otra alternativa es
usar el método clone() pero requiere una codificación mucho más compleja sin ventajas claras. De hecho
muchos programadores, entre los que nos incluimos, no lo emplean y prefieren usar la funcionalidad del
constructor copia. El método clone() se discutirá brevemente en la Sección 6.9.

6.2.4. Otros constructores


Además del constructor por defecto y el constructor copia, el programador de una clase puede crear
otros constructores. ¿Cuántos? ¿Cuáles? Son dos preguntas que no tienen una respuesta concluyente. Sin
embargo podemos dar algunas directrices elementales:

No se trata de incluir cuantos más constructores mejor. Si hay muchos constructores puede resultar
confuso o difı́cil para los programadores elegir el constructor adecuado.

Basta con crear los constructores útiles y necesarios. Parte del trabajo del creador de una clase es
decidir cómo pueden inicializarse sus objetos.

El número de constructores y los parámetros de cada uno de ellos dependen totalmente de la clase.
Cada clase presenta unas necesidades propias, hay clases que pueden requerir de un conjunto amplio
de constructores y para otras puede bastar con el constructor por defecto y el constructor copia.

El programador debe pensar en las formas más naturales e intuitivas, desde el punto de vista del
programador que usa la clase, para inicializar los objetos.

No deben hacerse constructores duplicados. Podemos valernos de las conversiones automáticas para
lograr que un solo constructor represente a varios.

Por ejemplo, en el caso de la clase Cı́rculo, además del constructor por defecto y el constructor
copia, no hay más alternativa que el constructor con un parámetro double. Además tiene sentido: un
programador puede querer crear un Cı́rculo cuyo radio no sea 1 (constructor por defecto), ni igual al
radio de otro objeto. Ese es el espacio que cubre el constructor con un parámetro double:

30 public Cı́rculo(double r) {
31 setRadio(r);
32 }

Podrı́amos haber pensado en hacer otro constructor con un parámetro int, pero es totalmente in-
necesario. Una creación de un objeto Cı́rculo con un entero puede hacerse con el constructor con un
parámetro double en virtud de las conversiones automáticas. Este constructor no solamente representa
las inicializaciones que se hagan con un argumento double, sino también las que se hagan con cualquier
argumento de un tipo básico gracias a las conversiones automáticas.

6.2.5. ¿Cómo funciona la creación de objetos en clases sin constructores?


Al principio de la sección hemos dicho que todos los objetos que se crean se inicializan invocando
un constructor. La pregunta evidente que debemos hacernos es ¿cómo es posible lograr eso si hay clases
que no tienen constructores? De hecho ninguna de las clases realizadas en el Tema 3 tenı́a constructores
y con todas ellas creamos objetos. La respuesta es que Java garantiza en esos casos la construcción de
objetos creando un constructor por defecto.
4
! Cuando una clase no tiene ningún constructor, el compilador de Java genera de forma
automática un constructor por defecto.

154
Con ello se garantiza que dichas clases tengan al menos un constructor, y por tanto se pueden crear
objetos con la inicialización por defecto. En algunas ocasiones los programadores se pueden aprovechar
de esta circunstancia y no escribir ningún constructor. Obviamente eso será porque el constructor por
defecto generado por Java cumpla exactamente con lo que la clase requiera. En la práctica eso es poco
frecuente ya que en general se necesitan constructores más elaborados y, sobre todo, diferentes versiones
del constructor, especialmente el constructor copia que permite clonar objetos.
El constructor por defecto generado por el compilador se rige por las siguientes reglas:

Incluye solamente el código necesario para inicializar los atributos de la clase.

Hay dos opciones:

1. Si el atributo tiene un valor inicial en su declaración, entonces el constructor incluye esa


inicialización.
2. Si el atributo no tiene un valor inicial, entonces se asignan los valores iniciales tı́picos según
el tipo de dato:
• enteros y reales: el valor inicial es 0.
• boolean: valdrá false.
• objetos (incluyendo vectores): el valor inicial es null. El valor null indica que el objeto o
vector está sin crear, es decir, no apunta a ningún objeto en memoria.

En realidad, el comportamiento descrito del constructor por defecto generado por el compilador de
Java es el mismo que se comentó en el Tema 5 cuando se discutió el valor por defecto que toman las
componentes de un vector al ser creado (ver Sección 5.2.2). Cuando se crea un nuevo vector los valores
iniciales de sus componentes dependen del tipo: los de un tipo básico se inicializan a 0 (salvo boolean
que es false) y los objetos a null. Lo mismo pasa con los objetos creados en clases que no tienen
constructores, sus atributos se inicializan a 0 sin son de un tipo entero o real, false si es boolean y null
para los atributos que son objetos de otra clase o vectores. Ojo, no ocurre lo mismo con las variables
locales de un método; en ese caso no se produce ninguna inicialización y es el programador el que se
debe encargar de dar un valor inicial correcto a esa variable local de acuerdo con su propósito dentro del
método.
En las clases realizadas en los temas anteriores nos aprovechamos de este funcionamiento para crear
clases que no tuvieran constructores, evitando con ello el tener que explicar en ese momento todos los
detalles que deben conocerse para programar correctamente los constructores de una clase. Por ejemplo,
en la clase Cı́rculo del Tema 3 no hicimos ningún constructor y dado que su atributo radio estaba
declarado como sigue:
6 private double radio;

el constructor por defecto generado hace que el valor inicial del atributo radio sea 0. Dado que no pusimos
un valor inicial al atributo, el constructor generado le da el valor 0 que corresponde a los tipos básicos
excepto boolean. La nueva clase que hemos presentado en este tema inicializa el atributo radio a 1 cuando
se crea un objeto Cı́rculo con una inicialización por defecto. Pero claro, ahora hemos escrito nosotros
el constructor por defecto. Podrı́amos haber logrado el mismo efecto en el Tema 3 simplemente con dar
valor inicial 1 al atributo radio. En ese caso el constructor por defecto generado por el compilador de
Java darı́a ese valor inicial al atributo radio de cualquier objeto Cı́rculo. Todo eso, por supuesto, si no
escribimos ningún constructor, dado que de escribir otro el compilador no generarı́a el constructor por
defecto.
En la clase Temperaturas del Tema 5 sı́ asignamos un valor inicial al atributo grados en su declaración:
6 private int[ ] grados = new int[24];

El constructor por defecto generado crea un vector de 24 componentes para cada objeto Temperaturas.
El valor inicial del atributo grados es una expresión con new. Eso implica que se reserva memoria para un
vector nuevo y se asigna al atributo grados cada vez que se cree un objeto Temperaturas. Si no hubiéramos
puesto ese valor inicial en la declaración del vector grados, como no tenı́amos ningún constructor, entonces
el constructor por defecto generado por el compilador inicializarı́a grados a null y tendrı́amos un grave

155
problema: el vector del objeto no tendrı́a memoria reservada y no podrı́a guardar los datos de las
temperaturas horarias.
4
! Si escribimos una clase sin constructores, la forma de asignar un valor inicial concreto a un
atributo para cada objeto que se cree se consigue dando ese valor inicial en la declaración
del atributo.

Es un funcionamiento bastante intuitivo: el valor inicial de un atributo para todo objeto que se cree
de la clase será el valor inicial que tiene ese atributo en su declaración. Eso nos sirve incluso para reservar
la memoria de un vector, como hicimos en la clase Temperaturas (ver Sección5.4.3).

6.2.6. Valores iniciales de los atributos


A pesar de que por lo comentado en la sección anterior pueda dar la sensación de que los valores
iniciales de los atributos solamente son útiles cuando estamos en una clase sin constructores y en la
que el propio compilador genera el constructor por defecto, la realidad es que los valores iniciales se
aplican siempre, es decir, con cualquier objeto de cualquier clase, tenga dicha clase constructores o no,
y se invoque o no el constructor por defecto generado por el compilador.
Podrı́amos resumir el funcionamiento de los valores iniciales de los atributos en los siguientes términos:

El valor inicial de un atributo, si tiene, es siempre el valor inicial de ese atributo para cualquier
objeto que se declare. Esto parece de perogrullo, pero vamos a intentar aclararlo mejor.

Antes de ejecutarse las sentencias del constructor, los valores iniciales de los atributos del objeto
son los valores iniciales dados en la declaración de los atributos en la clase.

Hay una doble inicialización: primero se asignan los valores iniciales y luego se ejecuta el constructor
aplicable según la sentencia new, que en muchas ocasiones inicializará los atributos del objeto con
valores distintos de los iniciales.

Por ejemplo, si tenemos la clase:


public class ClaseX {
private int número=5; //con valor inicial

public ClaseX(int n) {
//número=5
número=n;
}
}

dado que el atribulo número tiene valor inicial 5, antes de ejecutarse la sentencia del constructor de la
clase, dicho atributo ya vale 5. Luego la sentencia del constructor modifica su valor para darle el valor
del parámetro n. Se produce una doble inicialización.
En ausencia de valores iniciales, se emplean los valores iniciales tı́picos correspondientes al tipo del
atributo (0, false o null). Por ejemplo, si ahora tenemos la clase:
public class ClaseY {
private int número; //sin valor inicial

public ClaseY(int n) {
//número=0
número=n;
}
}

el atributo número no tiene un valor inicial especificado en la declaración. Su valor antes de ejecutarse la
sentencias del constructor será el valor inicial de su tipo (0 ya que es un int).
Aunque un atributo ya tenga, antes de ejecutarse las sentencias del constructor, un valor inicial (el
tı́pico de su tipo o el declarado), lo más correcto es indicar en el constructor el valor que va tomar
dicho atributo. Con ello se aumenta la claridad del código, ya que se especifica claramente en el propio

156
cuerpo del constructor el valor que el atributo va a tener. El programador que lea el constructor no
necesitará revisar la declaración del atributo para ver cuál va a ser su valor inicial.
4
! Es preferible especificar claramente en los constructores el valor que va a tomar cada
atributo.

La única causa para evitar esa doble inicialización, serı́a por motivos de eficiencia. Si tenemos una
clase en la que la eficiencia del código es una prioridad, nos podrı́amos servir de los valores iniciales para
que la construcción de los objetos sea lo más rápida y eficiente posible. Si la eficiencia no es una prioridad
fundamental, entonces es preferible hacer un código más claro e indicar en el cuerpo del constructor el
valor que van a tomar los atributos.
En la práctica lo más habitual es no dar valores iniciales a los atributos, salvo que sean constantes
estáticas. El punto donde se dan los valores iniciales a los atributos son siempre los constructores y lo
normal es dejar las declaraciones de los atributos sin valores iniciales, salvo en las excepciones comentadas
en este apartado.

6.2.7. Tareas de los constructores


Para finalizar las explicaciones sobre los constructores de una clase, vamos a exponer las labores
tı́picas que suelen hacer los constructores. A lo largo de la sección ha quedado claro que su principal
misión es inicializar atributos, lo cuál es cierto, al fin y al cabo el estado de un objeto se refleja a través
del valor que tienen sus atributos. Distintos valores en los atributos representan distintos objetos en
términos de lo que significa lo que el objeto representa. Dos objetos Cı́rculo con distintos valores para
el atributo radio representan cı́rculos diferentes, con distinto tamaño.
Sin embargo, las labores de los constructores van más allá de dar un valor u otro a un atributo. Vamos
a generalizarlas un poco:

La principal misión de un constructor es dar un valor inicial a los atributos.

Cuando los atributos no son de los tipos básicos eso implica más cosas. Dos ejemplos:

1. Si el atributo es un objeto de otra clase (composición) hay que crearlo con new e inicializarlo con
el constructor oportuno. Habrá que elegir entre los constructores de la clase del atributo cuál es
el más oportuno para inicializar el objeto. Por ejemplo, podrı́amos tener como atributo de una
clase un objeto File que permitiera leer datos de un fichero (ver ejemplo en Sección 5.7). En
esa situación, habrı́a que inicializar el objeto File asociándolo con el fichero en disco oportuno
y dejándolo listo para que se lean datos.
2. Si el atributo es un vector habrá que reservar el espacio de memoria suficiente con new e
inicializar sus componentes. En general no basta con crear el vector con new, salvo que las
componentes sean de un tipo básico y nos valga como valor inicial el valor inicial de su tipo.
Si las componentes son a su vez objetos, entonces habrá que crear esos objetos también o
especificar en la documentación de la clase que esa labor la debe hacer el programador que
emplee la clase a través de algún método público de la misma.

Cuando la clase extiende una clase ya existente (herencia), entonces se debe inicializar la parte del
objeto que se hereda de la clase que se está extendiendo. Esta opción se sale de los contenidos de la
asignatura, pero cuando se trabaja con herencia hay una parte de los objetos que se hereda; esa parte
debe ser también inicializada. Se usa la palabra reservada super para elegir con qué constructor se
realiza la inicialización. Esto se verá en la asignatura del segundo cuatrimestre.

La misión de los constructores es adquirir los recursos que necesite el objeto. Eso abarca casos tan
variados como dar simplemente un valor inicial a un atributo de un tipo básico, crear objetos que formen
parte a su vez del nuevo objeto, reservar la memoria dinámica de un vector o abrir un fichero.
4
! Construcción significa creación e inicialización de todos los elementos que posea el objeto.

157
6.3. Destrucción de objetos: el Recolector de basura
Del mismo modo que la construcción de los objetos es el momento en que se adquieren recursos
y se inicializan los elementos del objeto que se está creando, tiene que haber otro instante en el que
esos recursos se liberen y se devuelvan al sistema. Si cuando reservamos espacio en memoria para un
vector o cuando creamos un objeto nuevo, usando en ambos casos el operador new, estamos en realidad
adquiriendo memoria del sistema, tiene que existir el proceso contrario, el momento de su liberación. Al
tratar distintos aspectos en temas anteriores, ya se ha comentado que en las aplicaciones escritas en Java
ese papel corresponde al Recolector de basura (Garbage collector ) de la Máquina Virtual de Java (JVM,
Java Virtual Machine).

definición Recolector de basura: Es el proceso implı́cito que se encarga de liberar la memoria de los objetos
que ya no son usados.

En Java la memoria dinámica se reserva cada vez que se aplica el operador new y creamos un objeto (lo
dicho aquı́ para objetos vale también para vectores, al ser objetos). Las variables con las que se representa
el objeto apuntan a la zona de memoria donde se encuentran los atributos del objeto (ver Sección 3.9.2).
Además, cuando se producen asignaciones entre objetos, o cuando se pasan como parámetros a funciones,
es posible que varias variables apunten al mismo objeto. El control de estas acciones son parte de las
tareas del Recolector de basura:

Se encarga de controlar, para cada objeto que se crea, cuántas variables lo están referenciado. Es
decir, cada vez que una nueva variable apunta a un objeto, el Recolector de basura lo registra.
El Recolector lleva una cuenta de todas las referencias que existen para cada espacio de memoria
reservado (objeto).
Cuando un objeto no está siendo referenciado por ninguna variable, su memoria es candidata a ser
liberada. La idea es devolver esos recursos al sistema para que el conjunto de las aplicaciones que
se están ejecutando los puedan aprovechar.
Cada cierto tiempo se invoca al Recolector de basura que comprueba en la lista de objetos creados
cuáles ya no se usan y libera la memoria que ocupan. Obviamente esta tarea consume un cierto
tiempo, por lo que no se puede hacer continuamente. Cuantas más veces se realice, más eficiente es
la gestión de memoria, en el sentido de que habrá más cantidad de memoria libre. Pero eso tiene
una contrapartida: más ejecuciones del Recolector provocan una ejecución menos eficiente de las
aplicaciones. Cuanto más reiteradas sean esas ejecuciones, más ineficiente será la ejecución de las
aplicaciones.
Hay varias estrategias para decidir cuándo ejecutar el Recolector, por ejemplo:

1. Cuando no quede memoria libre.


2. Cuando la cantidad de memoria libre sea menor que un umbral. Por ejemplo, hay menos de
10Mb de memoria libre.
3. Justo antes de reservar nuevos objetos.
4. A intervalos fijos de tiempo. Lo cual no siempre es posible por la acción del resto de aplicaciones
que se están ejecutando.

El mecanismo más eficiente es el primero, ya que ejecuta el Recolector el menor número de veces
posible. En el resto de casos se puede estar ejecutando el Recolector sin que realmente haga falta más
memoria, ya que queda memoria libre disponible.
El objetivo del Recolector es encargarse de la liberación de memoria, evitando que los programadores
tengan que hacerla explı́citamente como ocurre en otros lenguajes. La alternativa al Recolector de basura
serı́a que los programadores realizasen explı́citamente (mediante operadores o métodos del lenguaje) la
liberación de los objetos. Cada una de las dos alternativas tiene sus ventajas y desventajas. Si las
pensamos desde el punto de vista del Recolector sus ventajas y desventajas son las siguientes:

Ventajas:

158
1. Es un mecanismo libre de errores. Cuando la gestión de la memoria, especialmente en lo
referente a la liberación, se deja en manos de los programadores no se puede garantizar que
esté libre de errores. Los programadores, como es lógico, cometemos errores. Un programador
podrı́a olvidar liberar la memoria de un objeto. Cuando de eso se encarga un programa ya
depurado y sin errores, la probabilidad de una mala gestión es (o deberı́a ser) nula.
2. Es un proceso transparente para el programador, no tiene que ocuparse de realizar la liberación
de memoria y puede centrarse en las tareas de su programa. Labores como la liberación de
memoria pueden ser consideradas tediosas por muchos programadores y eso al final tiene como
consecuencia que algunos de ellos tiendan a no hacerlas o a hacerlas inapropiadamente.

Desventajas:

1. La gestión de la memoria es más ineficiente. Entre ejecuciones consecutivas del Recolector


habrá momentos en los que haya memoria que está ocupada durante un cierto tiempo cuando
en realidad sus objetos no están siendo usados por ninguna aplicación. Cuando la liberación
la realiza el programador, éste puede liberar la memoria de un objeto justo en el momento en
el que deje de usarlo. No puede haber ningún otro mecanismo más eficiente que ése, desde el
punto de vista de la gestión de la memoria.
2. El Recolector de basura consume tiempo de ejecución, cuanto más veces se ejecute, más
ralentiza las aplicaciones. La utilización del Recolector puede ser incompatible con ciertas
aplicaciones o sistemas en tiempo real que requieren una ejecución continua para dar respuestas
en tiempo real.

4
! La liberación de recursos por parte del Recolector es un proceso más seguro pero menos
eficiente que si fuera llevado a cabo por los programadores.

6.3.1. El método finalize()


Aunque en Java el programador no tiene ninguna instrucción u operador para realizar explı́citamente
la destrucción de los objetos, sı́ existe un método que es el contrapunto a los constructores. Es el método
finalize():

public void finalize() {


...
}

Sus caracterı́sticas podrı́amos resumirlas en los siguientes aspectos:

Su misión es realizar las tareas necesarias previas a la destrucción de un objeto.

No implica liberar memoria, se encarga de otras tareas. La liberación de memoria siempre es cosa
del Recolector de basura.

Por ejemplo, si un objeto de una clase realiza operaciones de E/S, antes de destruir el objeto es
conveniente hacer todas las transacciones de E/S pendientes. Normalmente cuando se manejan
ficheros u otras estructuras, las operaciones de escritura no se hacen exactamente en el momento
que la sentencia de escritura se produce. Los datos de entrada y salida se suelen mantener en buffers
para optimizar las operaciones de E/S, sobre un fichero por ejemplo. Para garantizar que finalmente
se realicen fı́sicamente todas esas operaciones pendientes se puede escribir el código necesario en el
método finalize().

El método es invocado (en teorı́a) antes de que el Recolector libere el objeto, por lo que tampoco
el programador tiene control sobre la ejecución del mismo. En realidad no está garantizado que
el método se llegue a ejecutar, porque puede ocurrir que el objeto se libere después de que la
aplicación haya finalizado. De ahı́ que muchos programadores no lo utilicen.

En la práctica no se suele usar. El motivo es que el programador no puede controlar ni cuándo ni


cómo se va a ejecutar, y eso es un gran inconveniente en la programación imperativa.

159
Se puede definir otro método que haga las tareas que deseemos cuando el objeto ya no siga usándose.
Ese método sı́ puede ser invocado exactamente cuando el programador desee.

Muchas programadores evitan y desaconsejan el uso del método finalize(), y prefieren la última
opción comentada: en las clases que lo precisen realmente, realizar un método propio con un nombre dis-
tinto. Ese método se encarga de hacer las tareas finales sobre el objeto cuando el programador esté seguro
que ese objeto va a dejar de usarse. El uso de finalize() y de técnicas como la comentada se salen de
los objetivos de la asignatura. Dado que tampoco manejamos estructuras que hagan operaciones de E/S
complejas, como los ficheros, no necesitaremos nunca programar el método finalize() o uno alternativo
en nuestras clases.

6.4. Representación en memoria de clases y objetos


En esta sección nos vamos a centrar en explicar cómo se representan las clases y objetos, en concreto,
los atributos y los métodos. Ello nos permitirá entender aspectos que hasta ahora hemos pasado por
alto, como el modificador static. Pero antes de entrar en esos detalles, vamos a tratar de exponer las
diferencias conceptuales que existen, desde el punto de vista del paradigma de la Programación Orientada
a Objetos, entre las diferentes clases que hemos usado en los ejemplos de este curso.

6.4.1. Dos tipos de clases


En los programas de ejemplo usados a lo largo de este texto hemos hecho, básicamente, dos tipos de
clases:

1. Clases a partir de las creamos objetos, como la clase Cı́rculo.

2. Clases para hacer aplicaciones de consola.

La diferencia entre ambas clases es sustancial: las clases del primer grupo nos sirven para crear objetos
a partir de los que desarrollar aplicaciones. Del segundo grupo de clases nunca creamos un objeto. Son
clases que nos permiten crear aplicaciones de consola y con ellas probar la funcionalidad, bien de los
objetos de las clases del otro grupo, o de los elementos del lenguajes Java, como los condicionales, bucles
o vectores.
Esto mismo que pasa con las clases que hemos programado como ejemplos, ocurre con las clases que
incluyen los paquetes de Java.

1. Hay clases que nos sirven para crear objetos, como String o Scanner, y

2. otras que solamente nos sirven para usar sus métodos, como Math.

El hecho de hacer clases que luego no van a servir para crear objetos, en cierto modo supone una
deformación de lo que significa el concepto de clase en el paradigma de la POO. La piedra filosofal de este
tipo de programación es definir clases de forma que las aplicaciones se escribirán mediante la cooperación
entre los objetos creados a partir de dichas clases. Definir clases de las que no se van a instanciar objetos
contradice, en parte, la filosofı́a de la POO.
Sin embargo, al ser Java un lenguaje en el que todo debe programarse dentro de clases, hace que
haya determinadas situaciones en las que tener que crear objetos de una clase para realizar ciertas
tareas resultarı́a ineficiente y poco natural. El ejemplo paradigmático es la clase Math. La clase Math nos
proporciona un conjunto de métodos para realizar cálculos matemáticos, como la raı́z cuadrada, sqrt(),
o la potencia, pow(). El problema es que la raı́z cuadrada se calcula con respecto a un número, por
ejemplo, una variable double, y el tipo básico double no es una clase, luego no tiene métodos. Podrı́a
haberse hecho una clase Double1 que incluyese un método sqrt(), de forma que podrı́amos calcular la raı́z
cuadrada de un objeto Double. Sin embargo esa solución serı́a más ineficiente que manejar directamente
variables de los tipos básicos, como double, y aplicar los métodos de la clase Math.
1 De hecho existe una clase Double, igual que existe una clase para el resto de tipos básicos como: Integer,
Char, etc.. Son las clases llamadas de envoltura. Su utilidad se sale de los objetivos de esta asignatura.

160
Situaciones como la descrita, en las que se requieren ciertas funcionalidades que no están ligadas a
ningún objeto, en Java se implementan mediante clases que contienen un grupo de métodos2 , pero que
sin embargo no suelen tener atributos, salvo constantes, ni se crean objetos a partir de ellas. Como pasa
en la clase Math, que sirve para proporcionar un grupo de métodos, pero no se crean objetos. Los métodos
se aplicarán sobre la clase, no sobre objetos concretos.
4
! Hay veces que los elementos de una clase están ligados a la clase, no a los objetos de la
clase.

Esta es una situación común en los dos ejemplos descritos. Tanto en las aplicaciones de consola, en
las que tenemos un método main(), como en clases como Math, los métodos están ligados con la clase y
no se aplican sobre objetos de la clase.
El objetivo de esta sección es explicar cómo lograr hacer eso al programar una clase. En realidad
lo hemos hecho en varios programas: consiste en usar apropiadamente el modificador static al declarar
los métodos de una clase o sus atributos. Antes de presentar un ejemplo de un problema en el que esto
puede ser útil, vamos a exponer cómo se gestionan o representan las clases en memoria, los elementos
que contienen y los objetos que se crean. Para comprender más profundamente lo que estamos haciendo
cuando programamos un método de una clase es necesario entender cómo se guardan en memoria las
clases y sus objetos. Eso facilita la comprensión y el manejo dos elementos importantes dentro de la
programación de una clase, como son el modificador static y el objeto this.

6.4.2. ¿Cómo se mantiene en memoria una clase y sus atributos, métodos y


objetos?
Cualquier programa de ordenador ocupa espacio en memoria para guardar su código y sus datos.
Cuando lo ejecutamos, el código del programa se transfiere desde el disco a la memoria del ordenador
para que pueda ser ejecutado o interpretado. Además, durante la ejecución irá ocupando la memoria
necesaria para los datos que utiliza. Lo mismo pasa con los programas en Java, que siempre son clases.
Las clases de Java también necesitarán ocupar espacio en memoria para representar su código (sus
métodos) y los datos de cada uno de los objetos. Lo que ocurre es que todos los objetos de una misma
clase pueden compartir los métodos, no ası́ los datos. Dos objetos Cı́rculo guardarán valores diferentes
(un radio distinto), pero los métodos para fijar o leer su radio pueden ser, y de hecho son, los mismos.
No tendrı́a sentido que cada objeto tuviera su propia implementación de cada uno de los métodos. Para
ahorrar espacio lo que se hace es que la clase “guarde” la implementación de los métodos y cada objeto
sus propios datos. Además, de acuerdo con la discusión anterior, habrá métodos y atributos que estén
ligados a la clase y no a los objetos; esos elementos también estarán “guardados” en la clase.
La Figura 6.1 muestra la representación de los distintos elementos de que consta una clase, ası́ como
de los objetos que a partir de la clase se pueden instanciar. La clase contiene todos los métodos, tanto los
que trabajan sobre los objetos, como los que están ligados a la clase. Por otro lado, contiene los atributos
asociados con la clase, que de alguna forma comparten todos los objetos de la clase. Estos elementos
suelen ser constantes fundamentalmente. Por ejemplo, en el caso de la clase Cı́rculo la constante PI
está ligada a la clase. Es mucho más eficiente que sea compartida por todos los objetos, residiendo en la
clase, y no formando parte de los atributos que guarda cada objeto (tendrı́amos la constante PI guardada
muchas veces). Finalmente, cada objeto contiene los atributos que permite describir el propio objeto,
en el caso de la clase Cı́rculo el atributo radio. Cada objeto tiene que tener una copia individual del
radio que es lo que de verdad describe un objeto Cı́rculo. La diferencia entre los dos tipos de métodos
existentes es que los métodos estáticos solamente pueden acceder a los atributos asociados con la clase y
nunca a los atributos de los objetos.

6.5. Elementos estáticos


En la esta sección vamos a ver qué se debe hacer al declarar un atributo o un método para que
en lugar de pertenecer a los objetos, estén ligados a la clase en su conjunto. A estos elementos se les
denomina estáticos. Vamos a hacerlo a través del siguiente ejemplo:
2 En otros lenguajes esto se suele denominar librerı́a.

161
Clase
Elementos estáticos

Método #1 Método #m
(Clase) ... (Clase)

Atributos de la clase

Elementos no-estáticos

Método #1 Método #n
(Objetos) ... (Objetos)

Objeto #1 Objeto #k
Atributos Atributos
del objeto ... del objeto

Figura 6.1: Representación en memoria de las clases y los objetos. La clase contiene todos los métodos y
los atributos de la clase. Los métodos pueden estar asociados con la clase (tienen acceso a los atributos
de la clase) o con los objetos (tiene acceso también a los atributos del objeto con el que se llamen). Los
objetos contienen los valores para los atributos que representan dichos objetos

enun- Crear la clase Conversor que permita realizar conversiones entre kilómetros y millas, y entre kilogramos
ciado y libras.

Hay que hacer las siguientes consideraciones:

¿Tiene atributos esta clase? Se podrı́a pensar en que podrı́amos tener los atributos kilómetros,
millas, kilogramos y libras. Pero resultarı́a un poco absurdo, no representarı́a ninguna clase de
objeto, ya que son variables inconexas entre sı́.

¿Qué tiene que ver una cantidad en kilómetros, con otra en kilogramos? Los datos no guardan
relación, es más, no tiene sentido crear atributos para ciertas cantidades, por ejemplo, kilogramos
y libras, cuando a lo mejor al usuario de la clase solamente le interesa hacer conversiones entre
kilómetros y millas. Además, la clase podrı́a tener muchas más conversiones a parte de las in-
dicadas, como de metros cuadrados a pies al cuadrado (área) o de kilómetros por hora a nudos
(velocidad). ¿Definirı́amos también atributos para estas magnitudes que en muchas situaciones no
se usarı́an?

Es preferible seguir un enfoque como el de clase Math.

La clase Conversor va a proporcionar un conjunto de métodos asociados con la clase, y no


va a servir para crear objetos, ni va a guardar las cantidades que convierta.

Para lograrlo necesitamos usar el modificador static.

162
4
! El modificador static indica que un elemento está asociado con la clase, no con los objetos
de la clase.
static es un modificador, como lo son final o public o private, entre los que hemos estudiado.
Sirve para indicar que el elemento que se está declarando pertenece a la clase, no a los objetos. Sus
caracterı́sticas son:

Se añade al declarar un elemento dentro de la clase, ya sea un método o un atributo.

Indica que ese elemento será de la clase.

Atributos estáticos: los objetos no tendrán ese atributo. Sólo existirá una copia del atributo y se
guardará en la clase. Normalmente los atributos estáticos son constantes, mucho más frecuente-
mente que atributos estáticos variables.

Métodos estáticos (diferencias con los no estáticos):

• Solamente pueden acceder a los atributos estáticos. Tanto los métodos estáticos como los no
estáticos se guardan en la clase, como se explicó en el apartado anterior. La diferencia es que
los no estáticos trabajan sobre los atributos del objeto que hace la llamada al método, y los
estáticos sólo pueden trabajan con los atributos de la clase.
• Solamente pueden llamar a otros métodos estáticos. La única excepción a esta regla se darı́a
cuando el método estático reciba un objeto, entonces puede llamar a métodos no estáticos con
ese objeto. Pero en la práctica no tiene mucho sentido que un método estático de una clase
reciba como parámetro un objeto de la propia clase.

En los programas del Tema 5 hicimos muchos programas de consola en los que su método main()
se basaba en usar otros métodos estáticos para hacer las tareas del programa. El método main() en
las aplicaciones de consola que hemos hecho es un método estático, ya que no se emplea creando un
objeto, sino que está asociado a la clase de la aplicación. Al ser un método estático, los métodos a los
que llama que pertenecen a la clase tienen que ser también estáticos. Por eso todos aquellos métodos
auxiliares que programamos, como leerVector() o búsquedaLineal() eran estáticos. Haberlos declarados
como no-static habrı́a sido un error, ya que no hay un objeto sobre el que aplicar ese método no estático.
Volviendo la programa que nos ocupa, vamos a analizar una posible solución para la clase Conversor,
por ejemplo esta implementación:

Conversores/Conversor.java
1 /** Clase para hacer conversiones entre cantidades de
2 * distintas unidades de medida
3 * @author los profesores de IP
4 * @version 1.0 */
5 public class Conversor {

7 //Atributos estáticos
8 /** Constante para convertir de Millas a Km*/
9 public static final double MILLAS_KM = 1.609344;
10 /** Constante para convertir de Libras a Kg*/
11 public static final double LIBRAS_KG = 0.45359237;

13 //Evitar la construcción de objetos


14 /** Para que no se puedan crear objetos de la clase*/
15 private Conversor(){}

17 //Métodos estáticos
18 /** Convierte de millas a kilómetros
19 * @param millas valor en millas
20 * @return retorna el equivalente en kilómetros */
21 public static double millasAKm(double millas) {
22 return millas*MILLAS_KM;
23 }

25 /** Convierte de kilómetros a millas

163
26 * @param km valor en kilómetros
27 * @return retorna el equivalente en millas */
28 public static double kmAMillas(double km) {
29 return km/MILLAS_KM;
30 }

32 /** Convierte de libras a kilogramos


33 * @param libras valor en libras
34 * @return retorna el equivalente en kilogramos */
35 public static double librasAKg(double libras) {
36 return libras*LIBRAS_KG;
37 }

39 /** Convierte de kilogramos a libras


40 * @param kg valor en kilogramos
41 * @return retorna el equivalente en libras */
42 public static double kgALibras(double kg) {
43 return kg/LIBRAS_KG;
44 }
45 }

Lo primero es observar cómo declaramos las dos constantes para hacer las conversiones entre kilóme-
tros y millas y kilogramos y libras. Ambos atributos son públicos (modificador public), constantes (final)
y estáticos (static). El modificador static indica que estarán en la clase, no en los objetos. De hecho la
clase está diseñada para que no haya objetos. Precisamente para evitar la creación de objetos se ha decla-
rado como privado el constructor por defecto (lı́nea 15). Dado que no hay otro constructor, es imposible
crear objetos, ya que no hay constructor para inicializarlos, el que hay es privado y no es accesible fuera
de la clase. Y el compilador no creará el constructor por defecto ya que la clase tiene un constructor
aunque sea privado.
El resto de la implementación de la clase son los métodos que permiten hacer las conversiones deseadas.
Todos ellos son métodos estáticos que acceden únicamente a las dos constantes estáticas de la clase. Los
datos que necesitan se pasan como parámetros y por ello no son necesarios ni objetos, ni sus atributos.
Por último, indicar que la clase Math está programada de un forma similar a nuestra clase Conver-
sor. Tiene constantes estáticas, como los números PI y E, y una colección de métodos estáticos como
floor(), exp() o log(). Es la manera estándar de programar en Java este tipo de clases que proporcionan
funcionalidades que no están asociadas con objetos concretos.

6.5.1. Acceso a elementos estáticos


A la hora de usar la clase hay también una diferencia respecto a las clases que están diseñadas para
instanciar objetos a partir de ellas. Cuando tenemos objetos de una clase, para acceder a los métodos no
estáticos aplicamos el operador punto sobre el objeto e indicamos el método que quiere invocarse. Para
acceder a los elementos estáticos, ya sean atributos o métodos, en lugar de un objeto tenemos que usar
el operador punto pero sobre la clase, siguiendo la siguiente sintaxis:

sintaxis Acceso a los elementos estáticos (operador punto . ):


clase.elemento estático público

Por ejemplo, con la clase Math podemos calcular:


double senoPI=Math.sin(Math.PI); //Seno de la constante PI

que asigna a la variable senoPI el valor que devuelve el método estático sin() de la clase Math cuando el
valor que se le pasa es el de la constante estática PI también declarada en la clase Math. Ambos elementos,
el método y la constante, son públicos y estáticos, por eso son accesibles pero usando el nombre de la
clase con el operador punto para acceder a ellos.
Del mismo modo podemos utilizar la clase Conversor en cualquier programa:

Conversores/Conversores.java
1 import java.util.Scanner;

164
3 /** Ejemplo para probar la clase Conversor
4 * @author los profesores de IP */
5 public class Conversores {

7 public static void main(String[ ] args) {


8 //Objeto Scanner asociado con el teclado
9 Scanner teclado= new Scanner(System.in);
10 //Declaramos varibles para cada cantidad
11 double km,millas,kg,libras;
12 //Variable para leer las opciones del usuario
13 int opción;
14 do {
15 imprimeMenú();
16 opción=teclado.nextInt();
17 switch (opción) {
18 case 1://Km a millas
19 System.out.print("Introduce km: ");
20 km =teclado.nextDouble();
21 millas = Conversor.kmAMillas(km);
22 System.out.printf(" %.2f Km son %.2f millas",km,millas);
23 break;
24 case 2://Millas a km
25 System.out.print("Introduce millas: ");
26 millas = teclado.nextDouble();
27 km = Conversor.millasAKm(millas);
28 System.out.printf(" %.2f millas son %.2f km",millas,km);
29 break;
30 case 3://Kg a libras
31 System.out.print("Introduce kg: ");
32 kg = teclado.nextDouble();
33 libras = Conversor.kgALibras(kg);
34 System.out.printf(" %.2f Kg son %.2f libras",kg,libras);
35 break;
36 case 4://Libras a kg
37 System.out.print("Introduce libras: ");
38 libras = teclado.nextDouble();
39 kg = Conversor.librasAKg(libras);
40 System.out.printf(" %.2f libras son %.2f kg",libras,kg);
41 break;
42 default: //Fin o error
43 if (opción!=0)
44 System.out.println("\nOpción errónea");
45 break;
46 }
47 }
48 while ( opción != 0 );
49 System.out.println("Gracias por usar nuestro conversor");
50 }

52 /** Muestra un menú para elegir la conversión deseada */


53 public static void imprimeMenú() {
54 System.out.println("\nMenu Conversor: ");
55 System.out.println("0 - Salir");
56 System.out.println("1 - Km a Millas");
57 System.out.println("2 - Millas a Km");
58 System.out.println("3 - Kg a Libras");
59 System.out.println("4 - Libras a Kg");
60 System.out.print("Elige una opción: ");
61 }
62 }

El programa muestra un menú para que el usuario seleccione el tipo de conversión que quiere realizar.
Una vez elegida la conversión, se le pide que introduzca el valor de la magnitud que quiere convertir y
se muestra dicha conversión. Por ejemplo, si escoge la opción 1, conversión de kilómetros a millas, el
programa le pide la cantidad en kilómetros y le muestra la conversión a millas. Para eso utiliza el método
kmAMillas() de la clase Conversor. Al ser un método estático, la manera de llamarlo es usando el punto
sobre la clase: Conversor.kmAMillas() y pasándole la cantidad con los kilómetros de los que se quiere
obtener la conversión. El resto de opciones funciona de la misma forma, invocando en cada caso el método

165
estático oportuno de la clase Conversor.

pruebas Para probar la clase Conversor usando este programa de prueba, hay que probar todas las conversiones
programadas en ambas direcciones, por ejemplo, de kilómetros a millas y al revés. También hay que
probar con ciertos valores particulares, por ejemplo, probar a convertir 0, 1 y otros valores que se
consideren especiales para cada una de las conversiones.

6.6. El objeto this


Como se comenta en la Sección 6.4.2 y se muestra en la Figura 6.1, todos los objetos de una cla-
se usan la misma (y única) implementación de los métodos. Esta evidente optimización ocasiona una
pequeña dificultad que debe solventarse: cómo decirle a un método no estático sobre qué objeto debe
desempeñar su tarea. Por ejemplo, si con un objeto de la clase Cı́rculo hacemos una llamada a su método
calculaÁrea() le estamos indicando sobre qué objeto debe hacerlo. Analicemos el siguiente ejemplo:

11 Cı́rculo c1 = new Cı́rculo();


15 Cı́rculo c2 = new Cı́rculo(teclado.nextDouble());
17 Cı́rculo c3 = new Cı́rculo(c2);

19 System.out.printf("c1 área %f\n",c1.calculaÁrea());


20 System.out.printf("c2 área %f\n",c2.calculaÁrea());
21 System.out.printf("c3 área %f\n",c3.calculaÁrea());

En el ejemplo hemos creado tres objetos Cı́rculo: c1, c2 y c3, con lo que siguiendo la representación
de la Figura 6.1 tendremos tres objetos en memoria y la implementación de los métodos en la clase,
junto con la constante PI que habı́amos declarado en la clase Cı́rculo como static. Una representación
gráfica del ejemplo está en la Figura 6.2.
En las llamadas a cualquier método, calculaÁrea() en la figura, mediante el uso del operador punto
le estamos indicando sobre qué objeto debe hacer su trabajo. Por ejemplo, en la lı́nea 19 le indicamos
que debe se calcular el área del objeto c1, no de los objetos c2 y c3. Para que todo esto pueda funcionar
se usa un parámetro oculto en cada método que se llama this.

definición this:Es el parámetro oculto que tienen todos los métodos no estáticos y que se refiere al objeto
que ha invocado el método.

En cada método no estático de cualquier clase hay un primer parámetro implı́cito (invisible) llamado
this que se referirá al objeto que invoca el método. this es un parámetro oculto, que no se hay que
declarar, y que sirve para que el método sepa sobre qué objeto debe ejecutarse. Al efectuar la llamada
this se referirá al objeto que hace dicha llamada.
Volviendo a nuestro ejemplo de la Figura 6.2, si la llamada ha sido c1.calculaÁrea(), this se referirá al
c1 durante la ejecución de calculaÁrea(). Si la llamada se hubiera hecho con c2 o c3, igualmente this
se referirı́a a ellos. Eso es lo que permite que el método funcione con cualquier objeto y pueda acceder a
sus atributos.
4
! Cada vez que en las sentencias de un método aparece un atributo u otro método se reem-
plaza por la misma expresión añadiendo this.
Por ejemplo, en el método getRadio() escribimos
37 public double getRadio() {
38 return radio;
39 }

pero es como escribir implı́citamente


public double getRadio() {
return this.radio;
}

166
Clase Círculo
Elementos estáticos
static final double PI=3.1416

Elementos no-estáticos
Constructores
Círculo() Círculo(double r) Círculo(Círculo c)

this this this

r c

Otros métodos
calculaÁrea() setRadio(double r) getRadio()

this this this

c1.calculaÁrea( )

Objeto c1 Objeto c2 Objeto c3


radio = 0 radio = 5 radio = 5

Figura 6.2: Todos los métodos de la clase tienen un primer parámetro implı́cito qué indica sobre que
objeto se ejecutan. La llamada c1.calculaÁrea() significa que se ejecuta el método calculaÁrea() sobre el
objeto c1: this se referirá al objeto c1

6.6.1. Usos del objeto this


Aunque no siempre es necesario usar explı́citamente el objeto this, hay situaciones en las que es
imprescindible hacerlo. Lo más importante acerca de this es saber que en cualquier método de una
clase se dispone de una variable que hace referencia al objeto que ha hecho la llamada. Siempre que sea
necesario referenciar al objeto en su conjunto se debe usar this.
Hay tres situaciones tı́picas en las que se suele usar explı́citamente this:

1. para invocar a un constructor desde otro,


2. cuando los parámetros de un método ocultan los atributos por llamarse igual, y
3. para hacer algo con todo el objeto o referirnos a él.

En la clase Cı́rculo presentada anteriormente, programamos todos los constructores llamando al


método setRadio() con el valor para el radio apropiado en cada caso. La forma más estándar de hacer esto
es otra y consisten en llamar desde todos los constructores al constructor más genérico, en este ejemplo
al constructor que tiene un parámetro double para el valor del radio. Para llamar a un constructor desde
otro constructor se emplea this con los parámetros del constructor que se quiera invocar. Quedarı́a algo
ası́:

Ejemplo4Cı́rculo/Cı́rculo.java
5 public class Cı́rculo {
...
15 //Constructores
16 /** Constructor por defecto, sin parámetros, radio = 1 */

167
17 public Cı́rculo() {
18 this(1.0);
19 }

21 /** Constructor copia, un parámetro Cı́rculo, el objeto se inicializa


22 * con los mismos datos que el objeto Cı́rculo pasado como argumento
23 * @param c objeto Cı́rculo, radio=c.radio */
24 public Cı́rculo(Cı́rculo c) {
25 this(c.getRadio());
26 }

28 /** Constructor con un parámetro double valor inicial del radio


29 * @param r valor inicial del radio, radio = r */
30 public Cı́rculo(double radio) {
31 this.setRadio(radio);
32 }
...
66 }

4
! La llamada de un constructor a otro usando this debe hacerse en la primera sentencia del
constructor.
Aunque parezca peor solución, ya que primero se llama a un constructor, que a su vez invoca el
constructor que tiene un parámetro double y luego éste llama a setRadio(), en realidad es mejor porque
se canalizan todos los constructores a través de uno de ellos, en este caso el del parámetro double. Si
alguna vez hubiera que cambiar la forma de construir los objetos Cı́rculo solamente habrı́a que tocar este
constructor, ya que los otros trabajan a través de él. Además esa doble llamada, primero al constructor
con un parámetro double y luego a setRadio(), es optimizada por el compilador mediante lo que se
llaman llamadas en lı́nea, de forma que en la práctica es como si todos ellos ejecutaran directamente el
código del método setRadio() con el parámetro oportuno. Eso sı́, es importante recordar que la llamada
al otro constructor debe hacerse obligatoriamente en la primera instrucción del código del constructor.
El segundo uso se produce en los métodos set(). Es habitual que los parámetros de esos métodos
se llamen como los atributos. De esa forma es más intuitivo entender lo que significan, por ejemplo en
las documentaciones generadas con Javadoc. Al dar al parámetro el mismo nombre que tiene el atributo
que se pretende cambiar en el método set(), necesitamos usar this para referirnos al valor del atributo.
Los programadores que escriben de esta forma los métodos set() usando this, es habitual que también
lo hagan en los métodos get() para darle mayor uniformidad al código.
Ejemplo4Cı́rculo/Cı́rculo.java
5 public class Cı́rculo {
...
35 /**Devuelve el valor del radio del objeto Cı́rculo
36 * @return el radio del objeto */
37 public double getRadio() {
38 return this.radio;
39 }
41 /**Cambia el valor del radio del objeto, para que valga r
42 * @param radio nuevo valor para el radio del objeto
43 * @return nada */
44 public void setRadio(double radio) {
45 if ( radio >= 0 ) this.radio=radio;
46 }
...
66 }

En el ejemplo, como el parámetro de setRadio() se llama radio igual que el atributo de los objetos
Cı́rculo, la única forma de acceder a este último es usando this. Si en el código del método simplemente
se pone radio entonces nos referimos el parámetro, no el atributo de la clase. En las situaciones en las que
una variable de un bloque superior (el atributo en el ejemplo) se llama igual que otra de un bloque más
interno (el parámetro), se dice que la última está ocultando a la primera. Cuando se oculta el atributo de
un objeto de una clase, la forma de acceder a él es usando this, como se hace en el ejemplo: this.radio.
El último uso tı́pico de this es cuando queremos referirnos a todo el objeto. Es muy habitual usarlo
en el método equals(), donde antes de comprobar si todos los atributos son iguales, se suele comprobar
si ambos objetos son en realidad el mismo.

168
Ejemplo4Cı́rculo/Cı́rculo.java
5 public class Cı́rculo {
...
54 //Adaptación de los métodos heredados de la clase Object
55 /**Devuelve cierto si dos objetos Cı́rculo son iguales
56 * @param obj el objeto con el que se va a comparar
57 * @return true si el radio de los dos objetos es igual*/
58 @Override
59 public boolean equals(Object obj) {
60 if ( this == obj ) return true;
61 if ( obj instanceof Cı́rculo )
62 return ( this.getRadio() == ((Cı́rculo)obj).getRadio() );
63 else return false;
64 }

66 }

En la clase Cı́rculo hemos programado el método equals(). Este método forma parte de los métodos
que incluye la definición de la clase Object que se estudiará con más detalle en la Sección 6.9. El objetivo
del método es comprobar si dos objetos son iguales. Para ello se deben mirar los datos de todos los
atributos de ambos objetos y comprobar si son los mismos. Dado que esa comprobación puede ser
costosa cuando los objetos comparados tienen muchos atributos, las implementaciones de equals() suele
incluir una sentencia if inicial en la que se comprueba si ambos objetos se refieren al mismo objeto
en memoria. Para ello es necesario usar this y compararlo con el objeto que se pasa como argumento;
si ambas variables son iguales es que se refieren al mismo objeto fı́sico. Esa es la sentencia inicial del
código de la implementación de equals() en la clase Cı́rculo. El resto del código lo comentaremos en la
Sección 6.9.1.

6.7. Métodos sobrecargados


El concepto de sobrecarga aparece en muchos lenguajes de programación, pero no en todos. Java
sı́ permite la sobrecarga de métodos:

definición Sobrecarga: Un método está sobrecargado en Java cuando existen distintas versiones que se dife-
rencian en el número, orden, y/o el tipo de los parámetros.

Es decir, son métodos que tienen todos el mismo nombre. O dicho de otra forma, el mismo método
con varias versiones distintas. Las diferencias entre ellos están en sus parámetros.
Desde el punto de vista de la filosofı́a de la POO, la sobrecarga es la primera forma de polimorfismo
en Java. El polimorfismo es uno de los conceptos claves de la POO y representa la capacidad de los
objetos a reaccionar de diferentes formas. En el caso de la sobrecarga, es la capacidad de hacer cosas
diferentes aplicando el mismo método. El ejemplo más evidente de sobrecarga se da en los constructores:
hay distintas versiones, cada una de ellas se diferencia de las otras en:

el número de parámetros, o

el tipo de los parámetros, o

en el orden de los parámetros.

La idea de la sobrecarga es hacer la misma operación pero con datos diferentes. Esos datos diferentes
serán los parámetros del método sobrecargado. Tiene que haber alguna diferencia en la lista de parámetros
de los métodos sobrecargados. Lo habitual es que se diferencien en el tipo o el número de parámetros,
pero también pueden, aunque sea más infrecuente, diferenciarse por el orden (aunque tenga exactamente
el mismo número de parámetros y del mismo tipo). Lo que tiene que haber es alguna diferencia entre
ellos para que al hacer una llamada se pueda identificar la versión del método que debe ejecutarse. El
ejemplo más claro lo hemos visto en los constructores de las secciones anteriores. En función de la lista de

169
argumentos que se suministre al crear el objeto mediante new, se decide qué constructor va a inicializar
el objeto. Eso mismo que ocurre con los constructores puede pasar con el resto de métodos de la clase.
4
! Si dos métodos sobrecargados se diferencian solamente por el valor de retorno es un error
sintáctico.
El motivo de esto último es que cuando se hace una llamada no siempre está claro qué valor de retorno
se espera. Por ello Java no permite que los métodos sobrecargados se diferencien por el valor de retorno.
Dado que la definición de un método tiene tres elementos: el tipo de retorno, el nombre y los parámetros,
los métodos sobrecargados, que por serlo tienen el mismo nombre, solamente pueden diferenciarse en los
parámetros, ya sea por su número, tipo u orden.
Quizás el ejemplo más usado de método sobrecargado, por la utilidad que tiene, es el método print()
de la clase PrintStream (la clase de System.out). El método se puede utilizar para imprimir cualquier
dato de un tipo básico, ası́ como vectores de tipo char y objetos de las clases String y Object. Dado que,
como veremos, todo objeto en Java es en realidad de la clase Object (ver Sección 6.9), print() sirve para
imprimir cualquier dato. Las distintas versiones del método print() son las siguientes:
public void print(boolean b)
public void print(char c)
public void print(char[ ] s)
public void print(float f)
public void print(double d)
public void print(int i)
public void print(long l)
public void print(Object obj)
public void print(String s)

Cada una de las versiones de print() sirve para implementar la impresión de una variable u objeto
de un tipo diferente. El método printf() es polimórfico, en el sentido que actúa o reacciona de distinta
forma dependiendo del argumento que reciba.
Para programar nuestros propios métodos sobrecargados, sin que sean constructores, vamos a hacer
un ejemplo muy tı́pico de sobrecarga que consiste en hacer varias versiones para el calcular el máximo
de varios números enteros y reales:
enun- Crear la clase Máximos que proporcione métodos para calcular el máximo de:
ciado
1. dos números enteros, y

2. tres números reales.

Dado el enunciado debemos aplicar varios conceptos:

La clase puede implementarse mediante métodos estáticos. Leı́do el enunciado no hay nada que nos
indique que deben existir objetos Máximos.

Necesitamos un método sobrecargado ya que tenemos funcionalidades diferentes para una misma
acción.

Ejemplo1Máximos/Máximos.java
1 /** Clase para calcular máximo entre variables del mismo tipo
2 * @author los profesores de IP
3 * @version 1.0 */
4 public class Máximos {

6 //Evitar la construcción de objetos


7 /** Para que no se puedan crear objetos de la clase*/
8 private Máximos(){}

10 //Métodos estáticos
11 /** Calcula el máximo de 2 enteros
12 * @param v1 primer valor entero
13 * @param v2 segundo valor entero
14 * @return retorna el valor máximo de los dos parámetros */
15 public static int max(int v1, int v2) {

170
16 return ( v1 >= v2 ? v1 : v2 );
17 }

19 /** Calcula el máximo de 3 doubles


20 * @param v1 primer valor double
21 * @param v2 segundo valor double
22 * @param v3 tercer valor double
23 * @return retorna el valor máximo de los tres parámetros */
24 public static double max(double v1, double v2, double v3) {
25 if ( v1 >= v2 && v1 >= v3 ) return v1;
26 else return ( v2 >= v3 ? v2 : v3 );
27 }

29 }

En cada una de las versiones, el método correspondiente devuelve el máximo de los argumentos que
recibe. Obviamente las implementaciones son diferentes, ya que no es lo mismo calcular el máximo de
dos o de tres valores. Lo interesante es que se emplea el mismo nombre para ejecutar cualquiera de los
dos métodos, porque ambos hacen lo mismo pero con distintos datos de partida: calcular el máximo de
ellos.
La sobrecarga es algo inherente no solamente a los métodos, sino también a los operadores. Por
ejemplo, en Java el operador + sirve tanto para sumar variables int, como para concatenar objetos String.
En cada caso se hace una operación diferente, en el primero una suma, en el segundo la concatenación.
El operador + está sobrecargado ya que hace operaciones distintas según el tipo de los operandos. En
otros lenguajes la sobrecarga de operadores se puede extender, es decir, el programador puede definir
cómo se va a comportar un operador en función del tipo de los operandos. En Java eso no es posible y
solamente ocurre, de forma excepcional, con la clase String y el operador +.

6.7.1. Resolución de sobrecarga


La sobrecarga de los métodos hace que deban existir reglas precisas para decidir, sin ninguna am-
bigüedad, qué versión del método sobrecargado debe ejecutarse cuando se realiza una llamada. En la
Sección 3.5.3 se explicaron los aspectos claves para realizar una llamada a un método. Ahora vamos a
revisar las normas allı́ descritas teniendo en cuenta la posibilidad de tener métodos sobrecargados.
Las reglas que se aplican para resolver qué método se debe ejecutar son las siguientes (por orden de
aplicación):

1. Si la lista de argumentos coincide en tipo, orden y número con la lista de parámetros que tenga
una versión del método sobrecargado, entonces se invoca dicho método. Es el caso perfecto, la lista
de argumentos usada en la llamada concuerda perfectamente y sin necesidad de conversiones con
la lista de parámetros del método.
2. Si no hay ninguna versión que case exactamente, entonces se busca una que coincida en número
de parámetros y cuyos argumentos se puedan promover para que concuerden con el tipo de los
parámetros. Aplicando las conversiones automáticas expuestas en la Sección 2.11.2 se tratan de
promover aquellos argumentos que fuera necesario para que la llamada case con alguna versión del
método sobrecargado.
3. Si ninguna de las dos opciones anteriores funciona, entonces se busca si la llamada casa con algún
método que tenga un número variable de argumentos3 , realizando conversiones automáticas si fuera
necesario.

Recalcar que las reglas se aplican siguiendo el orden indicando. Es decir, la segunda regla solamente
se aplica si no se ha encontrado ningún método que case exactamente con la llamada, y del mismo modo
la tercera regla se trata de aplicar si han fallado las dos primeras.
Ejemplo: imaginemos que tenemos las siguientes dos llamadas que usan el método sobrecargado max()
de la clase Máximos:
6 System.out.println(Máximos.max(5,6)); //max(int,int)
7 System.out.println(Máximos.max(4,7,3)); //max(double,double,double)

3 La creación de métodos con un número variable de argumentos no forma parte de los contenidos de la asignatura.

171
En el primer caso es obvio que casa con la versión sobrecargada que tiene dos parámetros enteros.
Sin embargo, el segundo ejemplo es menos intuitivo. Los argumentos que se pasan son tres enteros, pero
no hay ninguna versión del método que tenga tres parámetros enteros. Es decir, no se puede aplicar la
primera de las reglas. Como hay un método que tiene exactamente tres parámetros y que además son
de tipo double, por lo que casan con los argumentos si éstos se promovieran automáticamente de int a
double, entonces se puede aplicar la segunda de las reglas, promover todos los argumentos a double y
ejecutar la versión del método max() que tiene tres parámetros reales.
De cualquier manera, lo lógico es hacer métodos sobrecargados que se diferencien significativamente
en sus parámetros, y al usarlos hacer llamadas, con las conversiones explı́citas oportunas para que no
haya ambigüedades respecto a la versión a ejecutar.

6.8. Relaciones entre clases


Cualquier aplicación, por pequeña que sea, requiere de varias clases para escribirla. Incluso nuestros
pequeños programas usan, además de las clases que nosotros mismos programamos, otras incluidas en
los paquetes de Java, como las clases Scanner o String. Es decir:

Las aplicaciones usan varias clases para poder hacer sus tareas.

Muchas veces esas clases no son independientes entre sı́. Existen dos tipos de relaciones principales
entre clases:

1. Composición: los objetos de una clase pueden estar compuestos por objetos de otras clases.
Es la relación tiene-un. Por ejemplo, un objeto coche tiene un objeto volante, varios objetos
rueda, un objeto motor, etc. Lo mismo ocurre con las clases más sencillas: un objeto Cı́rculo
puede tener un objeto Punto para describir dónde está el centro del cı́rculo que representa.
2. Herencia: una clase puede ser una especialización de otra ya existente heredando su com-
portamiento. Es la relación es-un. Por ejemplo, un humano es un mamı́fero, hace lo mismo
que todos los mamı́feros, pero además tiene rasgos distintivos respecto al resto de mamı́feros.
Trasladando esa idea a las clases y si pensamos en las clases que hemos programado en temas
anteriores, podemos crear una clase Cı́rculoGráfico que extienda la clase Cı́rculo. Un obje-
to Cı́rculoGráfico es un objeto Cı́rculo, heredará todo lo que tiene y puede hacer Cı́rculo,
tendrá un radio y podrá calcular su área, pero además podrá tener nuevos atributos para des-
cribir esa clase de objetos, por ejemplo un color, y podrá hacer más acciones que un objeto
Cı́rculo, por ejemplo un método dibujar().

El hecho de que las clases se relacionen entre sı́, tanto mediante relaciones de composición o de
herencia, presenta muchas ventajas, casi todas relacionadas con la reutilización de código:

Al hacer que una clase use de alguna forma otra clase, bien sea mediante una relación de composición
o de herencia, se está favoreciendo la reutilización del código.

Se reutilizan clases existentes que ya han sido probadas y verificadas.

Esto evita que, al diseñar una nueva clase, el programador tenga que reinventar la rueda.

Muchos beneficios:

1. Agiliza el tiempo de desarrollo,


2. mejora la fiabilidad del software, y
3. reduce costes.

Las formas en la que las clases se relacionan entre sı́ es uno de los aspectos más importantes de la
POO. En el resto de esta sección nos vamos a centrar en analizar las relaciones de composición que se
dan entre clases. La herencia la tocaremos de forma superficial en la sección siguiente, cuando se explique
la clase Object.

172
6.8.1. Composición
Presentaremos las relaciones de composición entre clases mediante un sencillo ejemplo que relacione
dos de las clases discutidas a lo largo del texto: la clase Cı́rculo y la clase Punto.
enun- Modificar la clase Cı́rculo de manera que los objetos guarden además del radio, las coordenadas X e Y
ciado de la posición donde se sitúa el centro del Cı́rculo

Habrı́a varias formas de implementar está extensión:

La primera idea serı́a declarar dos nuevos atributos: X e Y. Guardarı́amos en ellos las dos coordenadas
del centro, suponiendo que estamos trabajando en un espacio de dos dimensiones.

Pero según el enfoque de la POO, el centro es un objeto. Podrı́amos declararlo perfectamente como
un objeto de la clase Punto que programaremos al efecto.

Tendrı́amos que un objeto de la clase Cı́rculo tiene-un objeto de la clase Punto para describir su
centro.

A esa relación se la denomina composición: la clase Cı́rculo está compuesta por la clase Punto.

Al implementar los métodos de la clase tendremos que tener en cuenta dicha relación. Eso afecta
a las acciones que deben hacer y a cómo se deben implementar los métodos de la clase:

1. Constructores: deben crear el objeto Punto.


2. Métodos: se deben basar en la funcionalidad ofrecida por los métodos públicos de la clase
Punto.

definición Composición: Se produce cuando los objetos de una clase tienen entre sus atributos objetos de
otras clases.

Es exactamente lo que ocurre en la clase Cı́rculo que vamos a programar. Los objetos de la clase
Cı́rculo tendrán entre sus atributos un objeto de la clase Punto. La relación entre las clases Cı́rculo y
Punto se representa en notación UML tal como muestra la Figura 6.3. En este ejemplo, un objeto de la
clase Cı́rculo tiene solamente un objeto de la clase Punto, es lo que representa el 1 en la conexión entre
ambas clases.
Centrándonos en la implementación, la primera cosa importante es realizar bien los nuevos construc-
tores de la clase Cı́rculo. La principal diferencia respecto a los estudiados en las Secciones 6.2.1 y 6.6 es
que deben ocuparse de inicializar el atributo centro que es un objeto de la clase Punto. Si los construc-
tores no hicieran nada, dicho atributo quedarı́a inicializado a null siguiendo las normas descritas en la
Sección 6.2.6. La otra parte importante es dotar a la clase Cı́rculo de métodos públicos para acceder a las
coordenadas del atributo centro. Esos métodos se implementarán usando a su vez los métodos públicos
de la clase Punto.
Veamos la implementación (casi) completa:

Ejemplo5Cı́rculo/Cı́rculo.java
1 /** Representa objetos Cı́rculo, extensión de la versión 4
2 * con un campo Punto y con la implementación de métodos de Object
3 * @author los profesores de IP
4 * @version 5.0 */
5 public class Cı́rculo {

7 //Atributos estáticos
8 /**Constante matemática pi */
9 private static final double PI=3.1416;

11 //Atributos
12 /**Objeto Punto para representar el centro*/
13 private Punto centro;
14 /**Valor del radio del objeto Cı́rculo*/
15 private double radio;

173
Círculo Punto
- centro: Punto - x: double
- radio: double - y: double
1
+ Círculo() + Punto()
+ Círculo(Círculo) + Punto(Punto)
+ Círculo(double) + Punto(double, double)
+ Círculo(double,double,double) + getX(): double
+ getRadio(): double + setX(double)
+ setRadio(double) + getY(): double
+ getX(): double + setY(double)
+ setX(double) + toString(): String
+ getY(): double + equals(Object): boolean
+ setY(double)
+ setCentro(double, double)
+ calculaÁrea(): double
+ toString(): String
+ equals(Object): boolean

Figura 6.3: La clase Cı́rculo se relaciona con la clase Punto a través de una relación de composición. Un
objeto Cı́rculo tiene un objeto Punto para describir su centro. La forma de representarlo en notación
UML es la que muestra la imagen, donde el 1 indica que un objeto objeto Cı́rculo tiene un solo objeto
Punto

17 //Constructores
18 /** Constructor por defecto, radio = 1, centro (0,0) */
19 public Cı́rculo() {
20 this(1.0,0.0,0.0);
21 }

23 /** Constructor copia, el objeto Cı́rculo se inicializará con


24 * los mismos datos que el objeto Cı́rculo pasado como argumento
25 * @param c objeto Cı́rculo, radio=c.radio */
26 public Cı́rculo(Cı́rculo c) {
27 this(c.getRadio(),c.getX(),c.getY());
28 }

30 /** Constructor con un parámetro para el valor del radio


31 * @param radio valor inicial del radio */
32 public Cı́rculo(double radio) {
33 this(radio, 0.0, 0.0);
34 }

36 /** Constructor con valor para el radio y para las


37 * coordenadas x e y del centro
38 * @param radio valor inicial del radio
39 * @param x valor inicial de la coordenada x del centro
40 * @param y valor inicial de la coordenada y del centro */
41 public Cı́rculo(double radio, double x, double y) {
42 this.centro=new Punto(x,y);
43 this.setRadio(radio);
44 }

46 //Métodos públicos
47 /**Devuelve el valor del radio del objeto Cı́rculo
48 * @return el radio del objeto */
49 public double getRadio() {
50 return this.radio;
51 }

53 /**Cambia el valor del radio del objeto, para que valga r


54 * @param radio nuevo valor para el radio del objeto

174
55 * @return nada */
56 public void setRadio(double radio) {
57 if ( radio >= 0 ) this.radio=radio;
58 }

60 /** Para obtener el valor de la coordenada x del centro


61 * @return el valor de la coordenada x del centro */
62 public double getX() {
63 //usamos el método getX de la clase Punto
64 return this.centro.getX();
65 }

67 /** Para obtener el valor de la coordenada y del centro


68 * @return el valor de la coordenada y del centro */
69 public double getY() {
70 return this.centro.getY();
71 }

73 /** Para cambiar el valor de la coordenada x del centro


74 * @param x nuevo valor de la coordenada x del centro */
75 public void setX(double x) {
76 //usamos el método setX de la clase Punto
77 this.centro.setX(x);
78 }

80 /** Para cambiar el valor de la coordenada y del centro


81 * @param y nuevo valor de la coordenada y del centro */
82 public void setY(double y) {
83 this.centro.setY(y);
84 }

86 /** Para cambiar las coordenadas del centro


87 * @param x nuevo valor de la coordenada x del centro
88 * @param y nuevo valor de la coordenada y del centro */
89 public void setCentro(double x, double y) {
90 this.setX(x);
91 this.setY(y);
92 }

94 /**Devuelve el área del objeto Cı́rculo


95 * @return el valor del área del Cı́rculo*/
96 public double calculaÁrea() {
97 return PI*this.getRadio()*this.getRadio();
98 }
...
122 }

La clase tiene cuatro constructores. El constructor por defecto crea un Cı́rculo de radio unidad
situado en el origen de coordenadas. Es muy parecido al constructor con un parámetro double, pero en
éste el valor inicial del radio dependerá del argumento que reciba. El constructor copia, como siempre,
crea un nuevo objeto Cı́rculo que será igual que el objeto Cı́rculo que recibe. Los tres utilizan this para
invocar, según se explica en la Sección 6.6, al constructor más general de todos y que tiene tres parámetros
de tipo double. Es el más interesante de todos, ya que es realmente donde se crea el objeto Punto que
describe el centro. Es necesario que se cree el objeto ya que si no el valor del atributo centro serı́a null
y el objeto Cı́rculo no estarı́a convenientemente inicializado. Al crear el objeto Punto se le pasan los
valores de las dos componentes, usando el constructor de la clase Punto que tenı́a dos argumentos de tipo
double.
En cuanto a los métodos, se han añadido los métodos set() y get() necesarios para trabajar con
las coordenadas X e Y del centro. Nótese que realmente dichos atributos no existen, o al menos no bajo
ese nombre. Las coordenadas estarán guardadas en el objeto Punto que representa el centro y la forma
real en que estén guardadas depende de la implementación de la clase Punto. Lo interesante de esos
métodos es que el usuario programador que emplee la clase entenderá de una forma muy intuitiva lo que
significan los métodos setX(), getX(), setY() y getY() aunque los atributos no existan. La forma en la
que está implementada la clase es irrelevante para el programador que la emplea, lo importante es que
tenga las funcionalidades necesarias (métodos) para que le resulte útil.
Los métodos setX() y setY() se han implementado usando los métodos del mismo nombre de la clase

175
Punto. Para invocarlos se usa el atributo centro que es un objeto de la clase Punto. De la misma manera
los métodos getX() y getY() se han escrito usando los métodos getX() y getY() de la clase Punto. Es
decir, la clase Cı́rculo se escribe aprovechando el código que proporciona la clase Punto. Es una de las
ventajas de la composición: se reutiliza el código de clases ya probadas para producir una nueva clase.
Nótese además que ni siquiera necesitamos saber cómo se por dentro la clase Punto porque en todo
momento nos basamos en usar sus métodos públicos.

6.9. La clase Object: concepto de Herencia


Aunque el concepto de herencia se estudiará en la asignatura del segundo cuatrimestre, introduciremos
aquı́ brevemente la idea de herencia y mostraremos su funcionamiento a través de la clase Object. La
herencia es una herramienta muy potente para añadir nuevas funcionalidades a las que proporciona
una determinada clase. Como se comentó antes, la herencia es otra forma distinta a la composición de
reutilizar código.

definición Herencia: Es la relación jerárquica que se establece entre una nueva clase y otra ya existente, y
que permite que la nueva clase herede los atributos y métodos de la otra clase.

La nueva clase (subclase) extiende la funcionalidad de la clase de la que hereda (superclase). Como
en ocurrirı́a en el ejemplo puesto con anterioridad: la clase Cı́rculoGráfico extiende la funcionalidad
de la clase Cı́rculo; además de poder hacer lo mismo que un Cı́rculo, por ejemplo calcular su área,
los objetos de la nueva clase pueden representarse gráficamente.

La subclase puede modificar el comportamiento de su superclase:

1. añadiendo nuevos atributos y métodos, o


2. sobrescribiendo los atributos o métodos que hereda.

En Java todas las clases que se crean heredan directa o indirectamente de la clase Object. Cuando
escribimos la clase Cı́rculo, dado que no indicamos ninguna superclase, entonces hereda directamente de
la clase Object. Eso es lo que aparece representando en notación UML en la Figura 6.4. Esa relación de
herencia supone que los objetos de la clase Cı́rculo heredan los métodos de la clase Object4 .
Por tanto, todos los objetos en Java tienen los métodos que están declarados en la clase Object.
Algunos de ellos se han discutido brevemente a lo largo del texto, como es el caso de equals() o fi-
nalize(). La Tabla 6.1 muestra una breve indicación con la funcionalidad de cada uno de los métodos
de la clase Object. En las próximas secciones revisaremos dos de ellos que deben ser reprogramados en
muchas de las clases que se crean. La nueva clase debe escribir una versión de ese método, a ese proceso
se le denomina sobrescribir el método. De esta forma cuando se invoque el método con un objeto de la
nueva clase nunca se llamará al método heredado de la clase Object, sino a la nueva versión del método
implementado en la nueva clase.

6.9.1. Método equals()


El método equals() sirve para comprobar si dos objetos son iguales. Su objetivo es proporcionar un
mecanismo adicional a la escasa funcionalidad que proporciona el operador de comparación == cuando
se comparan objetos y que se discutió en la Sección 4.3.4. El operador == solamente devuelve el valor
true cuando los dos objetos que se comparan referencian al mismo objeto en memoria.
La implementación del método en la clase Object tiene un único parámetro que es un objeto de la
clase Object. Compara el objeto que se recibe con el que invoque el método. La implementación aportada
por la clase Object se limita a retornar lo mismo que devuelve el operador == aplicado sobre ambos
objetos. Lógicamente las nuevas clases deben reescribir el método para que realmente se comparen los
objetos, de forma que devuelva true, no solamente cuando referencien al mismo objeto en memoria, sino
también cuando los objetos tengan el mismo valor en cada atributo. Es decir, cuando representen, dentro
de lo que significa la clase, el mismo objeto.
4 No se hereda ningún atributo ya que la clase Object no tiene atributos.

176
Object

+ boolean equals(Object)
+ String toString()
+ int hasCode()
+ ...

Círculo
- centro: Punto
- radio: double
+ Círculo()
+ Círculo(Círculo)
+ Círculo(double)
+ ...

Figura 6.4: La clase Cı́rculo hereda, como toda clase en Java, de la clase Object. La forma de
representarlo en notación UML es la que muestra la imagen

La primera dificultad surge por la definición del método que indica que el parámetro debe ser un
objeto de la clase Object. Si se quiere sobrescribir el método, de forma que siempre se llame al método
proporcionado en la nueva clase y nunca al de la clase Object, el nuevo método debe tener también un
parámetro de la clase Object. Se podrı́a realizar con un parámetro de la nueva clase, como se hizo en la
Sección 4.3.4, pero es menos correcto ya que en ese caso la clase tendrá el método equals() sobrecargado
con dos versiones, una de ellas, la heredada de la clase Object, con escasa utilidad real y potencialmente
peligrosa.
Para poder sobrescribir correctamente el método equals() en las nuevas clases que creemos necesita-
mos usar dos cosas:

El operador instanceof: devuelve true cuando el objeto indicado como primer operando es de la
clase indicada como segundo operando. Sirve para comprobar que un objeto es una instancia de
una clase concreta.
Convertir mediante una conversión explı́cita el parámetro Object a un objeto de la clase que estemos
programando. Eso nos permitirá poder acceder a los elementos de la clase (métodos y atributos) y
con ello comparar ambos objetos.

La implementación del método equals para la clase Cı́rculo presentada en la sección anterior (con el
atributo centro) podrı́a ser la siguiente:
104 @Override
105 public boolean equals(Object obj) {
106 if ( this == obj ) return true;
107 if ( obj instanceof Cı́rculo ) {
108 Cı́rculo c = (Cı́rculo) obj;
109 return ( this.centro.equals(c.centro) &&
110 this.getRadio() == c.getRadio() );
111 }
112 else return false;
113 }

Varios aspectos interesantes en la implementación. Lo primero es la etiqueta @Override, su utilidad


es verificar que el método que se sobrescribe tiene la misma definición (tipo de retorno, nombre y lista
de parámetros) que la versión heredada. Si no fuera ası́ el compilador nos indicarı́a un error sintáctico.
La implementación tiene un primer if que comprueba si ambos objetos referencian el mismo objeto

177
Tabla 6.1: Métodos de la clase Object. Para una obtener una descripción más detallada, consúltese la
documentación de la clase Object

Método Descripción
boolean equals(Object) Devuelve cierto si los dos objetos que compara son iguales.
String toString() Retorna una representación en forma de String con la información que
almacena el objeto.
int hashCode() Devuelve un código entero que representa al objeto. Si dos objetos de
la clase tienen códigos distintos es que son diferentes (lo contrario no
siempre es cierto).
Object clone() Produce y devuelve un nuevo objeto que es una copia del objeto con el
que se invoca el método.
void finalize() Incluye las tareas que deben hacerse antes de que el objeto se destruya.
Class getClass() Retorna una objeto de la clase Class con información sobre la clase del
objeto.
void notify()
void notifyAll()
void wait() Tienen que ver con tareas para programas multi-hilo.
void wait(long)
void wait(long , int)

en memoria; si lo hacen devuelve true. Es la funcionalidad del operador ==. La siguiente sentencia
condicional comprueba mediante el operador instanceof si el objeto pasado como argumento es de la
clase. Si no lo es devolverá false. Cuando el objeto es de la misma clase, se convierte a la clase Cı́rculo
mediante una conversión explı́cita y se comprueba que el contenido de los atributos en ambos objetos sea
el mismo. En este caso se comparan los valores en ambos objetos de los atributos radio y centro; para
este último usando el método equals() de la clase Punto cuya implementación se muestra a continuación.
En la clase Punto la estructura del método equals() es equivalente, la única diferencia es que tiene en
este caso tiene que comprobar la igualdad entre las coordenadas de dos objetos Punto.
67 @Override
68 public boolean equals(Object obj) {
69 if ( this == obj ) return true;
70 if ( obj instanceof Punto) {
71 Punto p = (Punto)obj; //Convertimos el objeto a Punto
72 //Retorna true si las dos componentes son iguales
73 return ( this.getX()==p.getX() && this.getY()==p.getY() );
74 }
75 else return false;
76 }

6.9.2. Método toString()


El objetivo del método toString() es retornar una representación textual, en un objeto String, del
contenido del objeto con el que se invoque. La importancia del método es que aplicará siempre que un
objeto deba ser tratado como un String. Es decir, no solamente se puede llamar al método de forma
explı́cita, sino que también se invoca de forma implı́cita en dos situaciones:

cuando el objeto se imprime como una cadena (modificador %s), o

cuando el objeto se concatena mediante el operador + con un String.

Por ejemplo, se puede imprimir directamente un objeto de la clase Cı́rculo usando el modificador %s
con printf() y pasando simplemente el propio objeto, sin necesidad de llamar al método toString():
11 Cı́rculo c1 = new Cı́rculo();
19 System.out.printf("\nc1 %s", c1);

178
La implementación del método toString() es mucho más sencilla ya que no tiene ninguna complejidad
en lo referente a los parámetros. El método no tiene parámetros, tiene que devolver un objeto String y
normalmente se basa en usar dos elementos:

El operador + para concatenar objetos String con el valor de los atributos, aunque sean de otros
tipos. Si el atributo es de otra clase y se concatena se aplicará implı́citamente su método toString().

El método estático format() de la clase String. La ventaja que tendremos con este método es que
funciona como el printf(), con los mismos parámetros, el mismo significado y el mismo funcio-
namiento. La única diferencia es que en lugar de imprimir, por ejemplo en la consola, devuelve la
cadena que se genera en un objeto String, que es justo lo que necesitamos.

La implementación de toString() en la nuestra clase Cı́rculo es la siguiente:


115 /** Convierte a String la información del objeto Cı́rculo
116 * @return un String con la información del objeto */
117 @Override
118 public String toString() {
119 return String.format(" %s radio %.2f",
120 this.centro, this.getRadio());
121 }

La implementación realizada se basa en usar el método estático format() de la clase String. Como
todo método estático lo llamamos poniendo primero el nombre de la clase y después el del método
(ver Sección 6.5). Funciona exactamente igual que printf(): primero le pasamos la cadena con el texto
y la información para formatear los datos, y después los dos atributos que queremos imprimir. Para
imprimir el atributo centro usamos el método toString() de la clase Punto. No es necesario invocarlo
explı́citamente, basta con usar el modificador %s y pasar directamente el objeto. También se podrı́a
llamar explı́citamente a toString() pero es más habitual (y cómodo) no hacerlo.
Para finalizar vamos a ver la implementación de toString() en la clase Punto:
57 /**Conversión a String del punto
58 * @return un string con las coordenadas del punto */
59 @Override
60 public String toString() {
61 return String.format("( %.2f, %.2f)",this.getX(),this.getY());
62 }

En este caso lo que se genera es una cadena con la información de las dos coordenadas del objeto
Punto con el que se invoque el método. Para hacerlo
se emplea de nuevo el método estático format de la
clase String, obteniendo el valor de las coordenadas
mediante los métodos get() de la clase Punto.

6.10. Ejemplos de clases completas


En esta sección vamos a estudiar un par de clases completas que servirán de ejemplo para mostrar
la utilidad de los conceptos discutidos a lo largo del tema. Cada una de ellas cubre aspectos diferentes y
nos permitirán, además, introducir algunos detalles nuevos de una manera práctica.

6.10.1. La clase Diccionario


La primera clase que vamos a estudiar será la clase Diccionario que surge a partir del siguiente
enunciado:
enun- Realizar una clase que permita representar y trabajar con un diccionario de palabras.
ciado
Lo interesante del ejemplo es que podemos mostrar una clase que presenta una relación de composición
que requiere de un vector de objetos: un diccionario está formado por la definición de muchas palabras.
Es decir, en este caso en la relación de composición no tendremos un solo objeto, como pasaba en la
relación que se daba entre la clase Cı́rculo y la clase Punto (ver Sección 6.8.1), sino que tendremos varios
objetos de una clase presentes en la otra. El ejemplo se caracteriza por:

Relación de composición con varios objetos.

179
Necesitaremos un vector para representarlos.

Crearemos otra clase, llamada Definición, de manera que la clase Diccionario estará compuesta por
un conjunto indeterminado de objetos Definición. La Figura 6.5 muestra dicha relación de composición.
Uno de los aspectos claves de la implementación de la clase Diccionario será la creación de los objetos
Definición.

Diccionario Definición
- palabras: Definición[] - palabra: String
0..n - definiciones: String[]
- posiciónPalabra(String):int + Definición(Definición)
+ Diccionario() + Definición(String,String[])
+ añadePalabra(String,String) + setPalabra(String)
+ buscaPalabra(String):String[] + getPalabra(): String
+ imprimeDiccionario(PrintStream) + setDefiniciones(String[])
+ getDefiniciones(): String[]
+ añadeDefinición(String)
+ toString(): String

Figura 6.5: UML - Clase Diccionario. Tiene una relación de composición con la clase Definición.
Un objeto Diccionario podrá tener cualquier número de objetos Definición, desde 0 hasta un
número indeterminado n.

La clase podrı́a ser muy compleja ya que realizar en profundidad todas las tareas que puede requerir
un diccionario podrı́a llevarnos a incluir muchos métodos, sin embargo, eso extenderı́a innecesariamente
la implementación sin aportar detalles nuevos. Vamos a realizar una implementación bastante simple, tal
como muestra la especificación de la Figura 6.5, con la única dificultad de permitir que una misma palabra
pueda tener varias acepciones diferentes. Los objetos de la clase Diccionario serán lo suficientemente
potentes como para poder representar prácticamente un diccionario real. La limitación más destacable
es que una vez añadida una acepción al Diccionario nunca se podrá modificar, se podrán añadir, eso sı́,
definiciones nuevas para una palabra ya existente.
Dado que la clase Diccionario se basa en la clase Definición vamos a empezar por esta última:

Ejemplo1Diccionario/Definición.java
1 /** Representa un objeto Definición, se usa en la clase Diccionario
2 * @author los profesores de IP
3 * @version 1.0 */
4 public class Definición {

6 //Atributos
7 /** Atributo para representar la palabra */
8 private String palabra;
9 /** Definiciones de esa palabra */
10 private String[ ] definiciones;

12 //Constructores
13 /** Constructor copia, crea una nueva definición a partir de otra
14 * @param d objeto Definición del que hacer una copia */
15 Definición(Definición d) {
16 this(d.getPalabra(),d.getDefiniciones());
17 }

19 /** Crea una nueva definición


20 * @param palabra la palabra a definir
21 * @param definición el conjunto de sus definiciones */
22 Definición(String palabra, String[ ] definiciones) {
23 this.setPalabra(palabra);
24 this.setDefiniciones(definiciones);
25 }

180
27 //Métodos públicos
28 /** Devuelve el conjunto de definiciones de esa palabra
29 * @return un vector de String con las definicioens de la palabra */
30 public String[ ] getDefiniciones() {
31 return this.definiciones;
32 }

34 /** Cambia el conjunto de definiciones


35 * @param definiciones nueva definiciones para la palabra */
36 public void setDefiniciones(String[ ] definiciones) {
37 this.definiciones=new String[definiciones.length];
38 for (int i=0; i<definiciones.length;i++)
39 this.definiciones[i]=definiciones[i];
40 }

42 /** Devuelve la cadena con la palabra


43 * @return el valor de la palabra */
44 public String getPalabra() {
45 return this.palabra;
46 }

48 /** Cambia el atributo palabra


49 * @param palabra nuevo valor para la palabra */
50 public void setPalabra(String palabra) {
51 this.palabra=palabra;
52 }

54 /** Añade una nueva definición al vector de definiciones


55 * @param definición La nueva definición a añadir */
56 public void añadeDefinición(String definición) {
57 String[ ] tmp = this.definiciones;
58 this.definiciones=new String[tmp.length+1];
59 for (int i=0; i<tmp.length;i++)
60 this.definiciones[i]=tmp[i];
61 this.definiciones[this.definiciones.length-1] = definición;
62 }

64 //Adaptación de los métodos heredados de la clase Object


65 /**Conversión a String de la Definición
66 * @return un string con la Definición */
67 @Override
68 public String toString() {
69 String s = String.format(" %s: ", this.getPalabra());
70 for (int i=0; i<this.definiciones.length;i++)
71 s = s + String.format("\n\t# %d %s",i+1,this.definiciones[i]);
72 return s;
73 }
74 }

Los atributos son dos: un String llamado palabra para guardar la palabra de la guardaremos su defi-
nición, y el vector definiciones de objetos String para representar las distintas acepciones o definiciones
que puede tener la palabra. Aunque no lo representamos en la Figura 6.5, también la clase Definición
presenta un relación de composición: un objeto Definición estará compuesto al menos por dos objetos
String.
En cuanto a los constructores, hemos prescindido del constructor por defecto ya que en esta clase no
hay valores por defecto que tengan sentido y lo más natural es crear un objeto Definición que tenga
valores concretos asignados en sus atributos. Hemos implementado tan solo el constructor copia y otro
que reciba un objeto String con la palabra y un vector de objetos String con las definiciones. Como
hicimos en clases anteriores, el constructor por copia invoca al otro constructor. Éste a su vez utiliza los
métodos setPalabra() y setDefiniciones() para fijar el valor, respectivamente, de los atributos palabra
y definiciones.
Los métodos set() y get() son los tı́picos, se limitan a cambiar o devolver el valor de sus respectivos
atributos. El único de ellos que plantea una cierta dificultad es el método setDefiniciones() ya que se
encarga de crear un nuevo vector con el conjunto de definiciones.
La clase cuenta además con el método añadeDefinición() que sirve para añadir una nueva acepción.
Como los vectores no se puede redimensionar, necesitamos crear uno nuevo con una componente más y

181
copiar en sus posiciones los objetos String que contuviera previamente el atributo definiciones. Aunque
pueda parecer una operación costosa, no lo es tanto ya que al ser todos los objetos referencias la copia
es realmente rápida. En la última posición del vector definiciones se copia el objeto String que tiene
como parámetro el método. Un aspecto muy importante de la implementación de la clase es que, dado
que los objetos String son inmutables, nunca hace falta que los métodos creen copias de los argumentos
String que reciben, es suficiente (y muy eficiente) con que los asignen directamente.
Finalmente, dado que nos interesará imprimir el objeto Diccionario, y por tanto necesitaremos im-
primir cada objeto Definición, hemos sobrescrito el método toString(). La implementación realizada
se basa en concatenar objetos String, usando el operador + y el método estático format(), de manera
que se genera un String que tiene, en la primera lı́nea el contenido del atributo palabra, y en el resto
de lı́neas, tantas como sea necesario, cada una de las acepciones contenidas en el vector definiciones
convenientemente numeradas.
Ahora que tenemos implementada la clase Definición podemos basarnos en ella para escribir la clase
Diccionario:

Ejemplo1Diccionario/Diccionario.java
1 import java.io.PrintStream;

3 /** Representa un objeto Diccionario, usa la clase Definición


4 * @author los profesores de IP
5 * @version 1.0 */
6 public class Diccionario {

8 //Atributos
9 /** Vector para representar las Definiciones */
10 private Definición[ ] palabras;

12 //Constructores
13 /** Constructor por defecto, crea un diccionario vacı́o */
14 public Diccionario() {
15 this.palabras=new Definición[0]; //Crea un diccionario vacı́o
16 }

18 //Métodos públicos
19 /** Añade al Diccionario una nueva palabra. Si la palabra
20 * ya existen, añade su nueva definición. Si no existe la crea.
21 * @param p la palabra
22 * @param d la definición de esa palabra */
23 public void añadePalabra(String p, String d) {
24 int index=this.posiciónPalabra(p);
25 if (index!=-1) {
26 this.palabras[index].añadeDefinición(d);
27 }
28 else {
29 Definición[ ] tmp=this.palabras;
30 this.palabras=new Definición[tmp.length+1];
31 for (int i=0; i<tmp.length; i++)
32 this.palabras[i]=tmp[i];
33 String[] defs=new String[1];
34 defs[0]=d;
35 this.palabras[this.palabras.length-1]=new Definición(p,defs);
36 }
37 }

39 /** Dada una palabra retorna sus definiciones


40 * @param p la palabra
41 * @return un vector con sus definiciones si existe,
42 * si no existe devuelve null */
43 public String[ ] buscaPalabra(String p) {
44 int index=this.posiciónPalabra(p);
45 if (index!=-1) {
46 return this.palabras[index].getDefiniciones();
47 }
48 else return null;
49 }

51 /** Imprime el Diccionario en un objeto PrintStream

182
52 * @param ps el objeto PrintStream (como System.out) */
53 public void imprimeDiccionario(PrintStream ps) {
54 for (int i=0; i<this.palabras.length;i++)
55 ps.println(this.palabras[i]);
56 }

58 //Métodos privados
59 /** Busca la posición de una palabra en el Diccionario
60 * @param p la palabra
61 * @return el ı́ndice de la palabra en el vector de Palabras,
62 * -1, si la palabra no está */
63 private int posiciónPalabra(String p) {
64 int i=0;
65 while ( (i<this.palabras.length) &&
66 (!p.equals(this.palabras[i].getPalabra())) )
67 i++;
68 return ( i < this.palabras.length ? i : -1);
69 }
70 }

La clase es más simple de lo que pudiera pensarse en un principio. Tiene un único atributo, llamado
palabras, que es un vector de objetos Definición. Solamente hemos realizado el constructor por defecto
que crea un Diccionario vacı́o. Es decir, hacemos que el vector palabras no tenga componentes. Podrı́a
pensarse que serı́a mejor no haber hecho esta versión del constructor y dejar que el constructor por
defecto creado por el compilador se encargara de inicializar el atributo palabras a null. No es muy
acertado hacer eso ya que el vector estarı́a inicialmente sin crear y cualquier intento de acceso en los
miembros, por ejemplo al atributo length, causarı́a una excepción. Eso complicarı́a la programación del
resto de métodos; es preferible crear el vector con 0 componentes y poder acceder en el resto de métodos
al atributo length sin tener que comprobar previamente si el objeto es distinto de null.
El resto de métodos de la clase se basan en un método privado, llamado posiciónPalabra(), y que sirve
para localizar la posición de una determinada palabra dentro del vector palabras de objetos Definición.
La implementación de dicho método es una búsqueda lineal. Si la palabra que se pasa como argumento en
un objeto String se encuentra en el vector palabras de objetos Definición entonces el método devuelve
su ı́ndice. Si no se encuentra retorna −1.
Los métodos añadePalabra() y buscaPalabra() se valen de lo que retorna posiciónPalabra() para
hacer sus respectivas tareas. En añadePalabra(), si la palabra ya se encontraba en el Diccionario, entonces
se invoca el método añadeDefinición() sobre el objeto Definición que contiene las definiciones de dicha
palabra. Si la palabra no está en el Diccionario, entonces: 1) se asigna un nuevo bloque de memoria
al vector palabras para que tenga una posición más, 2) se copian los objetos Definición que tenı́a
previamente y 3) se crea, en la última posición del vector, una nueva Definición usando el operador new
con la palabra y definición que recibe el método como argumentos. La parte else es similar al método
añadeDefinición() de la clase Definición, salvo que en dicho método lo que se asigna es directamente
un objeto String y en éste necesitamos crear un nuevo objeto Definición.
El método buscaDefinición() es más sencillo ya que simplemente retorna un vector de objetos String
con las definiciones si la palabra p se encuentra en el Diccionario, o null si no está. En el primer caso
se usa el método getDefiniciones() proporcionado por la clase Definición.
La clase Diccionario no sobrescribe ninguno de los métodos de la clase Object. Implementar el método
equals() se ha considerado poco útil para esta clase; serán objetos con mucha información y no parece
lógico necesitar compararlos. Sin embargo, sı́ se podrı́a haber implementado el método toString() de
manera que retornara un objeto String con todas las definiciones del Diccionario concatenadas. Dado
que serı́a un objeto String muy grande en el que posiblemente habrı́a que hacer muchas operaciones de
concatenación, en este caso particular hemos decidido hacer un método, llamado imprimeDiccionario(),
que se encarga de imprimir todo el contenido del Diccionario sobre el objeto de la clase PrintStream
que recibe como argumento. La clase PrintStream es la clase de System.out. La ventaja de incluir un
parámetro de este tipo en el método, en lugar de imprimir directamente sobre System.out, es que nos
podrı́a servir para imprimir un Diccionario tanto en la consola como en un fichero de texto.
Podemos ver la clase Diccionario en acción en el programa de ejemplo que se muestra al final de este
párrafo. El programa es bastante básico, se limita a crear un objeto Diccionario vacı́o y va añadiéndole
nuevas definiciones usando el método añadeDefinición() de la clase Diccionario. Las nuevas definiciones
las lee inicialmente de un fichero y posteriormente permite que el usuario inserte nuevas definiciones.

183
Al final muestra en pantalla el contenido del Diccionario llamando al método imprimeDiccionario() y
pasándole System.out.

Ejemplo1Diccionario/Ejemplo1Diccionario.java
1 import java.io.File;
2 import java.io.FileNotFoundException;
3 import java.util.Scanner;

5 /** Diccionario de palabras y definiciones


6 * @author los profesores de IP */
7 public class Ejemplo1Diccionario {

9 public static void main(String[] args) throws FileNotFoundException {


10 //Objeto File para acceder a un fichero
11 File f = new File ( "palabras.txt" );
12 //Objeto Scanner asociado con el objeto f
13 Scanner fichero= new Scanner(f);
14 //Creamos el objeto diccionario, inicialmente vacı́o
15 Diccionario dicc=new Diccionario();
16 //Leemos el número de palabras que tiene el fichero
17 int palabras = fichero.nextInt();
18 //Leemos las palabras y sus significados y se añaden al diccionario
19 for (int i=1; i <= palabras; i++) {
20 String p=fichero.next();
21 String d=fichero.nextLine();
22 dicc.añadePalabra(p, d);
23 }
24 System.out.println("Diccionario"); //Lo imprimimos
25 dicc.imprimeDiccionario(System.out);
26 System.out.println("-----------");
27 //Bucle para que el usuario introduzca nuevas palabras
28 //Otro objeto Scanner, éste para el teclado
29 Scanner teclado= new Scanner(System.in);
30 System.out.println("Introduce palabra definición (FIN para acabar)");
31 String p=teclado.next();
32 while (!p.equals("FIN")) {
33 String d=teclado.nextLine();
34 dicc.añadePalabra(p, d);
35 p=teclado.next();
36 }
37 System.out.println("Diccionario");
38 dicc.imprimeDiccionario(System.out);
39 System.out.println("-----------");
40 }
41 }

Para concluir, indicar que esta clase Diccionario podrı́a haberse hecho de otras muchas formas y
la presentada es una más de ellas. Una ampliación muy interesante consistirı́a en mantener los objetos
Definición que contiene un Diccionario permanentemente ordenados alfabéticamente. Para hacerlo de-
berı́amos emplear el método compareTo de la clase String (ver Sección 5.8.1) para conocer en qué posición
del vector palabras se deberı́a insertar el nuevo objeto Definición que se añade al Diccionario y volver
a implementar el método buscaPalabra() para que fuera más eficiente y se aprovechase del hecho de que
las palabras estarı́an ordenadas.

pruebas Probar bien cualquier clase requiere probar todos sus métodos, pero en la clase Diccionario probando
dos tipos de casos concretos: añadir una palabra nueva al diccionario y añadir una definición nueva a
una palabra existente, estaremos probando no solamente esos métodos, sino a su vez los métodos que
ellos usan, por ejemplo el método posiciónPalabra(). Por tanto, lo básico es añadir palabras nuevas
y definiciones nuevas con la suficiente variabilidad en los casos de prueba que diseñemos.

6.10.2. La clase Fecha


La otra clase que vamos a usar como ejemplo es una que ya se utilizó en el Tema 3, la clase Fecha.
Daremos aquı́ una implementación mucho más completa que parte del siguiente enunciado.

184
enun- Realizar la clase Fecha, que sobrescriba los métodos de equals(), toString() y hashCode() y que permita
ciado imprimir las fechas en dos formatos: dd/mm/aaaa y dd de Mes de aaaa.

Destacaremos los siguientes aspectos en la implementación de esta clase:

Es una clase completa, con una funcionalidad clara y útil. Será una clase ya de un cierto tamaño
que nos va permitir usar los elementos más importantes discutidos a lo largo del tema, como los
constructores y los métodos de la clase Object principalmente, y además introducir algunas cosas
nuevas.

Vamos a programar los aspectos del formato usando:

1. Una enumeración. Introduciremos informalmente qué es.


2. Elementos estáticos. El atributo que va a guardar la forma de formatear los objetos Fecha
será estático, igual que los métodos que permitirán manipularlo.

Además programaremos el método hashCode() y explicaremos su utilidad.

La especificación completa de la clase Fecha que vamos a hacer aparece en notación UML en la
Figura 6.6. La clase tiene varios elementos estáticos que en notación UML se denotan subrayándolos. El
más importante de todos será el atributo formato ya que será el que indique cómo se va a imprimir la
información contenida en cualquier objeto Fecha.

Fecha
- formato: FORMATO
- día: int
- mes: int
- año: int
+ setFormato(FORMATO)
+ getFormato(): FORMATO
- esBisiesto(): boolean
+ Fecha()
+ Fecha(Fecha)
+ Fecha(int)
+ Fecha(int,int)
+ Fecha(int,int,int)
+ getDía(): int
+ setDía(int)
+ getMes(): int
+ setMes(int)
+ getAño(): int
+ setAño(int)
+ setFecha(int,int,int)
+ equals(Object): boolean
+ toString(): String
+ hashCode(): int

Figura 6.6: UML - Clase Fecha (versión completa). Los elementos estáticos aparecen subrayados

La programación completa de la clase es la que se muestra a continuación. En los párrafos siguientes


iremos explicando los detalles más importantes de cada una de las partes de la implementación realizada.

Ejemplo3Fecha/Fecha.java
1 //Definición de la clase Fecha y de otros elementos que requiere su implementación
2 //La clase está implementada con 3 atributos int y una enumeración para definir
3 //el formato para mostrar en forma de texto las fechas

5 /** Enumeración FORMATO


6 * <p>Define los dos tipos de formatos para mostrar objetos Fecha:
7 * <it>FORMATO.DDMMAAAA dd/mm/aaaa, p.e. 31/12/2050</it>
8 * <it>FORMATO.TEXTO dd de Mes de aaaa, p.e. 31 de Diciembre de 2050</it> */
9 enum FORMATO { DDMMAAAA, TEXTO }

11 /** Representa objetos Fecha capaces de imprimirse en dos formatos

185
12 * @author los profesores de IP
13 * @version 3.0 */
14 public class Fecha {

16 //Atributos estáticos (de la clase)


17 /**Formato para mostrar los objetos Fecha como String.
18 * Es estático, afecta a todos los objetos de la clase */
19 private static FORMATO formato=FORMATO.DDMMAAAA;

21 //Métodos estáticos
22 /** Cambia el formato textual de los objetos Fecha
23 * @param f nuevo valor para el formato */
24 public static void setFormato(FORMATO f){
25 formato=f;
26 }

28 /** Retorna el formato textual usado por la clase Fecha


29 * @return el formato actual */
30 public static FORMATO getFormato() {
31 return formato;
32 }

34 //Atributos (de los objetos Fecha)


35 /**Valor del dı́a del objeto Fecha*/
36 private int dı́a;
37 /**Valor del mes del objeto Fecha*/
38 private int mes;
39 /**Valor del año del objeto Fecha*/
40 private int año;

42 //Constructores
43 /** Inicialización sin parámetros: 1/1/2000 */
44 public Fecha() {
45 this(1, 1, 2000);
46 }

48 /** Inicializa una fecha a partir de otro objeto Fecha


49 * @param f valor del objeto Fecha para hacer la inicialización */
50 public Fecha(Fecha f) {
51 this(f.getDı́a(),f.getMes(),f.getAño());
52 }

54 /** Inicializa una fecha al dı́a 1 de Enero de el año indicado


55 * @param año valor del año */
56 public Fecha(int año) {
57 this(1, 1, año);
58 }

60 /** Inicializa una fecha al dı́a 1 del mes y año indicados


61 * @param mes valor del mes
62 * @param año valor del año */
63 public Fecha(int mes, int año) {
64 this(1, mes, año);
65 }

67 /** Inicializa una fecha al dı́a, mes y año indicados


68 * @param dı́a valor del dı́a
69 * @param mes valor del mes
70 * @param año valor del año */
71 public Fecha(int dı́a, int mes, int año) {
72 this.setFecha(dı́a,mes,año);
73 }

75 //Métodos privados
76 /**Devuelve cierto si la Fecha corresponde a un año bisieto
77 * @return cierto si el atributo Año corresponde a un año bisiesto */
78 private boolean esBisiesto() {
79 return ( (this.getAño() % 4 == 0 && this.getAño() % 100 != 0) ||
80 (this.getAño() % 400 == 0) );
81 }

186
83 //Métodos públicos
84 /**Cambia la fecha completa del objeto
85 * @param dı́a nuevo valor para el dı́a del objeto
86 * @param mes nuevo valor para el mes
87 * @param año nuevo valor para el año */
88 public void setFecha(int dı́a, int mes, int año) {
89 //Fijamos las fechas en este orden, para que al fijar el dı́a,
90 //sepamos si es correcto de acuerdo al nuevo año y mes
91 this.setAño(año);
92 this.setMes(mes);
93 this.setDı́a(dı́a);
94 }

96 /**Devuelve el valor del dı́a del objeto Fecha


97 * @return el dı́a del objeto */
98 public int getDı́a() {
99 return this.dı́a;
100 }

102 /**Cambia el valor del dı́a del objeto, para que valga d
103 * @param dı́a nuevo valor para el dı́a del objeto */
104 public void setDı́a(int dı́a) {
105 int[ ] dı́as_mes={0,31,28,31,30,31,31,30,31,30,31,30,31};
106 if (this.getMes() == 2 && this.esBisiesto())
107 dı́as_mes[2] = 29;
108 if ( (dı́a >= 1) && (dı́a <= dı́as_mes[this.getMes()]) )
109 this.dı́a = dı́a;
110 }

112 /**Devuelve el valor del mes del objeto Fecha


113 * @return el mes del objeto */
114 public int getMes() {
115 return this.mes;
116 }

118 /**Cambia el valor del mes del objeto, para que valga m
119 * @param mes nuevo valor para el mes del objeto */
120 public void setMes(int mes) {
121 if ( mes>=1 && mes<=12 ) this.mes=mes;
122 }

124 /**Devuelve el valor del año del objeto Fecha


125 * @return el año del objeto */
126 public int getAño() {
127 return this.año;
128 }

130 /**Cambia el valor del año del objeto, para que valga a
131 * @param año nuevo valor para el año del objeto
132 * @return nada */
133 public void setAño(int año) {
134 if ( año > 0 ) this.año=año;
135 }

137 //Adaptación de los métodos heredados de la clase Object


138 /**Devuelve un String con la fecha en el formato actual
139 * @return un string de acuerdo con el formato textual actual */
140 @Override
141 public String toString() {
142 if (formato==FORMATO.DDMMAAAA)
143 return String.format(" %02d/ %02d/ %04d",
144 this.getDı́a(),this.getMes(),this.getAño());
145 else {
146 String[ ] meses = { "", "Enero", "Febrero", "Marzo",
147 "Abril", "Mayo", "Junio",
148 "Julio", "Agosto", "Septiembre",
149 "Octubre","Noviembre","Diciembre" };

151 return String.format(" %d de %s de %d",


152 this.getDı́a(),meses[this.getMes()],this.getAño());
153 }

187
154 }

156 /** Método para saber si dos objetos fecha son iguales
157 * @param f la Fecha con la se va a comparar el objeto que haga la llamada
158 * @return true si ambos objetos representan la misma fecha */
159 @Override
160 public boolean equals(Object obj) {
161 if (this==obj) return true;
162 if (obj instanceof Fecha) {
163 Fecha f = (Fecha) obj;
164 return ( this.getDı́a()==f.getDı́a() &&
165 this.getMes()==f.getMes() &&
166 this.getAño()==f.getAño() );
167 }
168 else return false;
169 }

171 /** Código Hash para cada objeto Fecha, concatenación de la cifra
172 * menos significativa del dı́a, mes y año. Ej: 31/12/2050 -> 120
173 * @return código hash del objeto Fecha */
174 @Override
175 public int hashCode() {
176 return this.getDı́a() %10*100 +
177 this.getMes() %10*10 + this.getAño() %10;
178 }
179 }

El primer elemento novedoso de esta nueva versión de la clase Fecha es la forma de permitir que los
objetos de la clase se conviertan de dos maneras distintas a String a través del método toString(): la
primera siguiendo el formato numérico dd/mm/aaaa, y la segunda un formato textual que incluya el
nombre del mes, por ejemplo 31 de Diciembre de 2050. Para implementarlo hemos creado un atributo
estático, llamado formato, que afectará por tanto a todos los objetos de la clase y con el que se puede
determinar cómo se van a transformar en String los objetos de la clase. El tipo que le hemos asignado
a formato es el de la enumeración FORMATO definida dentro del mismo fichero.

definición Enumeración: Define un nuevo tipo de dato formado por un conjunto de constantes.

Las enumeraciones vienen a sustituir a un conjunto de constantes, como las definidas en el ejemplo de
la Sección 4.5. La enumeración FORMATO consta de dos constantes, DDMMAAAA y TEXTO, una para cada tipo
de formato disponible, y la forma de acceder a ellas será mediante el operador punto. Las enumeraciones
son como clases y las constantes atributos estáticos y públicos de las mismas, por lo que son accesibles
usando el nombre de la enumeración y el nombre de la constante. Muchos programadores prefieren usar
constantes estáticas en las clases en lugar de enumeraciones, tal como se hizo en la versión previa de la
clase Fecha de la Sección 4.5.
El atributo formato tiene como valor por defecto DDMMAAAA. Para poder acceder a él y modificarlo,
hemos programado dos métodos estáticos, getFormato() y setFormato(), que se encargan respectivamente
de retornar y modificar el atributo formato. Otra opción habrı́a sido no hacer el atributo formato estático y
permitir que cada objeto de la clase tuviera su propio formato. Se ha descartado esta opción, que también
serı́a válida, basándonos en la idea de que una aplicación que use la clase Fecha puede requerir imprimir
del mismo modo todos los objetos Fecha que cree. Si ese es el caso, simplemente habrá que cambiar el
valor del atributo formato una vez y todos los objetos se imprimirán de la forma deseada.
Los atributos no estáticos y los constructores son triviales. Tenemos tres atributos de tipo int, para
el dı́a, mes y año. El juego de constructores está formado por cinco constructores distintos: el constructor
por defecto, el constructor copia, un constructor en el que se suministra solamente el año (se inicializa
con el dı́a 1 de Enero de ese año), otro en el que se indica mes y año (dı́a 1 de ese mes y año) y el último
en que se pasarán los tres datos enteros. El resto de constructores están redirigidos hacı́a este último
usando this tal como hemos hecho en clases anteriores y cómo se explicó en la Sección 6.6. El constructor
con los tres parámetros int usa a su vez el método setFecha() para facilitar posibles modificaciones en
la implementación de la clase (ver Sección 6.10.3).
Muchas de las implementaciones de los métodos, especialmente en los métodos get() y set() coinciden
con la implementación previa de la clase presentada en el Tema 4. No es el caso del método setDı́a().

188
En la implementación de la Sección 4.5 usamos una sentencia switch para comprobar si el nuevo valor
para el atributo dı́a era correcto en función de los valores de los atributos mes y año. Valiéndonos de un
vector local de enteros (dı́as mes), que contiene el número de dı́as de cada mes, hemos simplificado la
codificación ya que conocemos directamente el número de dı́as máximo que tiene ese mes. Simplemente
tenemos que emplear como ı́ndice el valor del atributo mes gracias al pequeño truco de no usar la posición
0 del vector dı́as mes. Antes de usar dicho vector, modificamos el valor del mes de Febrero en el caso de
los años bisiestos.
El método heredado y sobrescrito toString() es en el que vamos a implementar la opción para escribir
los objetos Fecha con los dos formatos disponibles. Si el valor del atributo formato es FORMATO.DDMMAAAA
entonces se imprimen los valores numéricos de los atributos dı́a, mes y año, usando sus correspondientes
métodos get(). Cuando el formato para los objetos Fecha es FORMATO.TEXTO hemos usado el mismo truco
del vector local que empleamos en el método setDı́a(). En esta ocasión es un vector local con los nombres
de cada mes y dejando de nuevo la posición 0 vacı́a. Si la eficiencia fuera un requisito importante, este
vector deberı́a crearse como un vector constante (final) y estático dentro de la clase, de forma que
no se crease cada vez que se llama al método toString() (es los que haremos en la modificación de la
Sección 6.10.3).
Para finalizar, hemos sobrescrito el método equals() como hicimos en otras clases previamente, y otro
método de la clase Object, hashCode(). La implementación de equals() es idéntica algoritmicamente a
la de la clase Cı́rculo. Si directamente son el mismo objeto entonces se devuelve true y en caso contrario
se comprueba si el valor de los tres atributos es el mismo. Como siempre hay que recordar hacer una
conversión previa del parámetro Object a un objeto de la clase, aquı́ de la clase Fecha.
El método hashCode() sirve para, dado un objeto de la clase, asignarle un código entero, que se suele
llamar código hash, de manera que si dos objetos tienen un código hash diferente es que son objetos
distintos. Lo contrario no siempre es cierto, es decir, puede ser que dos objetos distintos tengan el mismo
valor hash. El código hash viene a ser una clave que representa de manera, a veces unı́voca, un objeto.
Su mayor utilidad es permitir guardar los objetos en lo que se denominan tablas hash, que el lector puede
imaginar como un vector en el que los elementos se indexan según su código hash. Si varios objetos
tienen el mismo código hash entonces en esa posición de la tabla tabla hash habrá una lista o un vector
conteniendo todos ellos. Las tablas hash agilizan enormemente las búsquedas ya que permiten conocer la
posición de un elemento dado calculando su código hash.
La clase Fecha es perfecta para mostrar su utilidad ya que podemos encontrar de forma sencilla un
código entero para cada objeto Fecha. El código hash devuelto por nuestro método hashCode() será un
número comprendido entre 0 y 999, formado concatenando los dı́gitos menos significativos del dı́a, mes
y año. Por ejemplo, si tenemos un objeto Fecha que representa la fecha 31/12/2050, entonces su código
hash se formará con el dı́gito menos significativo del dı́a (1), seguido del menos significativo del mes (2),
y el menos significativo del año (0), es decir, su valor será 120. Otros ejemplos: 29/09/2049, producirı́a
999, y 30/10/2050, el valor 0.
Si ahora guardáramos objetos de una clase que tuviera entre sus datos un objeto Fecha, por ejemplo
la clase EventoHistórico, podrı́amos guarda dichos objetos en una tabla hash indexada por el código
hash generado por el método hashCode() de la clase Fecha. La tabla hash guardarı́a en la misma posición
todos los eventos que tuvieran el mismo código hash. Por ejemplo, eventos que hubieran ocurrido en las
fechas 31/12/2050, 11/02/2040 o 21/12/2050 estarı́a guardadas en la posición 120. Se podrı́an guardar
usando una lista o un vector u otra estructura de datos. La ventaja de las tablas hash es que agilizan
mucho las búsquedas. Por ejemplo, si queremos buscar si ocurrió algún evento en una fecha concreta,
simplemente irı́amos directamente al elemento de la posición de su código hash y se harı́a una pequeña
búsqueda en la lista o vector que recogiera todos los eventos con un mismo código hash.
El código hash generado por el método hashCode() tiene ligeros sesgos por la mayor presencia de las
cifras 1 y 2 en la posición del mes y de la cifra 1 en la posición del dı́a. Hay muchas otras estrategias para
generar códigos hash a partir de una fecha, algunas sin este pequeño sesgo, pero creemos que el método
usado proporciona una visión sencilla e intuitiva de lo que es un código hash.
Podremos usar ya nuestra clase en cualquier programa que requiera manejar información sobre fechas.
En el programa de ejemplo que sigue a continuación nos hemos limitado a crear distintos objetos Fecha,
uno con cada constructor, y a imprimirlos usando el método toString(), primero lo hemos hecho usando
el formato por defecto de la clase Fecha y luego en formato textual invocando previamente el método
estático setFormato() y pasándole la constante FORMATO.TEXTO.

189
Ejemplo3Fecha/Ejemplo3Fecha.java
1 /** Ejemplo 3 de uso de la clase Fecha
2 * @author los profesores de IP */
3 public class Ejemplo3Fecha {

5 public static void main(String[ ] args) {


6 //Objetos de la clase Fecha inicializados con los
7 //cinco constructores de la clase
8 Fecha f1 = new Fecha();
9 Fecha f2 = new Fecha(2010);
10 Fecha f3 = new Fecha(8,2010);
11 Fecha f4 = new Fecha(31,12,2010);
12 Fecha f5 = new Fecha(f4);

14 //Los mostramos con el formato por defecto


15 System.out.printf("f1: %s\n",f1);
16 System.out.printf("f2: %s\n",f2);
17 System.out.printf("f3: %s\n",f3);
18 System.out.printf("f4: %s\n",f4);
19 System.out.printf("f5: %s\n",f5);

21 //Cambiamos el formato
22 Fecha.setFormato(FORMATO.TEXTO);
23 System.out.printf("f1: %s\n",f1);
24 System.out.printf("f2: %s\n",f2);
25 System.out.printf("f3: %s\n",f3);
26 System.out.printf("f4: %s\n",f4);
27 System.out.printf("f5: %s\n",f5);
28 }
29 }

pruebas Para probar que los métodos set() de la clase, en los casos de prueba hay que incluir tanto fechas
correctas como incorrectas. Hay que probar fechas correctas e incorrectas para los meses de 31 dı́as,
de 30 dı́as y Febrero, tanto en años bisiestos como no bisiestos. Es conveniente usar valores lı́mite,
por ejemplo, las fechas incorrectas de un mes de 31 dı́as podrı́an ser el dı́a 0 y el dı́a 32. Dentro
de este bloque de pruebas hay que dedicar especial atención a probar que la clase detecta bien los
años bisiestos y no bisiestos. Por último, para probar los formatos hay que hacer casos de prueba
que escriban fechas en ambos formatos y con todos los meses del año. Esta clase requiere en realidad
bastantes casos de pruebas distintos por la variabilidad de los parámetros de entrada, la dificultad
de probar bien los años bisiestos y se añade además, la parte de usar distintos formatos a la hora de
escribir las fechas.

6.10.3. Otra implementación de la clase Fecha


Un ejemplo interesante para demostrar cómo cambiar la implementación de un clase sin afectar a
su interfaz pública, consiste en rediseñar la clase Fecha de forma que en lugar de representar una fecha
concreta mediante 3 atributos de tipo int lo hagamos mediante uno solo que llamaremos fecha. Dicho
valor entero será un número de 7 u 8 dı́gitos (dependiendo del valor del dı́a, si es menor de 10 el código
tendrá 7 dı́gitos, y si es mayor o igual tendrá 8) siguiendo el formato DDMMAAAA, es decir, los cuatro
dı́gitos menos significativos representan el año, los dos siguientes el mes, y los más significativos (1 ó 2)
representan el dı́a. Por ejemplo, si tenemos la fecha 31/12/2005, el valor del atributo fecha será 31122050.
Si tuviéramos la fecha 09/12/2050, su valor serı́a 9122050.
La ventaja de esta nueva implementación es que, por un lado los objetos ocuparán solamente una ter-
cera parte del espacio que ocupaban con la implementación de la Sección 6.10.2, y por otro no hará falta
cambiar ninguno de los programas que usen la clase Fecha, como el mostrado anteriormente, ya que la
parte pública de la clase no cambia. Siempre que una cosa tiene ventajas, también suele tener incon-
venientes. Los programas que son más eficientes en el consumo de memoria, suelen ser más lentos en
su ejecución. Es precisamente el caso de la nueva implementación de la clase Fecha que se muestra a
continuación. Será más lenta ya que los accesos al atributo fecha mediante get() y set() requerirán de
divisiones por potencias de 10.

190
Ejemplo4Fecha/Fecha.java
7 /** Representa objetos Fecha, como la versión 3, pero con un solo atributo int
8 * @author los profesores de IP
9 * @version 4.0 */
10 public class Fecha {

...
12 //Atributo (de los objetos Fecha)
13 /** Representación de una Fecha con 8 dı́gitos DDMMAAAA */
14 private int fecha;

16 //Atributos estáticos (de la clase)


17 /**Formato para mostrar los objetos Fecha como String.
18 * Es estático, afecta a todos los objetos*/
19 private static FORMATO formato=FORMATO.DDMMAAAA;
20 /** Vector con el número de dı́a de cada mes, se usa en setDı́a() */
21 private final static int[ ] dı́as_mes={0,31,28,31,30,31,31,
22 30,31,30,31,30,31};
23 /** Vector con el nombre de cada mes, se usa en toString() */
24 private final static String[ ] meses = { "", "Enero", "Febrero",
25 "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto",
26 "Septiembre", "Octubre", "Noviembre", "Diciembre" };

...
95 /**Devuelve el valor del dı́a del objeto Fecha
96 * @return el dı́a del objeto */
97 public int getDı́a() {
98 return (this.fecha/1000000);
99 }

101 /**Cambia el valor del dı́a del objeto, para que valga dı́a
102 * @param dı́a nuevo valor para el dı́a del objeto
103 * @return nada */
104 public void setDı́a(int dı́a) {
105 int último=( this.getMes()==2 && esBisiesto() ?
106 29 : dı́as_mes[this.getMes()]);
107 if ( (dı́a >= 1) && (dı́a <= último) ) {
108 this.fecha = dı́a*1000000 +
109 this.getMes()*10000 + this.getAño();
110 }
111 }

113 /**Devuelve el valor del mes del objeto Fecha


114 * @return el mes del objeto */
115 public int getMes() {
116 return ((this.fecha/10000) %100);
117 }

119 /**Cambia el valor del mes del objeto, para que valga mes
120 * @param mes nuevo valor para el mes del objeto */
121 public void setMes(int mes) {
122 if ( mes>=1 && mes<=12 )
123 this.fecha = 1000000*this.getDı́a() +
124 10000*mes + this.getAño();
125 }

127 /**Devuelve el valor del año del objeto Fecha


128 * @return el año del objeto */
129 public int getAño() {
130 return (this.fecha %10000);
131 }

133 /**Cambia el valor del año del objeto, para que valga año
134 * @param año nuevo valor para el año del objeto
135 * @return nada */
136 public void setAño(int año) {
137 if ( año > 0 )
138 this.fecha = 1000000*this.getDı́a() +
139 10000*this.getMes() + año;

191
140 }

142 //Adaptación de los métodos heredados de la clase Object


143 /**Devuelve un String con la fecha en el formato actual
144 * @return un string de acuerdo con el formato textual actual */
145 @Override
146 public String toString() {
147 if (formato==FORMATO.DDMMAAAA)
148 return String.format(" %02d/ %02d/ %04d",
149 this.getDı́a(),this.getMes(),this.getAño());
150 else {
151 return String.format(" %d de %s de %d",
152 this.getDı́a(),meses[this.getMes()],this.getAño());
153 }
154 }

...
179 }

En la implementación anterior de la clase Fecha (ver Sección 6.10.3), los únicos métodos que acceden
directamente a los atributos son los métodos get() y set(). En el resto de métodos de la clase, siempre que
se necesita obtener o modificar el valor del un atributo, ya sea el dı́a, el mes o el año, siempre lo hacemos
a través de sus métodos get() y set() respectivamente. Al haber seguido esa forma de programar tan
estricta, al realizar un cambio en la forma de representar los objetos, o lo que es lo mismo, al cambiar
los atributos, los cambios que se deben hacer en la implementación de la clase se limitan únicamente a
los citados métodos get() y set(). El resto de métodos no hace falta tocarlos, ya que todos ellos usan
los métodos get() y set() para manipular los atributos. Sin contar con los cambios realizados en los
métodos set() y get(), el único cambio adicional que se ha hecho en la nueva versión del clase, con
el objetivo de mejorar la eficiencia de los métodos setDı́a() y toString(), ha sido que los dos vectores
locales que empleaban pasen a ser vectores estáticos (de la clase), creándose solamente una vez. El resto
de elementos de la clase no se han tocado.
Para trabajar con la nueva representación de los atributos de la clase, ya sea para obtener un valor
o modificar alguna parte, nos valdremos de multiplicaciones, divisiones y módulos de algunas potencias
de 10. Los tres métodos set() que tiene la clase son los que se encargan de guardar adecuadamente
el valor del atributo fecha. Para calcularlo todos ellos hacen exactamente lo mismo: multiplican el dı́a
por un millón, y a esa cantidad le suman el mes por 10000 y el año. Son un poco más complicados los
métodos get(). El más sencillo es getAño(), simplemente tiene que retornar el resto de dividir fecha por
10000, que serán los 4 dı́gitos menos significativos del atributo fecha y por tanto el valor del año. Para
obtener el valor del mes tenemos que hacer dos operaciones en el método getMes(), primero una división
entera entre 10000, lo que dejarı́a los dı́gitos del dı́a y el mes, y después el modulo de dividir por 100,
que dejarı́a los dos dı́gitos menos significativos de la cantidad anterior eliminando los dı́gitos del dı́a y
dejando solamente los del mes. Por último, el método getDı́a() retorna el dı́gito o los dos dı́gitos más
significativos del atributo fecha haciendo una división entera por un millón.
El programa de prueba para la clase podrı́a ser perfectamente el mismo de la versión anterior. La
parte pública no ha cambiado y por tanto no serı́a necesario cambiar nada en aquel programa. Esta
puede ser una situación muy habitual en muchas clases incluidas en los paquetes de Java. Mientras no
se cambie la interfaz pública, las nuevas versiones de Java pueden incluir cambios en la implementación
de las clases mejorando la eficiencia de algunas operaciones. Los programas que ya usaran dichas clases
no necesitan ser modificados y se aprovecharı́an automáticamente de las mejoras que se hayan realizado
en las clases.

192
Apéndice A

Palabras Reservadas

El lenguaje Java está compuesto por las palabras reservadas que aparecen en la Tabla A.1. Las
palabras reservadas son el conjunto de palabras que forman el lenguaje, como pasa en los lenguajes
hablados.

Tabla A.1: Lista de palabras reservadas en Java. Las palabras que aparecen con el sı́mbolo † no se utilizan
y las marcadas con § se incluyeron en versiones posteriores a la inicial

abstract assert§ boolean break byte


case catch char class const†
continue default do double else
enum§ extends final finally float
for goto† if implements import
instanceof int interface long native
new package private protected public
return short static strictfp§ super
switch synchronized this throw throws
transient try void volatile while

Respecto a la lista, hay que destacar dos aspectos:

Las palabras reservadas const y goto no se utilizan.


Las palabras false, null y true también se consideran reservadas por su significado.

Recuérdese que no se puede utilizar una palabra reservada como identificador de ninguno de los
elementos que use tu programa (variables, constantes, clases, métodos). Se producirı́a un error en tiempo
de compilación.
Varias de las palabras reservadas que aquı́ aparecen no las utilizaremos en los programas que realizare-
mos en esta asignatura. Su utilidad se descubrirá en la asignatura del segundo cuatrimestre Metodologı́a
de la Programación.

193
Apéndice B

Constantes

Una constante es un nombre simbólico que un programa emplea para referirse a un valor que no
cambiará durante la ejecución del programa. También es una constante, en este caso, sin nombre, un
valor que directamente se emplea en una expresión. En la Tabla B.1 aparece una descripción completa
de todos los tipos de constantes que se pueden definir en Java.

Tabla B.1: Formas de representación de constantes

Tipo Formato Ejemplo


Enteros Decimal 95
Octal 0141 → 95
Hexadecimal 0x0061 → 95
Reales Punto fijo 3.1416
Notación cientı́fica 0.31416e1
Caracteres Sı́mbolo ’a’
Secuencia de escape ’\n’
Octal \141 → ’a’
Hexadecimal (Unicode) \u0061 → ’a’
Cadenas “Hola”
Booleanos true y false

Como se puede apreciar, las constantes enteras pueden definirse de tres formas diferentes. La más
común es usar la notación decimal. Para usar la notación octal, la constante empezará siempre por 0
(esto es lo que la hace octal) e irá seguida del valor constante en formato octal, esto es, una secuencia de
dı́gitos del 0 al 7. Por ejemplo, la constante 015471 será correcta, mientras 01934 generará un error por
la presencia del dı́gito 9. Cuando la constante es decimal, no lleva nunca un 0 por delante. Las constantes
en hexadecimal empezarán siempre por 0x y luego una secuencia de dı́gitos del 0 al 9 y de las letras de
la A a la F. Por ejemplo, 0x1B39 será correcta y 0x1Z39 incorrecta.
Por defecto, las constantes enteras son de tipo int. Si se desea que una constante entera sea de tipo
long debe ponerse al final la letra L. Lo mismo pasa en las constantes reales. Por defecto son double y si
se desea que sea float debe ponerse al final una F (para indicar que es double se puede poner una D).
Las constantes carácter normalmente se representan con un carácter entre comillas simples. Cuando
se deba representar una secuencia de escape, como el salto de lı́nea o el tabulador, delante del carácter
que representa dicha secuencia se pone una barra invertida (\). Las secuencias de escape más empleadas
aparecen en la Tabla B.2. Dado que en Java se pueden usar caracteres Unicode, la representación puede
hacerse en notación hexadecimal, que comenzará con \, seguido de la letra u y el código del carácter en
hexadecimal (4 dı́gitos). También es posible usar la representación en octal, que comenzará con una \,
seguida del código del carácter en octal (3 dı́gitos).
Es un error muy común entre los programadores noveles confundir las constantes carácter con las
constantes cadena. Estas últimas van entre comillas dobles y no simples. Otra diferencia es que lo
normal es que estén formadas por una secuencia de varios caracteres. Tampoco se deben confundir con

195
las constantes carácter que representan secuencias de escape, ya que en este caso, aunque tengan dos
caracteres, realmente siguen representando un solo carácter (no imprimible, generalmente).
4
! No debe confundirse una constante cadena (entre comillas dobles) y una constante carácter
(comillas simples).
Las constantes cadena deben ocupar una sola lı́nea en el código fuente, no pueden extenderse en varias
lı́neas. Los caracteres que pueden contener incluyen todos los caracteres Unicode, incluso las secuencias
de escape que se muestran en la Tabla B.2 y los códigos que se pueden introducir directamente en
hexadecimal u octal, precedidos por la barra invertida.
Por último, el tipo de dato que tiene las constantes más sencillas de recordar y manejar es el tipo
boolean. Las dos únicas constantes que posee son sus dos únicos posibles valores: true y false. Recuérdese
que aunque estas dos palabras no son estrictamente reservadas (Apéndice A), se consideran como tales.
A diferencia de lo que pasa con las constantes de otros tipos, las constantes booleanas se suelen usar
directamente como constantes sin nombre, por ejemplo, en comparaciones (Tema 4).

Tabla B.2: Secuencias de escape

formato descripción
\n salto de lı́nea
\t tabulador horizontal
\f salto de página
\a alerta (campana)
\b retroceso
\’ comilla simple
\” comilla doble
\\ barra invertida
\ooo código del carácter en octal
\uhhhh código del carácter en hexadecimal

196
Apéndice C

Operadores en Java

Un Operador es un sı́mbolo del lenguaje que permite realizar una determinada operación. Dicha
operación devolverá un valor que dependerá del operador y de los operandos sobre los que se aplique. En
Java existen operadores de distinta cardinalidad, esto es, que necesitan un número diferente de operandos.
Ası́ existen operadores:

unarios: un operando, por ejemplo −a

binarios: dos operandos, por ejemplo a < b

ternarios: tres operandos, solamente hay uno, el operador condicional ( a ? b : c )

En las siguientes secciones vamos a explicar las operaciones que realizan cada uno de los operadores.
En Java podemos distinguir distintos grupos de operadores según su funcionalidad:

Aritméticos: realizan operaciones matemáticas (p.e. la suma).

Relacionales y de Comparación: permiten evaluar la relación de orden entre dos valores o su


igualdad.

Lógicos: realizan las operaciones lógicas tı́picas (p.e. el Y lógico).

De bits: realizan operaciones de bits, de desplazamiento o lógicas.

Asignación: hacen la asignación de un nuevo valor a una variable.

Otros: el resto de operadores hacen funciones de acceso o de otro tipo, como por ejemplo los ().

C.1. Operadores Aritméticos


Los operadores aritméticos se usan con los tipos enteros (byte, short, int, long, char) y los reales
(float y double). El valor que retornan es el del tipo “mayor” de sus operandos, cuando el operador es
binario, o el mismo tipo de su único operando cuando se trata de un operador unario. En la Tabla C.1
está la descripción de todos los operadores aritméticos soportados por Java.
Tal vez los más llamativos sean los pre- y post- incrementos y decrementos. Esos operadores equivalen
en realidad a dos operaciones. Una de ellas siempre es un incremento o decremento en una unidad de
la variable que aparezca en la expresión y el resultado de la misma depende de si el operando va antes
(pre-, devolverá la variable incrementada o decrementada) o si va después (devolverá el valor que tenı́a
la variable antes de que luego se haga el incremento o decremento). Solamente se pueden emplear con
variables, ya que necesitan un left-value para incrementarlo o decrementarlo (son como las asignaciones
+ = 1 y − = 1 respectivamente).

197
Tabla C.1: Operadores Aritméticos. Los ejemplos suponen que la variable a= 5 y el valor a la derecha
es el resultado de la expresión. Entre paréntesis el valor de la variable a tras la operación

Op. Uso Tipos Descripción Ejemplo


+ op1 + op2 enteros, reales Suma 7+4 11
− op1 − op2 enteros, reales Resta 2,1 − 4,8 −2,7
∗ op1 ∗ op2 enteros, reales Producto 4,8 ∗ 2,1 10,08
/ op1 / op2 enteros Divisón entera 7/4 1
/ op1 / op2 reales Divisón real 4,8/2,1 2,29
% op1 % op2 enteros Módulo 7 %4 3
% op1 % op2 reales Módulo real 4,8/2,1 0,6
+ +op enteros, reales Mantener signo +a 5
− −op enteros, reales Cambio de signo −a −5
++ ++op enteros, reales Pre-incremento ++a 6 (a=6)
++ ++op enteros, reales Post-incremento ++a 5 (a=6)
−− −−op enteros, reales Pre-decremento −−a 4 (a=4)
−− op−− enteros, reales Post-decremento −−a 5 (a=4)

C.2. Operadores Relacionales y de Comparación


La Tabla C.2 recoge los operadores de Java que permiten comparar dos expresiones, para comprobar
su orden o para ver si son iguales o distintas. Todos ellos son binarios, se pueden aplicar sobre cualquier
tipo básico (excepto los booleanos) y devuelven precisamente un valor de tipo boolean, reflejando si la
expresión es cierta o no.

Tabla C.2: Operadores Relacionales y de Comparación

Op. Uso Devuelve true si. . . Ejemplo


< op1 < op2 op1 es menor que op2 7<4 false
<= op1 <= op2 op1 es menor o igual que op2 4 <= 7 true
> op1 > op2 op1 es mayor que op2 7>4 true
>= op1 >= op2 op1 es mayor o igual que op2 4 >= 7 false
== op1 == op2 op1 y op2 son iguales 7 == 4 false
!= op1 ! = op2 op1 y op2 son distintos 4 ! = 7 true

C.3. Operadores Lógicos


Los operadores lógicos se emplean únicamente con operandos booleanos y devuelven también un valor
de tipo boolean. El valor depende obviamente de si la expresión lógica es cierta o no. En la Tabla C.3 se
muestra la descripción de todos los operadores lógicos que incluye Java y en la Tabla C.4 las tablas de
verdad de las cuatro operaciones lógicas.
La diferencia entre && y &, igual que entre || y |, es que en los primeros el segundo operando no se
evalúa si el resultado de la expresión puede determinarse tras evaluar el primer operando. Por ejemplo,
si hacemos op1 && op2 y el primer operando es false, entonces el resultado de la expresión va a ser
false, independientemente del valor del segundo operando. No evaluarlo ahorra tiempo de ejecución.
En cambio, si hubiéramos usado op1 & op2, el segundo operando se evaluarı́a igualmente. Teniendo en
cuenta que son menos eficientes estos últimos, cabe preguntarse en qué situación puede tener esto sentido
utilizarlos. La respuesta es que a veces ese segundo operando incluye una operación (por ejemplo, una
llamada a un método) que se quiere hacer siempre, independientemente del resultado, verdadero o falso,
de la expresión lógica.

198
Tabla C.3: Operadores Lógicos. Los ejemplos suponen que las variables a = true y b = false. El valor
más a la derecha es el resultado de la expresión

Op. Uso Devuelve true si. . . Ejemplo


&& op1 && op2 op1 y op2 son ciertos a && b false
|| op1 || op2 op1 o op2 son ciertos a || b true
! ! op op es falso !a false
& op1 & op2 op1 y op2 son ciertos (op2 se evalúa) a&b false
| op1 | op2 op1 o op2 son ciertos (op2 se evalúa) a|b true
ˆ op1 ˆ op2 op1 y op2 son distintos aˆb true

Tabla C.4: Tabla de verdad para los operadores lógicos Y, O, NO y O exclusivo. Las letras T y F
representan a las constantes true y false respectivamente. Como es natural la tabla de verdad de los
operadores & y | serı́a equivalentes, respectivamente, la de los operadores && y ||

p q p && q p || q !p pˆq
F F F F T F
F T F T T T
T F F T F T
T T T T F F

C.4. Operadores de Bits


En la Tabla C.5 se muestra la descripción de todos los operadores a nivel de bit. Esta clase de
operaciones manipulan directamente los bits de sus operandos enteros, bien haciendo operaciones lógicas
o desplazamientos con y sin signo.
Son especialmente útiles las operaciones lógicas a nivel de bit para representar con un solo entero
varias condiciones o flags. La idea es que cada bit del entero represente uno de esos flags, cuando el bit
está a 1 el flag está activo. Para verificar si un flag está activo basta con usar el operador & entre la
variable que recoja el estado actual de todas las condiciones o flags y una constante con todos los bits a
0 salvo el bit del flag del cual queremos verificar su estado.

Tabla C.5: Operadores a nivel de bits

Op. Uso Descripción Ejemplo


˜ ˜op1 Complemento a nivel de bit ˜7 −8
& op1 & op2 Y a nivel de bit 5&3 1
| op1 | op2 O a nivel de bit 5|3 7
ˆ op1 ˆ op2 O exclusivo a nivel de bit 5ˆ3 6
 op1  op2 Desplaza op1, op2 bits a la izq (signo) 61 12
 op1  op2 Desplaza op1, op2 bits a la dch (signo) 61 3
≫ op1 ≫ op2 Desplaza op1, op2 bits a la dch (sin) −1 ≫ 1 2147483647

C.5. Operadores de Asignación


Además del operador de asignación clásico, existen otros muchos operadores de asignación que com-
binan una operación aritmética, lógica o de desplazamiento, junto con una asignación. En la Tabla C.6
se muestra todos ellos. Como en todas las asignaciones, el valor que devuelve la expresión, es el valor
que se asigna a la variable de la izquierda (left-value).

199
Tabla C.6: Operadores de Asignación. Los ejemplos suponen que la variable entera a tiene valor 5 antes
de la operación. El valor a la derecha es el resultado de la expresión y por tanto el valor que se asigna a
la variable a
Op. Uso Equivalencia Ejemplo
= op1 = op2 Asignación elemental a= 7 7
+= op1 + = op2 op1 = op1 + op2 a+ = 7 12
−= op1 − = op2 op1 = op1 − op2 a− = 7 −2
∗= op1 ∗ = op2 op1 = op1 ∗ op2 a∗ = 7 35
/= op1 / = op2 op1 = op1 / op2 a/ = 7 0
%= op1 % = op2 op1 = op1 % op2 a %= 7 5
&= op1 & = op2 op1 = op1 & op2 a& = 7 5
|= op1 | = op2 op1 = op1 | op2 a| = 7 7
ˆ= op1 ˆ= op2 op1 = op1 ˆ op2 aˆ= 7 2
= op1 = op2 op1 = op1  op2 a 1 10
= op1 = op2 op1 = op1  op2 a 1 2
≫= op1 ≫= op2 op1 = op1 ≫ op2 a≫ 1 2

C.6. Otros Operadores


Además de todas las clases de operadores antes descritas, existen otros operadores que no se corres-
ponden precisamente con operaciones aritméticas, lógicas o relacionales, pero que tienen gran importancia
en el lenguaje y que se usan con mucha frecuencia. La descripción de la utilidad de cada uno de ellos
está recogida en la Tabla C.7.

Tabla C.7: Otros Operadores

Operador Uso Descripción


( ? : ) (op1?op2:op3) Devuelve op2 si op1 es cierto, y si es falso op3
new new tipo Devuelve la referencia de un nuevo objeto de tipo
[ ] op1[op2] Elemento de ı́ndice op2 del vector op1
. op1.op2 Atributo o método op2 del objeto op1
( par ) op(parámetros) Llamada al método op con los parámetros indicados
( tipo ) (tipo)op Convierte op al tipo tipo
instanceof op1 instanceof op2 true si op1 (objeto) es una instancia de op2 (clase)

200
Apéndice D

Precedencia y Asociatividad

Cuando en una expresión aparecen varios operadores, el orden en que dichos operadores se evalúan
depende de su precedencia y asociatividad. Recordemos la definición de ambos conceptos:

Precedencia: indica, cuando aparecen varios operadores de distinto tipo, cuál debe aplicarse
primero

Asociatividad: indica, cuando aparecen varios operadores del mismo tipo, cuál debe aplicarse
primero

Tabla D.1: Operadores de Java, ordenados según su precedencia

pred. grupo operadores operación asoc.


() llamada a método
1 acceso [] corchete (acceso vector) ID
. punto (acceso miembros)
2 postfijos ++ −− post- incremento y decremento ID
++ −− pre- incremento y decremento
+ − suma y resta (unaria)
3 prefijos ! ˜ NO lógico o nivel de bit DI
new instanciación objeto o vector
(tipo) conversión de tipo
4 multiplicativos ∗ / % multiplicación, división y resto ID
5 aditivos + − suma y resta (binaria) ID
6 desplazamientos << >> >>> con signo izq., con signo der., sin signo der. ID
< > <= >= menor, mayor, menor o igual, mayor o igual
7 relacionales ID
instanceof comparación de tipos
8 comparación == != igual que, distinto que ID
9 and & Y lógico a nivel de bits ID
10 xor ˆ O-ex lógico a nivel de bits ID
11 or | O lógico a nivel de bits ID
12 and && Y lógico condicional ID
13 or || O lógico condicional ID
14 condicional ?: operador condicional ternario DI
= asignación elemental
∗= / = %= asignación + operador multiplicativo
15 asignación += −= asignación + operador aditivo DI
<<= >>= >>>= asignación + operador desplazamiento
&= ˆ= | = asignación + operador lógico

201
La Tabla D.1 muestra todos los operadores del lenguaje ordenados según su precedencia. Los grupos
que aparecen primero en la tabla tienen más precedencia que los que aparecen a continuación, mientras
que los operadores de un mismo grupo tienen la misma precedencia. En ese caso, su orden de ejecución
depende de su asociatividad, mostrada en la columna de la tabla más a la derecha. Como se puede ver
todos los grupos tienen asociatividad de izquierda a derecha (ID), salvo los unarios (postfijos, prefijos y
de creación o conversión) y los de asignación que la tienen de derecha a izquierda.
Ejemplo: si tenemos la expresión 4 + 5 ∗ 7, aparecen dos operadores ∗ y + que pertenecen a distintos
tipos, uno es multiplicativo y el otro aditivo. En este caso el ∗ tiene más precedencia, con lo que primero
se hace la multiplicación (35) y luego la suma, produciendo el resultado entero 39. Si se quiere que la
suma se haga primero se deben usar paréntesis.
Los operadores que pertenecen al mismo tipo tienen la misma precedencia. En el caso de que en una
expresión aparezcan varios operadores de un mismo tipo, por ejemplo multiplicativos, el orden en que
se aplicarán depende de su asociatividad. Si ésta es de izquierda a derecha (ID), primero se ejecutará el
operador que se encuentre más a la izquierda. Si por el contrario, la asociatividad es de derecha a izquierda
(DI), primero se aplicará el operador más a la derecha.
Ejemplo: si tuviéramos la expresión 4 ∗ 5/7, estarı́amos en una situación en que ambos operadores
tienen la misma precedencia, ambos son multiplicativos, y se aplicarı́an según su asociatividad, de ID.
Primero la multiplicación (20) y luego la división entera, produciendo el valor entero 2.
A veces ocurre que, cuando se está escribiendo alguna expresión, el programador no recuerda exac-
tamente la información de precedencia y asociatividad que se refleja en la Tabla D.1. Algunas reglas
simples pueden ayudar a no cometer errores en estas situaciones:

Los paréntesis cambian la precedencia, hacen que lo que se encuentra entre paréntesis se haga antes.
Cuando se duda entre dos operadores cuál tiene más precedencia, se deben usar paréntesis para
agrupar la parte de la expresión que se quiere ejecutar primero.

Los operadores unarios tienen más precedencia.


Los postfijos más que los prefijos.
Los multiplicativos (∗, /, %) más que los aditivos (+, −), tanto los numéricos, con los lógicos (&&
ó & más que || y | respectivamente).

Los relacionales más que los comparativos.


Los relacionales y comparativos más que los lógicos.
Los de asignación son los que menos precedencia tienen.

202
Apéndice E

Imprimir en consola: printf()

El método printf() permite realizar un impresión con formato en la consola. Es un método con dos
parámetros:

1. Una cadena de texto que incluye texto e instrucciones de formato. Es obligatoria, debe aparecer
siempre.

2. La lista de las variables o expresiones que se quieren imprimir. No es obligatoria incluirla, y puede
estar formado por varias expresiones separadas por comas.

sintaxis Método printf():


printf( cadena-de-texto, expr1, expr2, . . . );

Ejemplo:
System.out.printf("\nUn real: %f\tUn entero: %d", a, b);

La llamada al método printf() del ejemplo imprime: 1o ) Un salto de lı́nea, 2o ) la cadena “Un real:
”, 3o ) el valor de la variable a, formateado como un real en formato decimal, 4o ) un tabulador, 5o ) la
cadena “Un entero: ”, y finalmente 6o ) el valor de la variable b formateado como un entero en formato
decimal.
Como se ve, la parte más difı́cil de dominar es la cadena de texto que constituye el primer parámetro.
Básicamente es una cadena que contiene texto que se desea imprimir, mezclado con información de
formato en las posiciones adecuadas para imprimir la lista de expresiones que se desea.
El texto de formato puede contener:

1. Texto normal: se imprimirá tal cual.

2. Especificadores de formato: implican la impresión de un dato.

3. Secuencias de escape: permiten imprimir caracteres especiales.

E.1. Especificadores de formato


Entre el texto que se desea imprimir se van insertando los especificadores de formato que indican la
forma en la que las expresiones del final deber ser impresas. Tiene la siguiente sintaxis:

sintaxis Especificadores de formato:

%[argumento$][indicadores][ancho][.precision]conversión

203
Como se aprecia, empieza siempre por el carácter % y el resto de elementos son opcionales, salvo la
conversión. Veamos cada elemento con detalle:

argumento: puede ser el número del argumento que se va a imprimir, seguido del carácter $.
Nomalmente este elemento no se incluye, y los datos se escriben en orden. En la posición del
primer especificador de formato se imprime el valor de la primera expresión, en el segundo el de
la segunda, y ası́ sucesivamente. Solamente resulta útil cuando una misma expresión se quiere
imprimir en varios sitios, tal vez con distinto formato.
indicadores: sirven para modificar el significado de la especificación de la conversión. Pueden
ponerse varios. Los indicadores más interesantes están en la Tabla E.1.

Tabla E.1: Lista de indicadores


indicador descripción
− justificación a la izquierda (derecha por defecto)
+ debe aparecer con signo
0 se incluirán ceros como relleno
( valores negativos entre paréntesis

ancho: número mı́nimo de caracteres para el formato. Si el resultado de la conversión tiene menos
caracteres, por defecto se rellenan con espacios.
precision: es un valor entero que representa, para las conversiones e y f, el número de dı́gitos de
la parte decimal, y para g el número total de dı́gitos. No se puede usar con otras conversiones.
conversión: es el único elemento obligatorio. Indica como dar formato al valor de la expresión que
se va a imprimir en ese punto. Los más importantes están en la Tabla E.2.

Tabla E.2: Lista de conversiones de formato


formato descripción
d imprime un entero en formato decimal
o imprime un entero en formato octal
x imprime un entero en formato hexadecimal
e imprime un número real en notación cientı́fica
f imprime un número real en formato decimal
g imprime un real en formato e ó f (el más adecuado)
c imprime un carácter Unicode
s imprime una cadena de caracteres
b imprime “true” o “false” en función del valor booleano
% imprime el carácter %
t conversiones de hora y fecha

E.2. Secuencias de escape


Por último, otros elementos muy empleados con el método printf() son las secuencias de escape.
Permiten imprimir caracteres especiales, o caracteres que no se pueden introducir dentro del propio
texto por diversos motivos, por ejemplo las propias comillas dobles ya que es el carácter usado como
delimitador de las cadenas de texto. En la Tabla B.2, dentro del Apéndice dedicado a las constantes,
aparecen las más usadas.

204

Vous aimerez peut-être aussi