Manual Imprescindible
2.ª EDICIÓN
Curso de programación
Java
Mariona Nadal Farré MULTIMEDIA
M
Manual Imprescindible
MI
Java
Curso de programación
Mariona Nadal Farré
Manual Imprescindible
Realización y adaptación de cubierta: Celia Antón Santos
Diseño de maqueta: Laura Apolonio Guerra
Revisión: Gelsys M. García Lorenzo y Gustavo Pérez
Maquetación: Claudia Valdés-Miranda Cros
Responsable editorial: Eugenio Tuya Feijoó
Primera edición electrónica: 2021
Todos los nombres propios de programas, sistemas operativos, equipos
hardware, etc., que aparecen en este libro son marcas registradas de sus
respectivas compañías u organizaciones.
Reservados todos los derechos. El contenido de esta obra está protegido
por la Ley, que establece penas de prisión y/o multas, además de las
correspondientes indemnizaciones por daños y perjuicios, para quienes
reprodujeren, plagiaren, distribuyeren o comunicaren públicamente, en
todo o en parte, una obra literaria, artística o científica, o su transformación,
interpretación o ejecución artística fijada en cualquier tipo de soporte o
comunicada a través de cualquier medio, sin la preceptiva autorización.
Imágenes no aportadas por la autora: © 2021 iStockphoto LP/ Getty Images
Edición española:
© EDICIONES ANAYA MULTIMEDIA (GRUPO ANAYA, S.A.), 2021
Calle de Juan Ignacio Luca de Tena, 15, 28027 Madrid.
ISBN: 978-84-415-4425-3
Edición digital sobre la 1.ª edición reimpresión revisada y actualizada
A todos mis júniores, tanto a aquellos que disfrutaron con el «macetohuerto», como
los que lo sufrieron (incluso a los que ni se enteraron de que lo hicimos). ;)
¡Con vosotros empezó todo!
Mariona Nadal Farré
SOBRE LA AUTORA
Mariona Nadal Farré (Barcelona, 1980) es ingeniera informática por la Universidad
Politécnica de Madrid (2003).
Cuenta con más de 20 años de experiencia en la práctica del desarrollo de aplicaciones
Java en entornos empresariales para grandes clientes internacionales, experiencia que
le ha permitido ser formadora en J2EE de jóvenes ingenieros en su primer empleo
como programadores.
También es instructora de LinkedIn Learning, donde cuenta con un número creciente
de cursos sobre Fundamentos de la Programación y Java.
Su manera de escribir fresca, directa y realista hace sus cursos amenos, claros y útiles,
llevándote a través de la práctica a un estilo de programación de fácil mantenimiento
y alta empleabilidad.
En [Link] encontrarás todos sus cursos y más información.
Web DSR School Cursos en LinkedIn Learning
[Link]/dsrschoool [Link]/lilmnadal
gracias Gracias a mi madre, Reyes, por regalarme,
con gran esfuerzo, mi primer ordenador. A
Fernando, por retarme a usarlo más y mejor.
Gracias a Julio, por empujarme a aprender
Java fuera del temario oficial y por acompa-
ñarme en mi aprendizaje de la programación;
y a Carmen, por convertirme en una progra-
madora profesional.
Gracias a quien decidiera que yo me encar-
gara durante un tiempo de las formaciones
de los nuevos júniores, y a todos ellos por
enseñarme mientras aprendían.
Gracias a Carlos por ficharme como instruc-
tora en LinkedIn Learning; y a Eugenio, por
ofrecerme la oportunidad de escribir este libro.
Gracias a Gelsys y Claudia, y a Celia y el
resto del equipo de Anaya Multimedia, por
convertir unos textos en esta obra.
Gracias a mis hermanos, Guillem y Pol, por
alejarme del ordenador regularmente.
Gracias también a Eduardo, Pablo, Mike,
Ricardo, Xavier y a Quines Mones, a cada uno por
sus distintas contribuciones y apoyos durante mis
procesos creativos.
Gracias a todas aquellas personas que, aunque no mencione explícita-
mente, han hecho sus aportaciones, a lo largo de mi vida, que me han llevado a escribir este volumen.
Y, por supuesto, también gracias a todo el personal involucrado en la impresión, distribución,
exposición, reposición, venta, recomendación, reparto… sin su trabajo no sería posible que tengas
hoy en tus manos este manual.
í
Cómo usar este libro
Introducción
ndice de
contenidos
16
20
PARTE 1. Estructuras básicas 23
1. Mi primer programa 25
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Creando una clase en Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
El método main en Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Ejecutar el método main de Java desde la línea de comandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
¿Qué puede salir mal con el método main de Java? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Mostrando mensajes al usuario en Java ([Link]). . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2. Argumentos, variables, métodos y operadores 41
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Argumentos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Los argumentos del main y las secuencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
¿Cómo ejecutar el main con argumentos de texto?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Declaración y uso de variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Tipos de las variables en Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Métodos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Declaración de métodos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Uso de métodos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8
Operadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Instrucción de asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Operadores aritméticos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Operadores relacionales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Operadores lógicos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Operadores bit a bit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Soluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3. Estructuras condicionales 73
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Una condición: si (if)… si no (else)…. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Varias condiciones: si no si (else if)…. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Constantes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Muchas condiciones: switch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Cuando no hace falta el if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
El operador ternario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
4. Estructuras iterativas 95
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Bucle «para cada» elemento de una colección (for each). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Bucle «para» (for) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Bucle «mientras» se cumpla la condición (while) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Bucle «haz mientras» se cumpla la condición (do while). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
La clase Scanner. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Bucles anidados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Caracteres especiales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Soluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
5. Proyecto «piedra, papel, tijeras» 121
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Presentación del programa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Preparar el esqueleto y las constantes necesarias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Mostrar instrucciones al usuario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Generar jugada del ordenador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Recoger jugada del usuario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Interpretar jugada del usuario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Calcular el ganador de la jugada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Mostrar el resultado de la jugada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Hacer el juego repetitivo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Mejoras y evoluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Índice de contenidos 9
PARTE 2. Orientación a objetos 139
6. Diseño orientado a objetos 141
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Requisitos del proyecto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Extracción de conceptos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Boceto del modelo conceptual. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Representación del modelo conceptual en UML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Soluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
7. Clases y objetos 151
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
Paquetes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
Interfaces. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
Las interfaces de sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
Clases abstractas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Las clases abstractas de sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Clases «normales». . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Las clases de sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Enumerados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
Los enumerados de sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
Objetos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
Los objetos de sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
Los objetos en la máquina virtual de Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
8. Relaciones orientadas a objetos 175
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
La herencia en sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Sobrecarga y sobrescritura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Sobrescritura de toString en el sOOPer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Algoritmo de distribución de productos en contenedores en sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . 188
Reparto de responsabilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Reparto de responsabilidades en sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Diagrama de secuencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Diagrama de secuencia del método addProducto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Detección y corrección de problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Detección y corrección de problemas en sOOPer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Probado no exhaustivo de la implementación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
10 Índice de contenidos
Modificadores de Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Ejemplo estático. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
9. Proyecto «macetohuerto» 215
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Presentación del proyecto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Extracción de conceptos del macetohuerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
Estructura de paquetes del macetohuerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Interfaces del macetohuerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Interfaz IHuerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Interfaz IMaceta. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Enumerado FormaMaceta. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Interfaz IPlanta. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Enumerado Familia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Enumerado Especie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Clases de la estructura de macetas del macetohuerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Clase abstracta Maceta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Clase MacetaRectangular . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Clase MacetaTubular . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Clases de la estructura de plantas del macetohuerto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
Clase abstracta Planta. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
Clase abstracta PlantaAromatica. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Clase abstracta PlantaFruto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Clase abstracta PlantaHoja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Clase abstracta PlantaRaiz. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Clase Hinojo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Clase Lechuga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Clase Perejil. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Clase Tomate. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Clase TomateCherry. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Nueva implementación de la clase Tomate. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Clase Zanahoria. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
El huerto y sus procesos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Clase Huerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Método plantar de la clase Maceta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Método esCompatible de la clase Planta. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
Método tengoEspacio de la clase Planta. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
Método tengoEspacio de la clase PlantaRaiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Clase Sistema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Índice de contenidos 11
PARTE 3. Buenas prácticas 243
10. Manejo de excepciones 245
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
¿Qué son las excepciones?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
¿Qué es una excepción?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
Tipos de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
Naturaleza de las excepciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
API de las excepciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Ejemplo práctico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
¿Cómo manejar las excepciones?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
¿De dónde vienen las excepciones? throw. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
¿Son contagiosas? throws . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
¿Pero no se pueden tratar? try / catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
¿Pero no se pueden tratar? (Más complicado) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
¿Siempre hay que tratarlas? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
¡Finalmente! finally. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Con recursos (try). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Multicaptura (catch). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Ejemplo práctico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Malas prácticas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Comérsela con patatas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Comérsela con lechuga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Perder la memoria histórica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Generalizar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
Conclusión. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Soluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
11. Depuración 295
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
Cómo depurar en Eclipse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
Depurando Supermercado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
Para qué sirve depurar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
12. Test unitarios 305
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306
Test Driven Development. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306
JUnit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
Esqueleto de un test unitario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
Primeros test unitarios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
12 Índice de contenidos
Detección de regresiones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
Elección de los test unitarios adecuados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
Testando cosas que van mal. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
Objetos maquetados (mocks). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Clase Pitagoras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Clase SuperCalculadora. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Clase de test SuperCalculadoraTest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Configuración para utilizar Mockito. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Clase de test PitagorasTest utilizando Mockito. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
13. Trazas de ejecución 329
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Niveles de prioridad. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Configuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Uso. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
Buenas prácticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
Ejemplo práctico. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Dependencias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Configuración. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
Clase de ejemplo [Link] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335
Salida por consola. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
Ficheros de logs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
14. Proyecto «Gestión de récords» 341
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
Funcionalidad básica del programa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Clases RecordsManager. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Validaciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
Clase RecordsManager. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
Clase PlayerNameTooShortException. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
Clase ScoreTooLowException. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
Control de errores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
Clase RecordsManager. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
[Link] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Salida por consola. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
Fichero de salida ([Link]). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
Test unitarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
Clase RecordsManagerTest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
Mejoras y evoluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Índice de contenidos 13
PARTE 4. Datos en Java 357
15. Estructuras de datos 359
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
Cadenas de texto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
Clase String. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
Clase Character . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Clases StringBuffer y StringBuilder. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
Internacionalización y localización. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
Clase MessageFormat. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364
Números. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Autoboxing y unboxing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Grandes números y alta precisión. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Clase NumberFormat. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
Fechas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Fechas a la antigua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Fechas a partir de Java 8 (paquete [Link]) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Colecciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
Tipos genéricos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
Funciones hash. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Diccionarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
16. Bases de datos: Mapeo Objeto-Relacional 393
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
Herramientas necesarias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
Configuración del proyecto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Entidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
Entidad Pedido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
Objetos de Acceso a Datos (DAO). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
Interfaz Dao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
Gestor de entidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
El DAO abstracto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Los DAO del gestor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
Consultas simples. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
Consulta del pedido más reciente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
Consulta de pedidos de la semana pasada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
Relaciones 1:N. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415
Entidad Albaran. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415
El DAO de Albaran. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
14 Índice de contenidos
Relaciones 1:N. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Entidad Factura. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
El DAO de Factura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Relaciones M:N. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Entidad Producto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421
Gestor de Pedidos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
Relaciones M:N bidireccionales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
Consultas con criterios de búsqueda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
Pros y contras de criterios y queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
Recuperando datos sin consultas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
Clase de prueba SinQueries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
Las queries que no vemos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427
Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
17. Expresiones lambda y Streams 433
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Expresiones lambda. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Calculadora lambda. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Cuentas varias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Test y ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
Soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
18. Proyecto «otra reunión más» 441
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
Código de base. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
Cartel de sala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
Clase ReunionDao. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Clase CartelSala. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Informe de reunión. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
Clase InformeReunion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
Informes de todas las reuniones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
Clase InformeReuniones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
Mejoras y evoluciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
Esto es solo el principio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
Índice alfabético 452
Índice de contenidos 15
Cómo usar este libro
A quién va dirigido y qué es necesario para empezar
Este libro va dirigido principalmente a programadores noveles de Java, que comienzan
a dar sus primeros pasos en el mundo de la programación. Empezamos desde cero para,
paso a paso, lograr el desarrollo de pequeños programas.
Los programadores con cierta experiencia también pueden usar este libro para afianzar
algunos conceptos más complejos, yendo directamente al capítulo que necesiten o,
para practicar test y ejercicios, yendo al final de cada capítulo o a los capítulos finales
de cada parte.
Hablo de programadores noveles y de programadores con cierta experiencia, pero ¿verdad
que no he dicho programadores jóvenes o mayores? Porque poco importa la edad para
empezar a programar. Este libro está pensado para ayudar a aprender a programar a:
• Niños (y niñas, evidentemente) mayores y adolescentes, que como actividad extraescolar
o curricular quieren sacarle el máximo partido a su razonamiento lógico y prepararse
para un mundo que nos exige ser capaces de afrontar y resolver problemas.
• Jóvenes estudiantes, ya estén estudiando programación ya sean de otras ramas del
conocimiento: para casi cualquier profesión del futuro saber programar será un plus
muy valorado.
• Adultos, profesionales o aficionados, programadores de otros lenguajes o con otras
profesiones, reciclarse y seguir aprendiendo forma parte de nuestro día a día.
• Mayores, con más tiempo disponible y aún mucha curiosidad vital. Nunca es tarde
para lanzarse a programar.
Para seguir los ejemplos y los ejercicios del libro necesitarás preferiblemente un ordenador con:
• JDK (kit de desarrollo Java), disponible en la web [Link]. No es suficiente con
la máquina virtual de Java (JVM) que suele estar ya instalada. Entre otras utilidades,
incluye los ejecutables javac y java para compilar y ejecutar el código sin utilizar
otras herramientas.
• Un IDE (entorno de desarrollo integrado), que es un editor de texto adaptado para
programadores que integra muchas funcionalidades útiles, como la de compilar y
ejecutar el código. No es imprescindible, pero, según van creciendo los proyectos,
se agradece tenerlo. En este libro he utilizado Eclipse IDE, pero, si prefieres otro,
también te sirve. Puedes descargar Eclipse en [Link].
Si no puedes instalar estas herramientas, pero tienes acceso a internet, busca «java
compiler online» y encontrarás páginas web que te permitirán probar tu código online,
desde una tablet o un móvil. Si bien no son los dispositivos más adecuados para
programar, sí te servirán para seguir aprendiendo.
16
Estructura del libro
Esta obra se divide en cuatro partes. Cada una de ellas trabaja tres o cuatro temas relacionados.
El último capítulo es un proyecto práctico con el que afianzar los conocimientos adquiridos.
La primera parte nos introduce al mundo de la programación en general mediante la
programación estructurada, es decir, los if, for, métodos y variables... ese tipo de cosas básicas.
Si es la primera vez que vas a programar es imprescindible que empieces por aquí. Si ya sabes
programar en algún otro lenguaje, te convendría al menos una lectura rápida para asociar tus
conocimientos previos con sus equivalentes en Java. Si ya has hecho tus pinitos en Java,
tampoco está de más que le eches un ojo, aunque quizá no sea lo más prioritario.
La segunda parte está dedicada a la orientación a objetos que es el paradigma de programación
que realmente se utiliza en Java. Aprenderás desde cómo extraer los conceptos de un problema
hasta convertirlos en una jerarquía de clases, aprendiendo a repartir funcionalidades, la
reutilización de código mediante herencia, la sobrescritura y la sobrecarga de código.
En la tercera parte veremos buenas prácticas que quizá no son necesarias en prácticas
académicas o proyectos personales, pero sí son muy importantes en proyectos profesionales.
Por favor, dedícale especial energía al tema de las excepciones. Era el tópico en el que mis
alumnos siempre querían profundizar más porque por lo general se trata muy superficialmente.
Y sin entender bien las excepciones, no es posible manejarlas bien. Depuración, test unitarios
y trazas de ejecución contribuyen a hacer el código mantenible.
Terminamos con la cuarta parte dedicada a los datos. En ella se explican tanto las estructuras
de datos que se utilizan durante la ejecución de los programas (textos, números, colecciones...)
como el acceso a las bases datos que almacenan de forma persistente la información, sin olvidar
las expresiones lambda y los streams.
Dentro de los capítulos «de teoría», además de los ejemplos, encontrarás test para afianzar
conocimientos y ejercicios para ponerlos en práctica. Intenta hacerlos antes de mirar las
respuestas, pero no te saltes la lectura de la solución propuesta porque puede que te explique
algo nuevo ahí mismo. Sin embargo, el capítulo final de cada parte es un proyecto en sí, a
desarrollar tú y yo juntos. Esta vez, sin test ni ejercicios adicionales.
Convenios utilizados en este libro
Para facilitar la comprensión de este manual se han utilizado algunos formatos especiales:
• Los nombres de comandos, menús, opciones, cuadros de diálogo y otros elementos
aparecen en letra «de palo seco» para distinguirlos fácilmente del resto del texto, por
ejemplo, la instrucción [Link].
• Las combinaciones de teclas aparecen separadas por un guion y también en un tipo de
letra diferente; por ejemplo, Ctrl-Mayús-O.
• Para indicar la secuencia para ejecutar una opción determinada, se ha decidido abreviar
su escritura presentando la secuencia de menús u opciones en el orden el que deben
Java. Curso de programación 17
seleccionarse, separados por el signo «mayor que» (>). Por ejemplo, en lugar de indicar
que seleccionemos la opción Perspectiva del menú Ventana y luego la opción Abrir
Perspectiva, indicaremos directamente Ventana>Perspectiva>Abrir Perspectiva.
• Los fragmentos de código los verás como textos de ancho fijo, al igual que los resultados
de la ejecución de los programas, el color del texto te dará la pista para saber de qué
tipo se trata:
fragmento de código
T0.0 resultados de ejecución
• Los test y ejercicios se encuentran al final del capítulo, pero en el momento en el que ya
E0.0 sabes lo imprescindible para resolverlos, encontrarás estos iconos referenciando el número
de test (blanco sobre negro) o de ejercicio (negro sobre blanco) que ya puedes intentar.
• A lo largo del libro aparecen notas informativas separadas del texto principal que
proporcionan información, tales como aclaraciones, advertencias, curiosidades o consejos:
NOTA:
Para facilitar o concretar información relacionada con el tema abordado. Incluyen recomendaciones
que conviene tener en cuenta.
ADVERTENCIA
Para evitar posibles errores como consecuencia de una operación mal realizada.
TRUCO:
Consejos y artimañas para facilitar el trabajo o conseguir mejores resultados.
Información de soporte
Los ejemplos, ejercicios y proyectos desarrollados en este libro están disponibles en el
repositorio git de acceso público [Link]
Repositorio en git
[Link]/gitMIJava
Puedes descargarte
todo en un fichero zip,
pero te recomiendo aprender las
operaciones básicas de git para
poder sacarle el máximo provecho,
pues algunos de los ficheros
Figura C.1. Histórico de un fichero.
pueden tener varias versiones, en
función de cómo vamos trabajando sobre ellos. En esta figura se aprecian las dos versiones
18 Cómo usar este libro
del primer ejemplo. Coge la primera (normalmente la de más abajo) y trabaja sobre ella
para lograr llegar a la de más arriba, que ya contiene la solución.
Algunas nociones para empezar a utilizar el repositorio:
• La interfaz de gitHub puede cambiar sin previo aviso, pero además de ver el código en
la propia web, mediante el botón Code es posible clonarlo parar tener tu propia versión
del repositorio o descargártelo como zip.
• Viendo un fichero en la web, el botón History te permitirá conocer todos los commits hechos
sobre ese fichero. Cuando cojas una versión en concreto, verás con el fondo blanco las
líneas de código sin cambios respecto a las anteriores, con fondo rojo y un signo menos,
las líneas que estaban antes y ya no están, y con fondo verde y un signo más, las nuevas.
Si una línea ha sido modificada, aparecerá la versión antigua en rojo y la nueva en verde.
• El título de cada commit te dará una pista de en qué sección del libro se modifica ese
fragmento de código.
Recomendaciones y buenas prácticas
Durante toda la obra te llevo de la mano para ir aprendiendo nuevos conceptos, te propongo
ejemplos y ejercicios para afianzar esos conocimientos, pero ¡hay que ser valiente! ¡Suéltate
de mi mano y avanza más! Si te muestro un ejemplo que calcula el máximo, intenta por tu
cuenta calcular el mínimo. Si durante el ejercicio se te ocurren mejoras o retos, ¡hazlos! El
espacio en el libro es limitado, tu capacidad de aprendizaje es infinita.
Puede parecer extraño, pero conviene que tengas papel y lápiz a mano. Poder tomar apuntes,
hacer esquemas y diagramas ayuda, y mucho, a la hora de programar.
Tienes todos los ficheros disponibles para descargar, pero si de verdad quieres aprender a
programar, escríbelos tú. Como los cuadernos de caligrafía de la escuela, escribir letra a letra
los programas te llevará a asimilar lo que estás escribiendo. Olvídate de copiar y pegar hasta
que no sepas perfectamente qué estás copiando y pegando.
Para generar buenos programas, debemos ordenar nuestra mente, pero también nuestros
ficheros. Clasifícalos bien en carpetas, da nombres significativos tanto a carpetas como a los
ficheros, de forma que te resulte fácil recuperar lo que estés buscando.
Contacta conmigo
Tus sugerencias, comentarios, correcciones, propuestas… son una fuente de crecimiento para
mí y mis contenidos. Puedes escribirme a school@[Link]. Procuraré contestar tan pronto
como las circunstancias tengan a bien. También puedes seguirme en LinkedIn para estar al
día de mis nuevas publicaciones.
También puedes seguirme en Twitter (@MarionaJava) o en LinkedIn (marionanadal) para
estar al día de mis nuevas publicaciones.
Java. Curso de programación 19
i
Introducción
Has elegido aprender Java. Me parece muy buena idea. Yo también lo hice a finales del
siglo pasado. Quizá algún amigo o conocido te diga que hubiera sido mejor escoger algún
otro lenguaje más «de moda». Puede ser. Depende de tus objetivos, claro.
Java es un lenguaje ya veterano, pero piensa que sigue habiendo demanda de
programadores COBOL, que es un lenguaje creado en 1959, que se ha utilizado en sistemas
de gestión muy importantes (banca, seguros…) que siguen funcionando muy bien y
continúan necesitando profesionales que los mantengan y evolucionen.
Java va por el mismo camino. En el prefacio de la segunda edición del libro Java:
Fundamentos de programación, de Judy M. Bishop, de 1998, uno de los que utilicé para
aprender yo este lenguaje que tanto me ha dado, decía: «…Java está revolucionando el
mundo de la computación y está camino de convertirse en el lenguaje a enseñar a todos
los estudiantes de programación del mundo entero…». Parece que Judy tenía razón.
Mi otro libro de referencia fue Thinking in Java de Bruce Eckel, también de 1998. Su prefacio
decía: «…originalmente me acerqué a Java como simplemente “otro lenguaje de
programación”, que en muchos sentidos lo es. Pero a medida que pasaba el tiempo y lo
estudiaba más profundamente, comencé a ver que la intención fundamental del lenguaje
es diferente a todos los demás lenguajes que he visto».
Ya lo digo en la contraportada: «Quizá Java no sea el lenguaje aparentemente más sencillo
para empezar, porque es estricto y fuerza a hacer las cosas bien. Aprender a programar
en Java permite hacerlo con solidez y enfrentarse a otros lenguajes».
Mi objetivo principal con este curso es ayudarte como programador a generar código de
calidad y fácil de mantener, y por desgracia... es necesario. Hay demasiado código de
mala calidad desperdigado por el mundo. Y te vas a encontrar con él. Es una pena. Solo
espero que, a partir de ahora, tú no generes más mal código y para ello necesitas una
buena base. Así que también veremos cosas que se suelen hacer mal y que tú entenderás
por qué están mal y por qué no deberías hacerlas.
No quiero enseñarte solo Java. Quiero que aprendas a programar bien y prepararte para
formar parte de un equipo de desarrollo. Java es solo la herramienta.
¡Espero que disfrutes del camino!
Java. Curso de programación 21
1
Parte
Estructuras
básicas
1 Mi primer programa
En este capítulo aprenderás a:
• Crear una clase Java.
• Identificar y generar el método main.
• Compilar una clase Java
• Ejecutar una clase Java.
• Mostrar textos al usuario.
Introducción
Java es un lenguaje orientado a objetos, pero antes nos centraremos en las estructuras
básicas de programación (if, for…), comunes en casi cualquier lenguaje de programación,
y olvidaremos las clases y los objetos hasta de aquí a unos capítulos, cuando ya hayamos
asentado la programación estructurada.
Para hacer los ejercicios, utilizaremos un fichero distinto para cada uno, que será una
clase Java diferente, con un método main (principal), que es el método que se ejecuta
cuando ejecutamos una clase Java.
En el futuro, cuando hagas programas de verdad (pronto, no te preocupes), apenas usarás
métodos main, pero ahora para aprender nos vendrá muy bien. No necesitaremos
servidores ni otras complicaciones.
Cuando programamos, es muy importante que escribamos todo correctamente. En un
escrito que va a leer un humano (una carta, una redacción, un libro), quedan muy mal
las erratas y las faltas de ortografía, pero aun así, el humano que lo lea, si conoce el
idioma, lo entenderá.
«Buennos dias, qué Tal?». No está bien escrito, pero lo entiendes, ¿verdad? Pues un
ordenador, no. No lo entendería. Por eso, tenemos que poner mil ojos en poner bien todas
las letras y símbolos e ir con mucho cuidado con las mayúsculas y minúsculas.
Java es un lenguaje que distingue mayúsculas y minúsculas (otros lenguajes no lo hacen),
y tú verás, ¡si no las respetas, no te funcionará nada! Aún recuerdo en la facultad ver a
un profesor suspender un examen en cuestión de segundos porque estaba todo escrito
en MAYÚSCULAS cuando el lenguaje usado tenía sus palabras clave en minúsculas
(como en Java). Supongo que a ese estudiante le pareció superinjusto ese resultado, pero
espero que aprendiera la importancia de este punto.
Lo dicho, fíjate bien, porque cada parte es muy importante: si te confundes en algún
detalle, puede que no funcione.
Creando una clase en Java
Empezaremos con un programa sencillo, en el que mostraremos un mensaje de texto,
con el que aprenderemos las palabras clave necesarias.
Vamos a crear nuestra primera clase en Java, esto es, un fichero normal de Java.
Crea un proyecto en tu entorno de desarrollo (IDE), para ir metiendo todos los ejercicios.
Llámalo PracticasIniciacion (sin acentos, sin espacios y respetando mayúsculas y
minúsculas). Aunque normalmente deberíamos separar los ficheros fuente y compilados,
para este curso los dejaremos juntos.
26 Mi primer programa
Dentro de este proyecto, crea una nueva clase y llámala Ejemplo01_01. Presta atención a
mayúsculas y minúsculas, no incluyas acentos, ni espacios, ni caracteres raros (como la
eñe, la cedilla, otros símbolos…). Hoy en día es probable que funcione con caracteres
especiales, pero también es muy probable que dé problemas al usarlo en otros entornos,
pasarle el código a alguien… Es una mala práctica, así que mejor, nada de caracteres raros.
El entorno de desarrollo generará por ti un fichero llamado como la clase, con extensión
punto java, en este caso Ejemplo01_01.java.
Si abrimos ese fichero, esa clase, veremos el siguiente contenido:
public class Ejemplo01_01 {
También puedes crear a mano el fichero, darle ese nombre y rellenarlo con ese texto. En
ese caso, fíjate bien en que el nombre del fichero coincida exactamente con el nombre
que le hemos dado a la clase. En este caso Ejemplo01_01.
Veamos qué significa cada parte del código que nos ha generado el entorno o de lo
que escribimos:
• public: es una palabra clave (reservada en Java, es decir, que no puedes usar tú para
nombrar métodos, variables, clases o lo que sea) que indica que lo que vas a crear es
accesible «desde fuera». En todos los ficheros .java debe haber una y solo una clase pública.
• class: es otra palabra reservada, que dice que lo que vamos a declarar es una clase. Aunque
de momento hemos dicho que pasamos de clases y objetos, en Java todo el código tiene
que estar metido en clases, así que necesitaremos crear al menos una, sí o sí, para poder
juguetear con el código.
• Ejemplo01_01: es el nombre que le queremos dar a nuestra clase. Podemos inventarnos
el nombre que queramos, pero lo suyo es que tenga un sentido y solo con leerlo podamos
imaginar para qué sirve. En este caso, es el primero de nuestros ejemplos. Por convenciones
de Java y para facilitar la legibilidad de nuestro código, nos acostumbraremos desde ya
a escribir el nombre de nuestras clases empezando con una letra mayúscula.
• { }: las llaves. Lamento informarte, si no lo habías descubierto ya, que el teclado no está
especialmente pensado para programadores… Usaremos muchos símbolos del teclado
que requieren varias teclas para escribirlos. Las llaves de apertura y cierre, en el teclado
español (en teclados de otros países o idiomas pueden estar en sitios distintos) suelen
estar situadas a la derecha del teclado en la fila central, cerca de la tecla Intro, como se
muestra en la figura 1.1. La llave de apertura está en la misma tecla que el acento agudo
(´) y la de cierre, con la cedilla (ç). Para escribir las llaves, debes apretar a la vez la tecla Alt
o Alt Gr, que están en la fila de abajo de tu teclado, cerca de la barra espaciadora, y la tecla
del acento o de la cedilla. Si te cuesta, practica un poco en cualquier fichero, porque son
dos teclas básicas que cualquier programador Java debe manejar con soltura.
Creando una clase en Java 27
Figura 1.1. Ubicación de las llaves en el teclado español.
Las llaves en Java se usan para marcar los distintos bloques del código, el ámbito de cada cosa.
Las llaves que acabamos de poner para la clase nos servirán para indicar que todo lo que esté
entre la llave de apertura y la de cierre forma parte de la clase. O, dicho de otra manera, que
todo el código de la clase lo tenemos que meter entre la llave de apertura y la de cierre.
Como vamos a tener muchas llaves anidadas, porque se usan también para métodos,
bloques, etc., es importante que tabulemos bien el código para que nos sea fácil de leer,
entender y mantener.
Según la costumbre, la llave de apertura se pone al final de la línea en la que se declara la clase
(o el método o el bloque o lo que sea en cada caso). Hay gente que prefiere ponerla al principio
de la línea siguiente, pero cada vez se ve menos, así que te recomiendo que te acostumbres a
escribirla al final de la primera línea.
T1.1 La llave de cierre de la clase irá sola en una línea y pegada al margen izquierdo, es
decir, sin tabular.
T1.2 ¡Ya tenemos nuestra primera clase! Este fragmento de código lo repetiremos unas cuantas
veces… eso sí, cambiándole el nombre de la clase en cada caso.
El método main en Java
El método main es como cualquier otro método Java (tiene las mismas partes), pero es
un método un poco especial. Un método en Java es parecido a las funciones en otros
lenguajes. Ya iremos aprendiendo más sobre métodos. De momento, este lo necesitamos
para poder hacer cualquier cosa.
El método que buscará Java para empezar la ejecución de una clase es main. Es decir, es
el punto de entrada de nuestros programas. Para que Java reconozca este método, tenemos
que escribirlo bien, respetando todas sus partes:
public static void main(String[] args) {
28 Mi primer programa
Veamos qué significa cada parte, palabra a palabra:
• public: como antes, es la misma palabra, es el mismo significado, pero ahora aplicado a
un método en vez de a una clase. En un método cualquiera, poner el public significará que
puedo llamarlo desde otras clases, pero eso ahora nos viene grande. En el caso concreto
del main, es necesario que sea público, pues es nuestro punto de entrada. Antes de la palabra
public, meteremos un tabulado (o 4 espacios) para separar la línea del margen izquierdo.
• static: esta es otra palabra reservada, y es muy pronto para explicarla, vamos a resumirlo
en que significa que no necesitamos tener un objeto para usar este método, pero por ahora
no te preocupes más por ella. Eso sí, es obligatoria en nuestro main.
• void: otra palabra reservada. En este caso, observa que va justo antes del nombre del
método, está en la posición del tipo del retorno del método. void significa que el método
en cuestión no devolverá ningún resultado. main siempre será void, nunca devolverá
ningún resultado. Si escribes el método main devolviendo algo que no sea void, Java no
lo reconocerá como main y no te funcionará.
• main: es el nombre del método. Como con el nombre de la clase, es posible ponerle el
nombre que queramos a un método, excepto para este. El método main se tiene que llamar
main. No hay otra. Observa que esta vez lo escribimos en minúsculas. Como hemos dicho
antes, por legibilidad y convención, vamos a escribir siempre en minúsculas la inicial de
los nombres de los métodos.
• (: paréntesis de apertura (en el teclado español, en la tecla del 8, Mayús-8). Los paréntesis
envuelven, en la declaración de los métodos, los argumentos que estos van a recibir. Iremos
aprendiendo más sobre los argumentos: en el main siempre serán String[] args.
Figura 1.2. Ubicación de los paréntesis en el teclado español.
• String: es el nombre de la clase que representa los textos en Java (fíjate en que se
escribe con la S mayúscula, ¡es una clase!). Los argumentos del main son textos (ni
números ni estructuras más complejas).
• [ ]: los corchetes se usan en Java (y en otros muchos lenguajes) para indicar que tenemos
un array (una secuencia de elementos). En este caso, una secuencia de String… uno,
ninguno, varios, muchos, aún no lo sabemos. En el teclado español, los corchetes están
en las teclas de encima de las de las llaves, en el acento grave (`) el de apertura, y en el
signo más (+) el de cierre.
El método main en Java 29
Figura 1.3. Ubicación de los corchetes en el teclado español.
• args: nombre que le damos a ese argumento. En el main se le suele llamar args a la
secuencia de textos que recibe de entrada, pero realmente podríamos ponerle otro
nombre. Es el único fragmento de esta línea que es modificable, pero, como no hay
ninguna necesidad de ello, mejor usar args y no complicar las cosas.
• ): paréntesis de cierre. Ya hemos terminado con todos los argumentos. En el main
solo tenemos uno, la secuencia de textos.
• { }: de nuevo las llaves. Entre llave y llave meteremos todo el código del main. Como antes,
la de apertura la pondremos al final de la línea. Y la de cierre, en una línea nueva, pero
esta vez, le meteremos una tabulación (o 4 espacios), en vez de dejarla pegada al margen
izquierdo. Tiene que quedar alineada con el inicio de nuestro método. Hoy en día la
mayoría de IDE si están bien configurados saben colocar cada llave en su sitio, pero ahora
mismo es importante que tú aprendas a colocarlas bien. Te ayudará a leer tu código.
Añadamos el código del método main dentro del código de nuestra clase:
public class Ejemplo01_01 {
public static void main(String[] args) {
}
}
Guarda los cambios (algunos IDE los guardan automáticamente). Si no se guardan los
cambios, Java no los tendrá en cuenta y el comportamiento puede no ser el esperado.
Ya puedes intentar ejecutar la clase Ejemplo01_01. No hará nada, porque aún no hemos
escrito nada de código, pero, si todo está bien, no debería fallar.
Aunque ahora parece la obra de El Escorial, porque lo estamos viendo con mucho detalle,
pronto crearás estas líneas en apenas unos segundos.
Ejecutar el método main de Java desde la línea de comandos
Si usas un entorno de desarrollo preparado para Java, ejecutarás el método main dándole
a un par de botones, pero, si quieres ser un buen programador, deberías tener ciertas
nociones de cómo hacer las cosas por ti mismo y, sobre todo, entender cómo funcionan.
30 Mi primer programa
Para ejecutar una clase Java desde la línea de comandos tenemos que seguir dos pasos:
1. Compilar el código.
El compilador javac es una herramienta que
procesa los ficheros que hemos escrito
nosotros (con letras y símbolos) y, si son
correctos, genera otros ficheros que son
legibles por el ordenador (en binario) y que
serán los utilizados para ejecutar el programa.
En el caso de Java, los ficheros que escribimos
nosotros, el código fuente, deben ser
nombrados *.java, siendo el nombre de cada
clase. Los ficheros binarios generados serán
nombrados *.class, siendo de nuevo el
nombre de cada clase.
Figura 1.4. Contenido del fichero
Así pues, las instrucciones para este caso Ejemplo01_01.class.
serían:
> javac Ejemplo01_01.java
Esta instrucción generará un fichero Ejemplo01_01.class en el mismo directorio
en el que estamos.
Si abrimos este fichero, veremos que, para nosotros, es ilegible.
2. Ejecutar el programa.
> java Ejemplo01_01
Esta instrucción ejecutará nuestro programa, pero como nuestro programa aún no hace
nada, pues no veremos ningún resultado. Pero tampoco deberíamos ver ningún error.
Fíjate en que ahora no indicamos la extensión del fichero, sino el nombre de la clase.
> javac Ejemplo01_01.java
> ls -la
-rw-r--r-- 1 mariona staff 263 2 oct 18:27 Ejemplo01_01.class
-rw-r--r--@ 1 mariona staff 80 2 oct 18:26 Ejemplo01_01.java
> java Ejemplo01_01
¿Qué puede salir mal con el método main de Java?
Al programar cometeremos errores. Es inevitable. Aprendamos cómo reconocerlos,
entenderlos y resolverlos.
Si quieres seguir este ejemplo paso a paso, descarga el código de ejemplo y añádelo a
la carpeta del proyecto (donde tienes Ejemplo01_01.java). Es código malo, pero es lo
que necesitamos ahora.
El método main en Java 31
El fichero y la clase no se llaman igual
Si tenemos un nombre de fichero (Ejemplo01_01a.java) que no coincide con el nombre
de la clase (Ejemplo01_01).
Código fuente
// Ejemplo01_01a.java
public class Ejemplo01_01 {
public static void main(String[] args) {
}
}
Resultado de la compilación
> javac Ejemplo01_01a.java
Ejemplo01_01a.java:2: error: class Ejemplo01_01 is public, should be declared in
a file named Ejemplo01_01.java
public class Ejemplo01_01 {
^
1 error
Explicación
Como la clase y el fichero no se llaman igual, nos da un error. Mira cómo empieza
diciéndonos:
• en qué fichero está el error :
Ejemplo01_01a.java
• el número de línea en el que falla:
:2:
• y qué problema ha habido:
error:
• seguido del mensaje:
class Ejemplo01_01 is public, should be declared in a file named Ejemplo01_01.
java
que podríamos traducir como «la clase Ejemplo01_01 es pública. Debe ser declarada
en un fichero que se llame Ejemplo01_01.java». ¿Puede ser más claro?
• después nos muestra el fragmento de código que está mal y nos lo señala con una flechita:
public class Ejemplo01_01 {
^
• terminando con el contador total de errores:
1 error
Solución
O cambiamos el nombre de la clase o el del fichero. En este caso, corregiremos el
nombre de la clase:
32 Mi primer programa
// Ejemplo01_01a.java
public class Ejemplo01_01a {
public static void main(String[] args) {
}
}
Volvemos a compilar. Ahora ya no tenemos errores.
> javac Ejemplo01_01a.java
Hay alguna palabra clave mal escrita
Código fuente
public clas Ejemplo01_01b {
public static void main(String[] args) {
}
}
Resultado de la compilación
> javac Ejemplo01_01b.java
Ejemplo01_01b.java:1: error: class, interface, or enum expected
public clas Ejemplo01_01b {
^
Ejemplo01_01b.java:2: error: class, interface, or enum expected
public static void main(String[] args) {
^
2 errors
Explicación
En este caso tenemos dos errores. El primero nos marca un problema en clas, en la
línea 1 y el segundo es en void en la línea 2.
El primer error es el que hemos provocado nosotros mismos, nos hemos dejado una
s en class, y nos dice que «espera una clase, una interfaz o un enumerado». Clase,
interfaz y enumerado son los elementos que Java nos permite crear y cuya declaración
se hace en ese momento del fichero. Nosotros queremos crear una clase, aún es
pronto para hablar de interfaces y enumerados.
El segundo error es un error fantasma: realmente no hay ningún otro problema en
el código, pero como al principio está mal, el compilador, que es bastante tontorrón
(o poco flexible), se pierde y puede ya no entender muchas otras cosas.
Solución
public class Ejemplo01_01b {
public static void main(String[] args) {
}
}
Añadimos la s que nos faltaba. Guardamos y volvemos a compilar. Ahora ya no
tenemos errores.
> javac Ejemplo01_01b.java
El método main en Java 33
Otro ejemplo del mismo tipo de error:
Código fuente
public class Ejemplo01_01c {
public staic void main(String[] args) {
}
}
Resultado de la compilación
> javac Ejemplo01_01c.java
Ejemplo01_01c.java:2: error: <identifier> expected
public staic void main(String[] args) {
^
Ejemplo01_01c.java:2: error: invalid method declaration; return type required
public staic void main(String[] args) {
^
2 errors
Explicación
De nuevo tenemos dos errores, el primero en staic, donde nos falta una t, y nos está
diciendo que espera un identificador. Como nos marca con la flechita la palabra staic, ya
nos podemos fijar bien en ella y encontrar que nos falta la t.
Como antes, el segundo error no es real, porque el compilador ya se ha perdido y no
entiende el resto de la declaración del main.
Solución
public class Ejemplo01_01c {
public static void main(String[] args) {
}
}
Añadimos la t que nos faltaba. Guardamos y volvemos a compilar. Ahora ya no
tenemos errores.
> javac Ejemplo01_01c.java
Otros errores
En efecto, nos pueden salir muchísimos más errores si nos confundimos en algún
T1.3 otro punto de nuestro breve código. Ante un error, nunca pierdas la calma. Lee
despacito, presta atención a lo que pone, porque normalmente el compilador nos
T1.4 dará buenas pistas del problema que haya encontrado.
Si te atascas en algún error, escríbeme, intentaré echarte una mano. Pega el código que falla
y el mensaje de error que recibes, porque sin esa información sería imposible ayudarte.
34 Mi primer programa
Mostrando mensajes al usuario en Java
([Link])
Ya tenemos una clase y un método main, la estructura mínima básica para ejecutar algo en
Java. Hagamos nuestro primer programa, que va a escribir «Bienvenido al curso de
programación Java» por la «salida estándar». La salida estándar en un IDE es una ventana o
pestaña que se suele llamar consola (o console si el entorno está en inglés). Si ejecutamos el
código desde la línea de comandos, será la propia línea de comandos.
Para escribir por la salida estándar, simplemente tenemos que escribir esta línea:
[Link]("bla bla bla");
Vamos a explicarla:
• [Link]: complicado de explicar ahora mismo, de momento nos quedaremos
con que es el método que tiene Java para escribir textos por la salida estándar.
• ( ): los paréntesis engloban los parámetros que le vamos a pasar a ese método.
Aquí, a diferencia de cuando hemos escrito la línea del main, no definimos qué
nos podría pasar (tipo y nombre), sino que le pasamos valores reales. En este
caso "bla bla bla".
• "bla bla bla": como acabamos de ver, es el valor real de lo que queremos imprimir
por pantalla. Observa que va entre comillas dobles (encima del 2 en el teclado).
Figura 1.5. Ubicación de las comillas en el teclado español.
• ;: el punto y coma… ¡otro signo más! Situado en el teclado español en la misma tecla
que la coma (,), escrito usando mayúsculas y coma, el punto y coma lo usaremos al
final de cada sentencia en Java. Si no lo ponemos, el compilador (el programa que
lee y procesa nuestros programas) no entenderá el código y nos dará errores.
Mostrando mensajes al usuario en Java ([Link]) 35
Figura 1.6. Ubicación del punto y coma en el teclado español.
Entonces, si queremos que nuestro programa escriba el texto que hemos dicho, tendremos
que añadir la línea que acabamos de ver dentro del método main de nuestro código:
public class Ejemplo01_01 {
public static void main(String[] args) {
[Link]("Bienvenido al curso de programación Java");
}
}
T1.5
Fíjate en que la línea que acabamos de añadir lleva ahora dos tabulaciones (u ocho
espacios) por delante. De forma que el código de dentro de cada bloque quede anidado,
E1.1 y así sean más fáciles de localizar los bloques.
Ejecutemos nuestro segundo programa, que es el primero que hace algo.
E1.2 > javac Ejemplo01_01.java
> java Ejemplo01_01
Bienvenido al curso de programación Java
Test y ejercicios
Test 1.1. ¿Cuál es la forma correcta de declarar una clase?
a) class public NombreDeLaClase() { }
b) public class NombreDeLaClase { }
c) class public NombreDeLaClase { }
d) public class NombreDeLaClase() { }
Test 1.2. ¿Cómo debe llamarse la clase que escribamos en el fichero
[Link]?
a) [Link]
b) mi-clase
c) MiClase
d) Como quiera
Test 1.3. ¿Cuál es la forma correcta de declarar el método main?
a) static public main void(String[] args) { }
b) public static main void(String[] args) { }
c) public static void main(String[] args) { }
d) public static main(String[] args): void { }
36 Mi primer programa
Test 1.4. ¿Cuál es la única palabra de public static void main(String[] args) que
se puede modificar?
a) args
b) main
c) static
d) void
Test 1.5. ¿Cuál es la forma correcta de escribir una línea por la salida estándar?
a) [Link]()
b) [Link]()
c) [Link]()
d) System_out_println()
Ejercicio 1.1. Mostrando un mensaje de una línea
Escribe un programa que imprima por pantalla la frase «Estoy aprendiendo a programar
bien en Java».
Ejercicio 1.2. Mostrando un mensaje de varias líneas
Escribe un programa que imprima por pantalla estas dos frases: «La vida es tan buena
maestra,» y «que si no aprendes la lección te la repite.».
Soluciones
Test 1.1. ¿Cuál es la forma correcta de declarar una clase?
b) public class NombreDeLaClase { }: El orden de las palabras y el uso de los símbolos es
el que tiene que ser.
Test 1.2. ¿Cómo debe llamarse la clase que escribamos en el fichero
[Link]?
c) MiClase: Efectivamente, debe coincidir con el nombre del fichero, sin la extensión.
Test 1.3. ¿Cuál es la forma correcta de declarar el método main?
c) public static void main(String[] args) { }: ¡Exacto! Apréndetelo bien, porque este es el
único orden válido.
Test 1.4. ¿Cuál es la única palabra de public static void main(String[] args) que
se puede modificar?
a) args: Aunque no ganamos nada cambiando el nombre de los argumentos, efectivamente,
podríamos poner otro.
Test 1.5. ¿Cuál es la forma correcta de escribir una línea por la salida estándar?
c) [Link](): Entenderás el porqué más adelante, pero si te impacienta: System
es una clase, out un atributo y println un método, por eso System empieza en mayúscula
y el resto no.
Soluciones 37
Ejercicio 1.1. Mostrando un mensaje de una línea
• Primero declararemos la clase:
public class Ejercicio01_01 {
}
ADVERTENCIA:
Usar el guion bajo o de subrayado en el nombre de la clase no es una buena idea, porque no
cumple las convenciones de nombrado de Java (que aprenderemos más adelante); sin embargo,
llamar a la clase «Ejercicio0101» dificulta la legibilidad.
• El segundo paso será declarar el método main, dentro de la clase.
public class Ejercicio01_01 {
public static void main(String[] args) {
}
}
No olvides respetar las tabulaciones.
• Tercer y último paso: incluir la sentencia para imprimir por pantalla el mensaje que nos han pedido:
public class Ejercicio01_01 {
public static void main(String[] args) {
[Link]("Estoy aprendiendo a programar bien en Java");
}
}
Cuidado no te confundas con las mayúsculas y minúsculas.
Guardamos el fichero, vamos a la línea de comandos y:
• compilamos
> javac Ejercicio01_01.java
• ejecutamos
> java Ejercicio01_01
Estoy aprendiendo a programar bien en Java
¡Ahora sí obtenemos un resultado! Justo el mensaje que queríamos enseñarle al usuario.
Ejercicio 1.2. Mostrando un mensaje de varias líneas
• Primero declararemos la clase:
public class Ejercicio01_02 {
}
• El segundo paso será declarar el método main, dentro de la clase.
public class Ejercicio01_02 {
public static void main(String[] args) {
}
}
38 Mi primer programa
• Tercer y último paso: incluir las sentencias para imprimir por pantalla los mensajes que nos
han pedido:
public class Ejercicio01_02 {
public static void main(String[] args) {
[Link]("La vida es tan buena maestra,");
[Link]("que si no aprendes la lección te la repite.");
}
}
Para escribir dos frases, nos basta con llamar dos veces al [Link], una tras otra, dentro
del método main, cada vez con una de las dos frases.
Guardamos el fichero, vamos a la línea de comandos y:
• compilamos
> javac Ejercicio01_02.java
• ejecutamos
> java Ejercicio01_02
La vida es tan buena maestra,
que si no aprendes la lección te la repite.
¡Esta vez veremos dos líneas!
Podemos también ejecutarlo desde nuestro IDE. En la figura 1.7 se muestra en Eclipse.
Botón
Ejecutar
Código
fuente
Resultado
de la
ejecución
Figura 1.7. Ejecución de un programa desde Eclipse.
Soluciones 39
2 Argumentos, variables,
métodos y operadores
En este capítulo aprenderás a:
• Identificar qué son los argumentos y cómo pasárselos a tus
programas.
• Reconocer las variables y de qué tipos pueden ser.
• Declarar y cómo llamar a métodos.
• Utilizar los operadores de Java.
Introducción
Argumentos, variables, métodos y operadores son cuatro conceptos fundamentales de
la programación.
Los operadores nos permiten realizar operaciones lógicas o matemáticas sobre las
variables en las que almacenamos datos. Estas cuentas las haremos dentro de métodos
que pueden ser llamados desde distintos puntos de los programas, pasándoles argumentos
para adaptarlos a cada caso.
Aunque lo que veremos en este capítulo está orientado al lenguaje de programación Java,
es aplicable a casi cualquier otro lenguaje de alto nivel, aunque puede haber diferencias
en la sintaxis, es decir, en la forma de escribir las cosas.
Los conceptos como tal son universales. Algunos detalles pueden ser propios de Java.
Argumentos
Los argumentos del main y las secuencias
Cuando declaramos nuestro primer método main, dijimos que recibía como argumentos una
secuencia de textos (String[] args). Vamos a jugar ahora un poco con estos argumentos.
Los argumentos de un método posibilitan que el método se comporte de manera distinta
en función del valor de los mismos. De hecho, hasta ahora, le hemos pasado argumentos
distintos al [Link] cada vez que lo hemos llamado y, en función del texto que
le hayamos pasado cada vez, eso nos ha impreso por pantalla.
En el cole, en su día nos enseñaron a sumar y a poner el símbolo + entre dos números. Y
si la señorita escribía 1 + 2 en la pizarra, coreábamos juntos un «¡tres!». Y si escribía 2 + 3,
decíamos «¡cinco!». Pues bien, ahí tenemos un método, la suma, que recibe argumentos
(1 y 2 en el primer caso, 2 y 3 en el segundo) y cuyo resultado depende de esos dos
argumentos. Pues esa misma idea se aplica a los métodos cuando programamos.
Volvemos a los argumentos del main. Son una secuencia de Strings. En la programación
Java de hoy en día apenas se usan los arrays (o secuencias), porque tenemos otras
estructuras más flexibles y potentes, las colecciones, pero para aprender, las secuencias
nos vendrán muy bien y en el main siempre las tendremos. Aunque en la programación
real, apenas se usa el main o el [Link].
Pero no te preocupes, no estás perdiendo el tiempo. De adultos apenas gateamos, pero
antes de andar, cuando fuimos bebés, gatear nos resultó muy útil. Pues aquí es lo mismo.
Estamos aprendiendo a gatear en Java y a dar nuestros primeros pasos, para que cuando
«seamos mayores» podamos correr, saltar y hacer lo que haga falta para resolver los
problemas a los que hagamos frente.
42 Argumentos, variables, métodos y operadores
Volvamos a la secuencia de argumentos del main. Para acceder a los argumentos, tenemos
que usar la palabra que hayamos utilizado para nombrarlos. En este caso es args, pero
recuerda que podríamos haber puesto cualquier otra.
Para acceder a cada uno de los elementos de la secuencia, tenemos que usar ese nombre,
args, junto a la posición del elemento deseado entre corchetes: args[0].
Ten en cuenta que en Java empezamos a contar los elementos de las secuencias, listas y del
resto de colecciones desde cero. El primer elemento estará en la posición 0 y, por ejemplo,
el último elemento de una secuencia de tres elementos estará en la posición 2 (args[2]).
Para saber cuántos elementos tiene una secuencia, es posible consultar el atributo length,
haciendo [Link]. Por tanto, para acceder al último elemento de una secuencia,
haremos args[[Link] - 1]. Es muy importante el -1, porque empezamos a contar
desde cero y, si no pusiéramos el -1, ¡nos saldríamos del array!
Hagamos nuestro primer ejemplo de uso de los argumentos. Vamos a imprimir por
pantalla el texto «El último elemento que me has pasado es …» y el valor del último
argumento recibido.
• Creamos la clase.
• Añadimos el método main.
• Escribimos la sentencia [Link].
TRUCO:
Eclipse y los entornos de desarrollo en general son muy majos y nos ayudan… Si escribimos sysout
así todo junto y en minúsculas, y le damos a Ctrl + espacio, ¡sorpresa!, ¡nuestro IDE escribe el resto!
• Añadimos el texto entrecomillado.
• Concatenamos el valor requerido usando el signo más (+), el de sumar, de toda la
vida. Lo emplearemos aquí para «sumar» textos o, dicho más profesionalmente, para
«concatenar Strings».
public class Ejemplo02_01 {
public static void main(String[] args) {
[Link]("El último elemento que me has pasado es "
+ args[[Link]-1]);
}
}
Fíjate bien en que al final del String literal (con el texto "El último elemento que me has
pasado es ") y antes de las comillas que lo cierran he puesto un espacio. Esto es para
que la palabra "es" no se quede pegada al valor que saquemos del último elemento.
Cuando concatenas Strings debes tener cuidado con los espacios y otros separadores,
para cerciorarte de que el resultado sea legible.
Argumentos 43
¿Cómo ejecutar el main con argumentos de texto?
Intentemos compilar y ejecutar este código como hicimos en el capítulo anterior:
> javac Ejemplo02_01.java
> java Ejemplo02_01
Exception in thread "main" [Link]: 0
at Ejemplo02_01.main(Ejemplo02_01.java:3)
¿Qué ha pasado aquí? Al ejecutar el primer ejemplo, no necesitábamos argumentos,
pero esta vez ya sí. Y como no le estamos pasando ningún argumento y luego intentamos
acceder a uno de ellos, nos da una excepción. Observa que no nos dio ningún problema
de compilación, porque la sintaxis de nuestro código es correcta. Nos da una excepción
que es el mecanismo que usa Java para avisarnos de que ha habido algún problema
durante la ejecución del código. En el capítulo 10 aprenderás a tratar y resolver
excepciones, pero, de momento, lo resolveremos sin meternos en ellas.
Nuestro problema viene de no pasarle argumentos… ¡pasémoselos!
> java Ejemplo02_01 hola
El último elemento que me has pasado es hola
Como puedes ver, ahora, ya no nos da errores. Le hemos pasado un argumento (hola).
Como solo tenemos uno, ese mismo será el último.
Probamos ahora con más argumentos, esta vez tres: hola, qué, tal. Presta atención a que
no les ponemos ni comas ni comillas.
> java Ejemplo02_01 hola qué tal
El último elemento que me has pasado es tal
Si quisiéramos que «hola qué tal» fuera un único argumento, con ponerlo entre comillas
tendríamos suficiente:
> java Ejemplo02_01 "hola qué tal"
El último elemento que me has pasado es hola qué tal
Ejecución con argumentos en un IDE
En cada entorno de desarrollo los pasos para pasarle los argumentos pueden ser
ligeramente distintos, veamos cómo hacerlo en Eclipse.
1. En vez de clicar directamente sobre la flecha verde para ejecutar el código, vamos a
desplegar, con un clic en la flechita negra hacia abajo (paso 1 de la figura 2.1), y
escogeremos la opción Configuraciones de Ejecución o Run configurations (según
en qué idioma tengas configurado el Eclipse) (paso 2 de la figura 2.1).
2. Seleccionaremos la pestaña Argumentos o Arguments (paso 3 de la figura 2.2) y en
el primer cuadro de texto (paso 4 de la figura 2.2) meteremos nuestros argumentos.
Como hemos hecho por línea de comandos, sin comas ni comillas, a no ser que, como
hemos visto antes, queramos pasar un argumento con espacios, que entonces sí
meteremos las comillas.
3. Clicamos sobre el botón Ejecutar (paso 5 de la figura 2.2).
44 Argumentos, variables, métodos y operadores
1
Figura 2.1. Lanzar configuraciones de ejecución en Eclipse.
Figura 2.2. Establecer los argumentos de ejecución en Eclipse.
T2.1
E2.1
Figura 2.3. Resultado de ejecución con argumentos en Eclipse.
A partir de ahora, mientras no lo modifiques, cada vez que ejecutes esta clase, tendrás E2.2
los argumentos establecidos.
Argumentos 45
Variables
Declaración y uso de variables
Seguimos avanzando con pequeños pasos en el mundo de la programación. ¿Recuerdas
cuando resolvías problemas de matemáticas en el cole? A veces, para llegar al resultado
final necesitabas varios pasos e ibas anotando en una esquina tus cuentas intermedias,
¿verdad? Pues más o menos eso son las variables.
Lo vemos con un ejemplo. Supongamos un problema de primero de primaria: En el
frutero de casa hay 3 manzanas y 5 peras. ¿Cuántas piezas de fruta hay?
int numManzanas = 3;
int numPeras = 5;
int numFrutas = numManzanas + numPeras;
Declaramos una variable llamada numManzanas, de tipo int (entero, es decir, un número
positivo o negativo, pero sin decimales) y le asignamos, con un igual (=), el valor 3.
Queremos representar que tenemos tres manzanas.
En la línea siguiente, declaramos numPeras, también numérica con un valor de 5, para
representar nuestras cinco peras.
Como el problema pide el número total de piezas de fruta, declaramos otra variable,
numFrutas, y le asignamos el resultado de sumar numManzanas y numPeras. Es decir,
sumaremos el número de manzanas (3) y el de peras (5), nos dará 8, y lo guardaremos
en la variable numFrutas, que luego podremos seguir usando para lo que queramos.
Veamos este mismo ejemplo en un método main:
public class Ejemplo02_02 {
public static void main(String[] args) {
// Los argumentos que recibimos son Strings.
// Tenemos que convertirlos a números para poder operar con ellos
// Los guardamos en variables y les damos un nombre representativo
int numManzanas = [Link](args[0]);
int numPeras = [Link](args[1]);
// TODO deberíamos comprobar que al menos tenemos dos argumentos
// TODO y asegurarnos de que los argumentos son números enteros
// Guardamos el resultado del cálculo en una variable
int numFrutas = numManzanas + numPeras;
// Tras hacer todos los cálculos necesarios, mostramos el resultado
// Cuidado con los espacios al concatenar Strings, para que no se
// peguen las palabras
[Link]("El frutero tiene " + numFrutas +
" piezas de fruta.");
}
}
¡Uy! Hay ahí un par de cosas nuevas: ¿qué son tantas barras? ¿Qué es eso del Integer.
valueOf?
46 Argumentos, variables, métodos y operadores
Los fragmentos de código precedidos de dos barras (de las de la tecla del 7), //, son
comentarios. Tras las dos barras, podemos escribir «lo que queramos» que Java no va a
intentar entenderlo.
Sobre el [Link], según vayas aprendiendo más, ya comprenderás ese fragmento
de código. Por ahora, dejémoslo en que es una forma de convertir una cadena de texto
(uno de los Strings del array de argumentos) en un número entero.
En cuanto al nombre de las variables, como también pone en los comentarios, es
importante que tengan un nombre representativo, para que luego sea más fácil de entender
el código, tanto por otros programadores como por nuestro propio yo del futuro.
Por convención, en Java, los nombres de variable empezarán siempre en minúscula, y si
están formados por varias palabras, como no podemos poner espacios, para facilitar la
lectura usaremos camelCase, es decir, pondremos la inicial de cada palabra en mayúscula.
Se llama camelCase porque va subiendo y bajando como las jorobas de un camello. En
otros lenguajes, la convención puede ser otra.
Nos queda otra cosa nueva en el código… El TODO dentro de un comentario. No se lee
todo, en castellano, sino to-do, en inglés (‘por hacer’). Es una marca que muchos IDE
identifican y señalizan de alguna manera para que localices rápidamente las cosas que
has dejado a medio hacer (que están por hacer, to do).
En este ejemplo lo uso para apuntar que hay que controlar los posibles errores de entrada,
pero aún es pronto para que sepamos hacerlo. Si quieres ver las consecuencias de no
tratar eso, intenta ejecutar este ejemplo sin pasarle argumentos o pasándole letras.
• Compilamos:
> javac Ejemplo02_02.java
• Si le pasamos un tres y un cinco o cualquier otro par de números enteros, funcionará
perfectamente:
> java Ejemplo02_02 3 5
El frutero tiene 8 piezas de fruta.
• Pero si le pasamos menos argumentos o no todos son números, dará errores, que
más adelante aprenderemos a evitar y/o tratar:
> java Ejemplo02_02 3
Exception in thread "main" [Link]: 1
at Ejemplo02_02.main(Ejemplo02_02.java:7)
> java Ejemplo02_02 3 a
Exception in thread "main" [Link]: For input string: "a"
at [Link](NumberFormatException.
java:65)
at [Link]([Link])
at [Link]([Link])
at Ejemplo02_02.main(Ejemplo02_02.java:7)
Variables 47
Tipos de las variables en Java
Java es un lenguaje fuertemente tipado. Aunque eso nos pueda parecer un engorro a la
hora de programar, porque nos dará un poco de guerra al compilar, en tiempo de ejecución
nos va a garantizar un código mucho más robusto. En otros lenguajes no hace falta decir
si una variable será un texto o un número e, incluso, primero se le puede dar un valor
numérico y luego ponerle un texto. Eso en Java no se hace. En otros lenguajes, aunque
se pueda, no debería hacerse, es un poco «guarrería». Java nos obliga a decir de qué tipo
será una variable en el momento de la declaración y tendremos que respetarlo durante
todo el ciclo de vida de la variable.
// declaramos una variable entera:
int num;
// declaramos una variable booleana (cierto o falso):
boolean largo;
// declaramos e inicializamos una variable de texto:
String apellido = "García";
// inicializamos la variable num
num = 7;
// inicializamos la variable booleana largo
largo = [Link]() > num; // el método length nos da la longitud del String
// > compara dos valores numéricos y devuelve un booleano
Como puedes ver, podemos declarar las variables e inicializarlas en la misma línea o en
líneas separadas. Lo importante es que antes de usarlas las hayamos inicializado. Ya irás
aprendiendo cuándo conviene hacerlo en la misma línea o cuándo en sitios separados.
De entrada, te recomiendo hacerlo en la misma línea, si no necesitamos dos líneas, ¡para
qué complicar las cosas!
Fíjate en que int y boolean están escritos en minúsculas: son tipos básicos o primitivos
(como double, float, byte, long, char…).
Tabla 2.1. Tipos primitivos en Java.
Tipo de dato Tamaño Descripción Ejemplo
byte 1 byte Almacena números enteros desde -128 byte numMini = 100;
hasta 127.
short 2 bytes Almacena números enteros desde -32 768 short numPeque = 5000;
hasta 32 767.
int 4 bytes Almacena números enteros desde int numNormal =
-2 147 483 648 hasta 2 147 483 647. 500000;
long 8 bytes Almacena números enteros desde long numGrande =
-9 223 372 036 854 775 808 hasta 123000000000L;
9 223 372 036 854 775 807.
float 4 bytes Almacena números fraccionarios (con float numComa = 12.34f;
decimales), con precisión de 6 a 7 float comaCientifica =
dígitos decimales, desde 3.4e−038 hasta 67e8f;
3.4e+038.
48 Argumentos, variables, métodos y operadores
Tipo de dato Tamaño Descripción Ejemplo
double 8 bytes Almacena números fraccionarios (con double numDoble =
decimales), con precisión de 15 dígitos 54.321;
decimales, desde 1.7e−308 hasta 1.7e+308. double dobleCientifica
= 12E4d;
boolean 1 bit Almacena los valores cierto (true) o boolean javaMola =
falso (false). true;
char 2 bytes Almacena una sola letra o carácter o char letra = 'a';
valores ASCII. char simbolo = '@';
Sin embargo, String empieza por mayúscula. ¿Por qué? String, en Java (cuidado, en otros T2.2
lenguajes puede ser distinto), no es un tipo básico, sino una clase. Por convención, los
nombres de clase empiezan por mayúscula (y luego ya el camelCase como las variables).
T2.3
¿Qué es una clase? Bueno, eso ya lo aprenderemos más adelante; de momento,
considerémoslo un tipo de datos complejo.
E2.3
NOTA:
Para Java, "a" entre comillas dobles es un String. Sin embargo, 'a' entre apóstrofos o comillas E2.4
simples es un char.
Métodos
Declaración de métodos
Los métodos, también llamados funciones o procedimientos, son un mecanismo que
tienen los lenguajes de programación que nos permite agrupar un conjunto de instrucciones
para realizar una tarea y a las que podremos llamar tantas veces como necesitemos
usando un nombre, sin necesidad de repetir el código.
Tomemos un problema de sumas, concretamente de sumar seis cosas dos a dos. Pongamos
que recibimos estos datos: número de clientes, de empleados, de sillas, de mesas, de
contratos y de reclamaciones. Y necesitamos calcular cuántas personas, cuántos muebles
y cuántos expedientes tenemos.
Este código, sin duda, lo resolvería. Dedica un momento a leerlo y entenderlo:
public class Ejemplo02_03Mal {
public static void main(String[] args) {
int numClientes = [Link](args[0]);
int numEmpleados = [Link](args[1]);
int numSillas = [Link](args[2]);
int numMesas = [Link](args[3]);
int numContratos = [Link](args[4]);
int numReclamaciones = [Link](args[5]);
Métodos 49
// Guardamos el resultado del cálculo en una variable
int numPersonas = numClientes + numEmpleados;
int numMuebles = numSillas + numMesas;
int numExpedientes = numContratos + numReclamaciones;
[Link]("Tenemos " + numPersonas +
" personas, " + numMuebles + " muebles y " +
numExpedientes + " expedientes.");
}
}
Compilamos y ejecutamos:
> java Ejemplo02_03Mal 1 2 3 4 5 6
Tenemos 3 personas, 7 muebles y 11 expedientes.
Funciona bien, pero observa que estamos repitiendo tres veces la operación de suma.
También estamos repitiendo mucho la transformación en números de los parámetros,
pero no nos fijaremos en eso ahora mismo.
Ya que nuestra principal tarea es sumar elementos de dos en dos, creemos un método
que haga esas cuentas.
Recuerda que ya conocemos al método más especial de Java, se llama main, y tenía que
declararse tal cual lo aprendimos, sin demasiada (casi ninguna) flexibilidad.
Creemos un método que llamaremos suma (porque va a sumar) y que recibirá como
parámetros dos números enteros y devolverá otro número entero:
private static int suma(int a, int b) {
return a + b;
}
Como hicimos con main, veamos una a una las palabras que hemos puesto y por qué:
• private: es un modificador que nos indica la visibilidad del método. Para el main
pusimos public porque se tenía que poder llamar desde fuera. Como de momento
no tenemos otras clases, esta vez lo vamos a dejar privado. De hecho, mi consejo es
que, por defecto, pongas como privado todo lo que puedas… Y solo si lo necesitas
de verdad, lo pongas public. Piensa que las cosas privadas las puedes cambiar o
eliminar cuando quieras, mientras que, si creas algo público, «otros» pueden usarlo
y, por tanto, cualquier cambio que hagas afectará a ese código que esté usando el
tuyo. De momento, privado, private.
• static: sigue siendo pronto para hablar de ella, digamos que como main era static y
queremos llamar a suma desde el main, necesitaremos que suma también sea static.
• int: el tipo colocado antes del nombre del método nos indica el tipo que tendrán los
resultados del método. En algunos lenguajes se pueden hacer cosas muy raras con
los tipos, como devolver cosas distintas según nos convenga, pero Java es muy estricto
con estas cosas (ya lo vimos con las variables), así que tenemos que definir claramente
qué va a devolver el método (o si no va a devolver nada, void). En este caso,
devolveremos un número entero.
50 Argumentos, variables, métodos y operadores
• suma: es el nombre del método. Es posible llamarlo «como queramos», pero
respetando las mismas convenciones que para las variables, ya sabes, empieza por
minúscula, si hay varias palabras usamos camelCase… Los nombres de métodos
deberían ser verbos que describan la acción que vaya a hacer cada método.
• ( ) { }: tienen las mismas funciones que vimos en el main: entre paréntesis tendremos
los parámetros del método y entre las llaves, las instrucciones.
• int: el tipo colocado antes del nombre de un parámetro nos indica de qué tipo tiene
que ser ese parámetro.
• a: es el nombre del primer parámetro. De nuevo, podemos llamarlo «como queramos»,
pero siguiendo las mismas convenciones que para las variables. En este caso, debería ser
un sustantivo que describa el significado de eso que estamos recibiendo. Aunque parezca
increíble, a es un buen nombre en este caso, porque estamos haciendo un método muy
genérico que suma dos valores y, en la clase de mates, ¿cómo solíamos llamarlos? a, b,
c… Si nuestro método calculara el perímetro de una circunferencia y recibiera el radio
como parámetro, ya no deberíamos llamarlo a, si no r o, aún mejor, radio.
• int b: lo mismo, pero para el segundo parámetro. Podemos tener tantos parámetros
como necesitemos, aunque se recomienda que no sean demasiados. Los separamos
por comas. También podríamos no tener parámetros, en cuyo caso, pondríamos los
paréntesis () sin nada en medio.
• return: palabra clave que indica que el resultado de lo que vaya detrás será lo que
devuelva ese método.
• a + b: la instrucción que debe ejecutar nuestro método: en este caso, sumar el valor
de lo que me llegue como primer parámetro (a) con el valor de lo que me llegue como
segundo parámetro (b).
• ;: el punto y coma es el separador entre instrucciones en Java y debemos ponerlo al
final de cada instrucción (que no de cada línea, porque puede haber instrucciones
que ocupen varias líneas).
Uso de métodos
Sabiendo cómo declarar un método, veamos cómo usarlo, cómo llamarlo.
Podemos llamar al método pasándole valores:
suma(3, 4);
En este caso, el compilador a asignarle a a el valor 3 y a b el valor 4, va a sumar a + b, y
nos devolverá un 7.
O pasándole variables:
int numClientesJuan = 2;
int numClientesMaria = 3;
suma(numClientesJuan, numClientesMaria);
Métodos 51
Ahora el compilador le asignará a a el valor de numClientesJuan, que es 2, y ¿a b? A b
le asignará el valor de numClientesMaria, que vale 3. Sumará a + b y nos dará 5.
O incluso, llamadas a métodos o combinaciones de valores y variables:
suma(3, suma(numClientesJuan, numClientesMaria));
Ahora parece mucho más complicado, pero no te asustes. Vamos a por la primera suma,
a a le asignará el 3 que le pasamos directamente. A b le asignará el resultado de la segunda
suma. Vamos a por ella: a a le asignará el numClientesJuan, 2. Y a b, el numClientesMaria,
3, sumará y devolverá 5, como hemos visto antes, y ahora ya tiene el valor que necesitaba
asignarle a la «primera» b. Así pues, tendremos a = 3, b = 5, ¿resultado? ¡8!
Pues ahora que ya tenemos claro cómo va esto, resolvamos de una forma más elegante
el ejemplo de sumar seis elementos de dos en dos:
public class Ejemplo02_03 {
public static void main(String[] args) {
int numClientes = [Link](args[0]);
int numEmpleados = [Link](args[1]);
int numSillas = [Link](args[2]);
int numMesas = [Link](args[3]);
int numContratos = [Link](args[4]);
int numReclamaciones = [Link](args[5]);
// Guardamos el resultado del cálculo en una variable
int numPersonas = suma(numClientes, numEmpleados);
int numMuebles = suma(numSillas, numMesas);
int numExpedientes = suma(numContratos, numReclamaciones);
[Link]("Tenemos " + numPersonas +
" personas, " + numMuebles + " muebles y " +
numExpedientes + " expedientes.");
}
private static int suma(int a, int b) {
return a + b;
}
}
Si te fijas, nuestro método suma está declarado dentro de las llaves de la clase, pero fuera
de las llaves del método main. Por convención, en Java se declaran primero las cosas
públicas y luego las privadas, por eso hemos puesto el método suma debajo del método
main. Cuidado, porque en otros lenguajes es necesario que las cosas estén declaradas antes
de usarlas y, por tanto, se requiere poner el método suma por encima del método main.
Hemos añadido a nuestra clase de ejemplo la declaración del método suma y, además,
desde el método main hemos cambiado el cálculo de numPersonas para usar, llamar,
dicho método. Si lo declaramos, pero no lo usamos, estaríamos haciendo trabajo inútil.
También lo hemos usado para calcular el número de muebles y el de expedientes. Para
cada una de las tres llamadas el compilador asignará a a y a b los valores correspondientes,
sin liarse ni mezclarlos, porque cada llamada es independiente. a y b solo se pueden usar
dentro del método suma y, en cuanto salimos de dicho método, desaparecen.
52 Argumentos, variables, métodos y operadores
Dentro de un método puedo usar de la misma manera los parámetros y las variables
definidas dentro del método. Pongamos un ejemplo tonto y raro: el método sumaMas5 T2.4
que recibe dos parámetros, como vimos en suma, pero devuelve la suma de esos dos,
más 5 de regalo. T2.5
private static int sumaMas5(int a, int b) {
int c = 5;
return a + b + c; T2.6
}
Como ves, en la línea de return estoy usando a, b y c sin distinciones, aunque a y b son
E2.5
parámetros y c es una variable. A eso me refería.
Volvamos al ámbito. El ámbito, la disponibilidad, de una variable va desde el momento
E2.6
que la declaro hasta que se cierran las llaves que la envuelven. Para los parámetros, su
ámbito es todo el método, es decir, desde las llaves de apertura hasta las de cierre. Según
vayamos aprendiendo más cosas, verás que hay más llaves en nuestro código y nos E2.7
podemos liar un poco más.
Operadores
Instrucción de asignación
variable = expresión;
Ya la hemos utilizado en alguno de nuestros ejemplos y ejercicios. La instrucción de
asignación, que utiliza el signo igual (=) en Java, tiene el efecto de evaluar la expresión
a la derecha del igual y asignar el valor resultante a la variable de la izquierda del igual.
La asignación requiere que tanto la expresión como la variable sean del mismo tipo.
Algunos ejemplos:
int edad = 18;
int valor = [Link](unString);
edadEnMeses = edad * 12;
Operadores aritméticos
Los operadores matemáticos en Java son, más o menos, los mismos que en la mayoría
de los lenguajes de programación (suma, resta, división, multiplicación y módulo). El
módulo da el resto de la división entera.
El operador de división se comporta de forma distinta en función de los tipos
involucrados: la división entera, entre enteros, da un resultado entero: trunca el
resultado, no lo redondea. La división con números reales, por su parte, tendrá como
resultado un número real.
Operadores 53
Mejor lo vemos con un ejemplo:
public class Ejemplo02_04 {
public static void main(String[] args) {
int divisionEntera = 7 / 2;
int modulo = 7 % 2;
double divisionReal = 7.0 / 2.0;
[Link]("División Entera 7/2: " + divisionEntera);
[Link]("Módulo 7%2: " + modulo);
[Link]("División Real 7.0/2.0: " + divisionReal);
}
}
Compilamos, ejecutamos y nos fijamos en los resultados:
> javac Ejemplo02_04.java
> java Ejemplo02_04
División Entera 7/2: 3
Módulo 7%2: 1
División Real 7.0/2.0: 3.5
Supongo que si alguien te pregunta cuánto es siete entre dos, le dirías tres y medio,
pero, si te fijas, Java puede no responder lo mismo. Para Java, si divides el entero
siete entre el entero dos, el resultado será un entero, es decir, tres. En cambio, si
divides el double siete punto cero entre el double dos punto cero, el resultado tendrá
decimales, es decir, será el tres y medio que esperábamos. Aclarado este punto,
veamos la lista entera de operadores aritméticos.
Tabla 2.2. Operadores aritméticos en Java.
Operador Nombre Descripción Ejemplo
+ Suma Suma dos valores. x + y
- Resta Resta un valor de otro. x – y
* Multiplicación Multiplica dos valores. x * y
/ División Divide un valor entre otro. x / y
% Módulo Devuelve el resto de la división. x % y
++ Incremento Incrementa el valor de una variable en 1. x++
++x
-- Decremento Decrementa el valor de una variable en 1. x--
--x
Los operadores de incremento (++) y decremento (--) se emplean de dos formas
distintas: por delante o por detrás. Si hacemos un preincremento o predecremento,
poniendo el operador antes del nombre de la variable, primero se incrementará o
decrementará en uno ese valor, luego la variable ya tomará el nuevo valor. Si hacemos
un postincremento o postdecremento, el valor de la variable no se modificará hasta
la siguiente instrucción. Esto suena muy complejo, pero seguro que con este ejemplo
lo ves más claro.
54 Argumentos, variables, métodos y operadores
El código:
public class Ejemplo02_05 {
public static void main(String[] args) {
int n = 7;
[Link]("1) n: " + n);
[Link]("2) ++n: " + ++n);
[Link]("3) n: " + n);
[Link]("4) n++: " + n++);
[Link]("5) n: " + n);
}
}
El resultado de ejecución:
> java Ejemplo02_05
1) n: 7
2) ++n: 8
3) n: 8
4) n++: 8
5) n: 9
Partiendo de un 7 (línea 1 del resultado), el preincremento hace que el número
impreso (línea 2) ya tome el valor incrementado, 8. En la tercera línea, mostramos
el valor de n, que se mantiene en 8, sin misterios, no lo hemos modificado. Si pintamos
el resultado de postincrementar, en la línea 4, seguimos viendo un 8, pero, en la
quinta línea, ya tenemos un 9. Este pequeño detalle es importante a la hora de hacer
comparaciones sobre los valores.
Precedencia de los operadores
La precedencia de los operadores define cómo se evalúa una expresión de varios
operadores. Java tiene reglas específicas para determinar el orden de evaluación. La más
sencilla es que la multiplicación y la división se ejecutarán antes que la suma y la resta.
Como los programadores a menudo no tenemos claro cómo va la precedencia, es mejor
utilizar paréntesis para hacer explícito el orden.
No es lo mismo:
a = x + y - 2 / 2 + z;
Que:
a = x + (y - 2) / (2 + z);
Operadores relacionales
Los operadores relacionales o comparadores generan un resultado booleano, i. e., cierto
o falso (true o false, en inglés). Evalúan la relación entre los valores de los operandos.
Operadores 55
Tabla 2.3. Operadores relacionales en Java.
Operador Nombre Ejemplo
== Igual x == y
!= Distinto x != y
> Mayor que x > y
< Menor que x < y
>= Mayor o igual que x >= y
<= Menor o igual que x <= y
Con los operadores igual y distinto hay que tener cuidado. Si estamos comparando
variables de tipos básicos (int, float, char…), igual y distinto compararán los valores. Pero
si estamos comparando objetos (de los que aún no hemos hablado), lo que se van a
comparar son los punteros, su posición en memoria, es decir, comprueba si son el mismo
objeto. Mejor lo vemos en un ejemplo:
public class Ejemplo02_06 {
public static void main(String[] args) {
int x = 3;
int y = 3;
[Link]("x: " + x);
[Link]("y: " + y);
[Link]("x == y? " + (x == y) );
[Link]("x != y? " + (x != y) );
String a = new String("hola");
String b = new String("hola");
[Link]("a: " + a);
[Link]("b: " + b);
[Link]("a == b? " + (a == b) );
[Link]("a != b? " + (a != b) );
}
}
Y vemos qué sucede al ejecutar:
> java Ejemplo02_06
x: 3
y: 3
x == y? true
x != y? false
a: hola
b: hola
a == b? false
a != b? true
Es decir, si comparamos la x con la y, ambas variables enteras con valor 3: responde lo
que esperamos, que sí son iguales y que no son distintas. Pero si comparamos la a con
la b, ambas objetos de tipo String con el texto "hola", nos dice que son distintas. Porque
al tratarse de objetos, está comparando si son el mismo objeto, no si tienen el mismo
valor. No te preocupes si aún no lo entiendes mucho, simplemente recuerda que tienes
que ir con cuidado a la hora de comparar objetos.
56 Argumentos, variables, métodos y operadores
Operadores lógicos
Los operadores lógicos (y, o, no) producen un valor booleano, cierto o falso, basado en
la relación lógica entre los operandos, que deben ser booleanos también.
Tabla 2.4. Operadores lógicos en Java.
Operador Nombre Denominación Descripción Ejemplo
&& and conjunción Devuelve cierto si ambos operandos son x > 4 && x < 8
ciertos.
|| or disyunción Devuelve cierto si al menos uno de los x < y || x > z
operandos es cierto.
! not negación Invierte el resultado, devuelve falso si !x
el operando es cierto.
Cuando se manejan operadores lógicos, Java cae en la evaluación perezosa. Deja de evaluar
expresiones en cuanto determina de forma no ambigua cuál será el resultado de la expresión.
Quiero decir que, si estamos ante un and, que requiere que todos los operandos sean ciertos
para ser cierto, si el primer operando es falso, ya no intentará calcular el valor del segundo.
Devolverá directamente un falso. Por otro lado, si estamos ante un or, que requiere un solo
operando cierto, si el primer operando es cierto, ya devolverá cierto; pero si es falso, evaluará
el segundo.
¿Lo vemos en un ejemplo?
public class Ejemplo02_07 {
public static void main(String[] args) {
[Link]("1) AND: " + (siempreCierto() && siempreFalso()));
[Link]("2) AND: " + (siempreFalso() && siempreCierto()));
[Link]("3) OR: " + (siempreCierto() || siempreFalso()));
[Link]("4) OR: " + (siempreFalso() || siempreCierto()));
}
private static boolean siempreCierto() {
[Link]("siempreCierto");
return true;
}
private static boolean siempreFalso() {
[Link]("siempreFalso");
return false;
}
}
Resultado:
> java Ejemplo02_07
siempreCierto
siempreFalso
1) AND: false
siempreFalso
2) AND: false
siempreCierto
Operadores 57
3) OR: true
siempreFalso
siempreCierto
4) OR: true
Nuestra primera prueba es un and entre algo cierto y algo falso, y se ejecutan ambos
métodos. Pero en la segunda prueba, hacemos un and entre algo falso y algo que nunca
llegaremos a mirar cómo es, porque al ser ya falso, ya paramos.
En la tercera y cuarta pruebas, jugamos con el or. En la tercera tenemos algo cierto o
E2.8 algo… ya tenemos el cierto, para. Sin embargo, en la cuarta prueba, como el primer
operando es falso, necesitamos evaluar el segundo, que es el que determinará el resultado.
Operadores bit a bit
Los operadores bit a bit manipulan los bits individuales de variables de tipos primitivos,
ejecutando álgebra booleana sobre la representación binaria de los operandos.
NOTA:
Explico los operadores bit a bit porque son parte del lenguaje Java, y hay que contarlo, pero la
verdad es que nunca he tenido que usarlos.
Tabla 2.5. Operadores bit a bit en Java.
Ejemplo Ejemplo Resultado Resultado
Op. Denominación Descripción decimal binario binario decimal
& Conjunción and - el bit resultante será 5 & 1 0101 & 0001 1
1 si el bit de esa posición 0001
en ambos operandos es 1.
| Disyunción or - el bit resultante 4 | 1 0100 | 0101 5
será 1 si el bit de esa 0001
posición en alguno de los
operandos es 1.
~ Complemento not - invierte todos los bits. ~10 ~1010 0101 5
^ Disyunción xor - el bit resultante será 1 5 ^ 2 0101 ^ 0111 7
excluyente si el bit de esa posición en 0010
solo uno de los operandos
es uno, es decir, si son
distintos.
<< Desplazamiento Desplaza los bits a la 13 << 1101 1010 10
de bits a la izquierda añadiendo ceros 1 << 1
izquierda por la derecha y perdiendo
los bits de más a la
izquierda.
58 Argumentos, variables, métodos y operadores
Ejemplo Ejemplo Resultado Resultado
Op. Denominación Descripción decimal binario binario decimal
>> Desplazamiento Desplaza los bits a la 12 >> 1100 1111 15
de bits a la derecha añadiendo por 2 >> 2
derecha con la izquierda copias del bit
signo de más a la izquierda y
perdiendo los bits de más a
la derecha.
>>> Desplazamiento Desplaza los bits a la 12 >>> 1100 0011 3
de bits a la derecha añadiendo 2 >>> 2
derecha con ceros por la izquierda y
ceros perdiendo los bits de más
a la derecha.
En los operadores de desplazamiento, el primer operando es el dato a tratar y el segundo
es el número de posiciones a rotar.
ADVERTENCIA:
Cuidado con no confundir los signos repetidos (== comparación, && and lógico, || or lógico…)
con los signos simples (= asignación, & and binario, | or binario…).
Test y ejercicios
Test 2.1. ¿Cuántos argumentos estamos pasando en este caso?
> java Ejemplo02_01 hola,juan "qué tal" estás ?
a) 1
b) 2
c) 3
d) 4
e) 5
f) más
Test 2.2. ¿Cuál es el mejor nombre en Java para una variable que guarde el
primer apellido del padre de un alumno?
a) primerApellidoDelPadreDeUnAlumno
b) apellido1
c) primerApellidoPadreAlumno
d) apellido1Padre
e) apellido
f) a1pa (por Apellido 1 Padre Alumno)
g) 1apPad
Test 2.3. ¿Qué imprimirá este fragmento de código?
int a = 10;
int b = 1;
int c = a + b;
String d = "Total: " + a + b + c;
[Link](d);
Test y ejercicios 59
a) Total: 22
b) Total: 10111
c) Total: 11101
d) Total: 11
e) Total: 10 1 11
Test 2.4. ¿Cuál es la salida de este código?
public class Test02_04 {
public static void main(String[] args) {
int a = 3;
int b = 8;
[Link]("1)");
test(b, a);
[Link]("2)");
test(a, a);
}
private static void test(int a, int b) {
[Link]("A=" + a + " B=" + b);
}
}
a) 1) A=8 B=3
2) A=3 B=3
b) 1) A=3 B=8
2) A=3 B=8
c) 1) A=8 B=3
2)A=3 B=8
d) 1) A=3 B=8
2) A=3 B=3
Test 2.5. ¿Cuál es el resultado de este código?
public class Test02_05 {
public static void main(String[] args) {
int uno = 1;
int res = suma(uno, suma(uno + uno, suma(-uno, uno * uno)));
[Link](res);
}
private static int suma(int a, int b) {
return a + b;
}
}
a) 6
b) 3
c) error
d) 5
e) 4
Test 2.6. ¿Cuál sería la mejor definición para un método que calcula el
área de un triángulo?
a) int area(int a, int b)
b) int area(int base, int altura)
c) void areaTriangulo(int a, int b)
d) int areaTriangulo(int base, int altura)
60 Argumentos, variables, métodos y operadores
Test 2.7. ¿Cuál sería la traducción explícita equivalente de este fragmento
de código?
res = a + b / a + c / b;
a) res = (a + b) / (a + c) / b;
b) res = a + (b / a) + (c / b);
c) res = a + b / (a + (c / b));
d) res = (a + b / a) + c / b;
Test 2.8. ¿Qué valor toma res?
int a = 4;
int b = 8;
int c = 2;
boolean res = c >= (b / a) || (a * b) - c < 0;
a) true
b) false
Ejercicio 2.1. Mostrando un mensaje con un argumento variable
Escribe un programa que imprima por pantalla el argumento que ha recibido («He recibido este
argumento: ») seguido del valor de dicho argumento.
Ejercicio 2.2. Mostrando un mensaje que indique el número de argumentos
recibido
Escribe un programa que imprima por pantalla cuántos argumentos ha recibido: «He recibido …
argumentos.», indicando en el lugar de los puntos suspensivos, el número de argumentos recibido.
Ejercicio 2.3. Calcula el área de un rectángulo
Escribe un programa que calcule el área de un rectángulo e imprima por pantalla el texto «El
rectángulo de … por … tiene un área de …». Recibirá el tamaño de los dos lados como
argumentos.
Ejercicio 2.4. Calcula los nombres de toda la familia
Escribe un programa que reciba estos siete argumentos:
0. Primer apellido
1. Segundo apellido
2. Nombre primer hijo
3. Nombre segundo hijo
4. Nombre tercer hijo
5. Nombre del padre
6. Nombre de la madre
Y saque los datos de toda la familia: una línea por cada miembro.
Veámoslo en un ejemplo más claro:
Test y ejercicios 61
> java Ejercicio02_04 Gómez García María Lucas Pedro Juan "María Luisa"
Padre: Juan Gómez
Madre: María Luisa García
Hijos:
María Gómez García
Lucas Gómez García
Pedro Gómez García
El primer paso ante cualquier ejercicio o proyecto real es asegurarte de que entiendes bien lo
que se está pidiendo. Si lo que programas está muy bien, pero no hace lo que piden, no sirve
de nada, así que fíjate bien en lo que cuenta el enunciado y, cuando te den un ejemplo, como
en este caso, comprueba que lo entiendes y es lo que esperabas.
Ahora, intenta resolver el ejercicio por tu cuenta. Un ejercicio no está resuelto hasta que no está
probado y funcionando. Cuando lo tengas, ve al final del capítulo para comprobar la solución.
Ejercicio 2.5. Calcula el área de un rectángulo usando un método
Repetimos el ejercicio 2.3, pero sacando el cálculo del área a un método.
Escribe un programa que calcule el área de un rectángulo e imprima por pantalla el texto «El
rectángulo de … por … tiene un área de …». Recibirá el tamaño de los dos lados como
argumentos.
Ejercicio 2.6. Calcula los nombres de toda la familia usando un método
Retomando el ejercicio 2.4, crea un método pintarPersona que reciba como argumento tres
Strings (el nombre y los dos apellidos) y saque como salida: "Nombre: <nombre> Apellidos:
<ap1> <ap2>". Usa la cadena vacía ("") para los segundos apellidos desconocidos.
Usa el método para sacar los datos de todos los miembros de la familia, en el mismo orden
de antes.
Ejercicio 2.7. Calcula los nombres de toda la familia usando dos métodos
Vamos a mejorar lo que hemos hecho en el ejercicio 2.6.
Esta vez crearemos dos métodos:
• uno que construya el nombre de cada persona:
private static String construyeNombreCompleto(String nombre, String apellido1,
String apellido2)
que se encargue de la parte de concatenar los distintos elementos del nombre.
• otro que reciba el nombre completo y lo pinte por pantalla:
private static void pintarNombreCompleto(String nombreCompleto)
62 Argumentos, variables, métodos y operadores
Soluciones
Test 2.1. ¿Cuántos argumentos estamos pasando en este caso?
> java Ejemplo02_01 hola,juan "qué tal" estás ?
d) 4: 1) "hola,juan" 2) "qué tal" 3) "estás" 4) "?"
Test 2.2. ¿Cuál es el mejor nombre, en Java, para una variable que guarde el
primer apellido del padre de un alumno?
d) apellido1Padre: La virtud está en el punto medio. Indicamos que es un apellido,
concretamente el primero, pero solo ocupamos un carácter, y que es del padre.
Supongamos que todos los datos que estamos usando son de alumnos. Cuidado con
el uso de números. Aquí está bien porque tenemos dos apellidos y solemos identificarlos
por su posición, pero suele ser mala señal si necesitamos numerar nuestras variables,
quizá es que necesitamos usar alguna estructura tipo array, lista...
Test 2.3. ¿Qué imprimirá este fragmento de código?
int a = 10;
int b = 1;
int c = a + b;
String d = "Total: " + a + b + c;
[Link](d);
b) Total: 10111: Bien visto. Está claro que ya sabes que el + entre números suma, pero entre
Strings concatena.
Test 2.4. ¿Cuál es la salida de este código?
public class Test02_04 {
public static void main(String[] args) {
int a = 3;
int b = 8;
[Link]("1)");
test(b, a);
[Link]("2)");
test(a, a);
}
private static void test(int a, int b) {
[Link]("A=" + a + " B=" + b);
}
}
a) 1) A=8 B=3
2) A=3 B=3
Poco importa cómo se llamen las variables dentro del método main, tenemos que fijarnos
en qué le pasamos al método test como parámetro. Y la primera vez le estamos pasando
b y a, y luego a y a, es decir, 8, 3 y 3, 3.
Soluciones 63
Test 2.5. ¿Cuál es el resultado de este código?
public class Test02_05 {
public static void main(String[] args) {
int uno = 1;
int res = suma(uno, suma(uno + uno, suma(-uno, uno * uno)));
[Link](res);
}
private static int suma(int a, int b) {
return a + b;
}
}
b) 3: 1 + ((1+1) + (-1 + (1*1))), que sería 1 + (2 + (-1+1)), es decir, 1 + 2 + 0.
Test 2.6. ¿Cuál sería la mejor definición para un método que calcula el área de
un triángulo?
d) int areaTriangulo(int base, int altura): Perfecto. Tengo un método que calcula el área
de un triángulo, recibe dos parámetros, su base y su altura, y devuelve un número con
el resultado. ¡Me lo dice todo!
Test 2.7. ¿Cuál sería la traducción explícita equivalente de este fragmento
de código?
res = a + b / a + c / b;
b) res = a + (b / a) + (c / b);: correcto, las divisiones van antes que las sumas.
Test 2.8. ¿Qué valor toma res?
int a = 4;
int b = 8;
int c = 2;
boolean res = c >= (b / a) || (a * b) - c < 0;
a) true: 2 >= 8/4 OR (4*8 - 2) < 0, que sería 2>=2 OR … ya da igual, cierto.
Ejercicio 2.1. Mostrando un mensaje con un argumento variable
• Primero, preparamos la clase y el main:
public class Ejercicio02_01 {
public static void main(String[] args) {
}
}
• Dentro del main, metemos una llamada para escribir, con el texto que nos pide el enunciado.
(Atención al espacio detrás de los dos puntos).
public class Ejercicio02_01 {
public static void main(String[] args) {
[Link]("He recibido este argumento: ");
}
}
64 Argumentos, variables, métodos y operadores
• Solo nos falta cumplir con la parte «seguido del valor de dicho argumento». ¿Qué argumento
queremos mostrar? El primero, y ese en Java se llama cero. Así pues, de todos los args, queremos
el 0: concatenamos al String que ya tenemos, aún dentro de los paréntesis, args[0].
public class Ejercicio02_01 {
public static void main(String[] args) {
[Link]("He recibido este argumento: " + args[0]);
}
}
NOTA:
Faltaría controlar que se ha recibido al menos un argumento, pero ya lo veremos más adelante.
• Compilamos y ejecutamos:
> javac Ejercicio02_01.java
> java Ejercicio02_01 hola
He recibido este argumento: hola
Ejercicio 2.2. Mostrando un mensaje que indique el número de argumentos
recibido
• Empezamos con la clase, el main y el [Link]:
public class Ejercicio02_02 {
public static void main(String[] args) {
[Link]();
}
}
• Y esta vez, como argumentos del [Link], concatenamos tres fragmentos. Los dos literales
que necesitamos y, entre ellos, el número de argumentos que podemos conseguir llamando al
atributo length del array de Strings args.
public class Ejercicio02_02 {
public static void main(String[] args) {
[Link]("He recibido " + [Link] + " argumentos.”);
}
}
• Compilamos y ejecutamos:
> javac Ejercicio02_02.java
> java Ejercicio02_02 hola
He recibido 1 argumentos.
> java Ejercicio02_02
He recibido 0 argumentos.
Soluciones 65
Ejercicio 2.3. Calcula el área de un rectángulo
• Partimos de la estructura del método main:
public class Ejercicio02_03 {
public static void main(String[] args) {
}
}
• Inspirándonos en el ejemplo del frutero, recuperamos primero los dos argumentos que
necesitamos. Lado x y lado y del rectángulo:
public class Ejercicio02_03 {
public static void main(String[] args) {
int ladoX = [Link](args[0]);
int ladoY = [Link](args[1]);
}
}
Recuerda que como los argumentos recibidos son String y nosotros queremos trabajar con números,
debemos convertirlos usando [Link].
• Ahora que ya tenemos los lados, vamos a calcular el área del rectángulo, multiplicando los
valores de ambos lados. La multiplicación, en programación, usa el asterisco *.
public class Ejercicio02_03 {
public static void main(String[] args) {
int ladoX = [Link](args[0]);
int ladoY = [Link](args[1]);
int area = ladoX * ladoY;
}
}
• Y teniendo el área ya calculada, solo nos falta mostrar el resultado por pantalla:
public class Ejercicio02_03 {
public static void main(String[] args) {
int ladoX = [Link](args[0]);
int ladoY = [Link](args[1]);
int area = ladoX * ladoY;
[Link]("El rectángulo de " + ladoX + " por " + ladoY +
" tiene un área de " + area);
}
}
¡No olvides nunca probar tu código al terminar de escribirlo! Tendrás que pasarle los argumentos
que quieras al IDE (o a la línea de comandos) para que funcione bien.
El área de un rectángulo de dos por tres es:
> javac Ejercicio02_03.java
> java Ejercicio02_03 2 3
El rectángulo de 2 por 3 tiene un área de 6
¡6!
Ejercicio 2.4. Calcula los nombres de toda la familia
• Partimos de la estructura de siempre, un método main vacío.
public class Ejercicio02_04 {
public static void main(String[] args) {
}
}
66 Argumentos, variables, métodos y operadores
• Podrías haber conseguido resolver todo el ejercicio en una línea, pero un buen programador
no se preocupa por el número de líneas de su código, sino por su legibilidad. Por eso, en la
solución que te propongo, primero recojo cada uno de los argumentos esperados y lo guardo
en una variable de texto (String).
Son siete argumentos esta vez. ¡Tenemos trabajo!
public class Ejercicio02_04 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
}
}
Aquí ya empieza a demostrarse importante eso de ponerles nombres con significado a las variables.
• Como todos los hijos tienen la misma combinación de apellidos, aprovecho para prepararlos
en una variable y no tener que «calcularlos» para cada hijo.
public class Ejercicio02_04 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
String apellidosHijos = apellido1 + " " + apellido2;
}
}
• Finalmente, voy imprimiendo línea por línea, cada una con su [Link]. ¿Recuerdas
el truco de Eclipse para que te facilite la vida aquí? Ctrl + Espacio.
public class Ejercicio02_04 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
String apellidosHijos = apellido1 + " " + apellido2;
[Link]("Padre: " + padre + " " + apellido1);
[Link]("Madre: " + madre + " " + apellido2);
[Link]("Hijos:");
[Link](hijo1 + " " + apellidosHijos);
[Link](hijo2 + " " + apellidosHijos);
[Link](hijo3 + " " + apellidosHijos);
}
}
Este ejercicio es un poco engorroso de resolver… Pronto aprenderemos a que sea el ordenador y
no nosotros quienes tengamos que repetir las cosas.
Soluciones 67
¡Acuérdate de probarlo! Con los argumentos que se te dieron en el ejemplo y con otros. De momento
no estamos haciendo ningún control de errores, así que, si cometes cualquier fallo con los
argumentos, el código explotará, pero no pasa nada, puedes hacerlo y así te vas familiarizando
con qué errores se producen y cuándo.
> javac Ejercicio02_04.java
> java Ejercicio02_04 Gómez García María Lucas Pedro Juan "María Luisa"
Padre: Juan Gómez
Madre: María Luisa García
Hijos:
María Gómez García
Lucas Gómez García
Pedro Gómez García
Ejercicio 2.5. Calcula el área de un rectángulo usando un método
Esta vez vamos a partir del ejercicio 2.3, haciendo una copia del fichero y renombrando fichero y
clase acorde a la numeración del ejercicio.
public class Ejercicio02_05 {
public static void main(String[] args) {
int ladoX = [Link](args[0]);
int ladoY = [Link](args[1]);
int area = ladoX * ladoY;
[Link]("El rectángulo de " + ladoX + " por " + ladoY +
" tiene un área de " + area);
}
}
Los cambios que tenemos que hacer son los siguientes:
1) creamos un método que calcule el área del rectángulo:
private static int areaRectangulo(int base, int altura) {
return base * altura;
}
2) lo llamamos desde el método main
public class Ejercicio02_05 {
public static void main(String[] args)
{
int ladoX = [Link](args[0]);
int ladoY = [Link](args[1]);
int area = areaRectangulo(ladoX, ladoY);
[Link]("El rectángulo de " + ladoX + " por " + ladoY +
" tiene un área de " + area);
}
private static int areaRectangulo(int base, int altura) {
return base * altura;
}
}
Y como siempre… ¡hay que probarlo!
> javac Ejercicio02_05.java
> java Ejercicio02_05 3 7
El rectángulo de 3 por 7 tiene un área de 21
68 Argumentos, variables, métodos y operadores
Ejercicio 2.6. Calcula los nombres de toda la familia usando un método
Partimos de nuevo del código del ejercicio 2.4, copiando el fichero y renombrando fichero y clase.
public class Ejercicio02_06 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
String apellidosHijos = apellido1 + " " + apellido2;
[Link]("Padre: " + padre + " " + apellido1);
[Link]("Madre: " + madre + " " + apellido2);
[Link]("Hijos:");
[Link](hijo1 + " " + apellidosHijos);
[Link](hijo2 + " " + apellidosHijos);
[Link](hijo3 + " " + apellidosHijos);
}
}
• Para resolver este ejercicio creamos el método pedido, pintaPersona, con sus tres argumentos.
Como el método nos pide que «pintemos» los datos de una persona, directamente llamamos
al [Link] dentro del método y no devolvemos nada.
private static void pintaPersona(String nombre, String apellido1,
String apellido2) {
[Link]("Nombre: " + nombre + " Apellidos: " +
apellido1 + " " + apellido2);
}
• Ahora, usamos el nuevo método para pintar los datos de cada miembro de la familia y, como
desconocemos el segundo apellido de los padres, como tercer argumento en esos casos pasamos
la cadena vacía ("", dos dobles comillas juntas, las de la tecla del 2 en el teclado español, sin
espacios entre ellas).
Observa que ya no necesitamos la variable apellidosHijos que creamos originalmente. La
podemos borrar.
public class Ejercicio02_06 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
pintaPersona(padre, apellido1, "");
pintaPersona(madre, apellido2, "");
pintaPersona(hijo1, apellido1, apellido2);
pintaPersona(hijo2, apellido1, apellido2);
pintaPersona(hijo3, apellido1, apellido2);
}
private static void pintaPersona(String nombre, String apellido1,
String apellido2) {
[Link]("Nombre: " + nombre + " Apellidos: " +
apellido1 + " " + apellido2);
}
}
Soluciones 69
Ejercicio 2.7. Calcula los nombres de toda la familia usando dos métodos
• Partimos del código del ejercicio 2.6, copiamos, renombramos...
public class Ejercicio02_07 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
pintaPersona(padre, apellido1, "");
pintaPersona(madre, apellido2, "");
pintaPersona(hijo1, apellido1, apellido2);
pintaPersona(hijo2, apellido1, apellido2);
pintaPersona(hijo3, apellido1, apellido2);
}
private static void pintaPersona(String nombre, String apellido1,
String apellido2) {
[Link]("Nombre: " + nombre + " Apellidos: " +
apellido1 + " " + apellido2);
}
}
• Hacemos los dos métodos que nos piden construyeNombreCompleto y pintaNombre
Completo, a la vez que suprimimos el viejo pintaPersona.
private static String construyeNombreCompleto(String nombre,
String apellido1, String apellido2) {
return "Nombre: " + nombre + " Apellidos: " +
apellido1 + " " + apellido2;
}
private static void pintaNombreCompleto(String nombreCompleto) {
[Link](nombreCompleto);
}
Nos tenemos que fijar en una cosa en construyeNombreCompleto. Aunque los argumentos se
llamen igual que las variables del main, los valores no tienen por qué coincidir. Por ejemplo, en el
caso de la madre, le pasaremos la variable apellido2, que viene de args[1], como apellido1, y la
cadena vacía, como apellido2.
• Y los usamos:
public class Ejercicio02_07 {
public static void main(String[] args) {
String apellido1 = args[0];
String apellido2 = args[1];
String hijo1 = args[2];
String hijo2 = args[3];
String hijo3 = args[4];
String padre = args[5];
String madre = args[6];
70 Argumentos, variables, métodos y operadores
pintaNombreCompleto(construyeNombreCompleto(padre, apellido1, ""));
pintaNombreCompleto(construyeNombreCompleto(madre, apellido2, ""));
pintaNombreCompleto(
construyeNombreCompleto(hijo1, apellido1, apellido2));
pintaNombreCompleto(
construyeNombreCompleto(hijo2, apellido1, apellido2));
pintaNombreCompleto(
construyeNombreCompleto(hijo3, apellido1, apellido2));
}
private static String construyeNombreCompleto(String nombre,
String apellido1, String apellido2) {
return "Nombre: " + nombre + " Apellidos: " +
apellido1 + " " + apellido2;
}
private static void pintaNombreCompleto(String nombreCompleto) {
[Link](nombreCompleto);
}
}
Aunque pueda parecer que este código es más «feo» que el anterior, estamos separando responsabilidades
entre los métodos. Cada método se encarga de una sola cosa. construyeNombreCompleto concatena
los distintos Strings, pintaNombreCompleto, los pinta.
Si supiéramos más, también tendríamos un método para la recuperación de argumentos… todo llegará.
Soluciones 71
3 Estructuras condicionales
En este capítulo aprenderás a:
• Identificar las instrucciones condicionales en Java.
• Aplicar buenas prácticas en el uso de condicionales.
• Trabajar con las palabras reservadas if, else, switch, case y default.
• Usar constantes: cómo y cuándo.
Introducción
Hasta ahora hemos visto cómo crear programas muy sencillos y bastante inútiles. Eso
va a ir mejorando poco a poco a lo largo del libro. En este capítulo aprenderemos a
controlar el flujo de ejecución del programa. Puede sonar muy complicado, pero en la
vida diaria lo aplicamos continuamente. Si llueve, cojo el paraguas. Si llevo dinero
suficiente, pago en efectivo y, si no, con tarjeta. Si eres adulto, puedes entrar; si eres
menor, debes ir acompañado.
Con conocer los elementos de la sintaxis propia de Java, y general de la programación,
métodos, argumentos, variables… no es suficiente para «programar».
Una condición: si (if)… si no (else)…
Veamos la primera estructura de control: if. If en inglés significa ‘si’, el «si» condicional
(no el afirmativo):
if (condición) {
instrucciones;
}
Que leeremos «si se cumple la condición, se ejecutan las instrucciones».
Por ejemplo, si llueve, coge el paraguas y ponte el chubasquero:
if (estaLloviendo()) {
cogeElParaguas();
ponteElChubasquero();
}
La condición puede ser cualquier expresión booleana (cierto o falso), por ejemplo:
• true: valor literal
• a > 0: expresión simple
• esPar(): llamada a un método
• b < a && (c * d > b || c + d < a): resultado de una expresión compleja
• encontrado: variable
Pero ¿qué sucede cuando no se cumple esa condición?, ¿qué hay que hacer? Normalmente,
los if no suelen ir solos (aunque sería perfectamente correcto), sino acompañados de una
rama alternativa, else.
if (condición) {
instruccionesIf;
} else {
instruccionesElse;
}
Viendo un caso más práctico, vamos a escribir un método que nos muestre la temperatura.
Si el valor es positivo, dirá * «ºC positivos». Si no, dirá «ºC bajo cero».
74 Estructuras condicionales
public class Ejemplo03_01 {
public static void main(String[] args) {
int temp = [Link](args[0]);
[Link]("La temperatura es de ");
if (temp > 0) {
[Link](temp + "ºC positivos.");
} else {
[Link](temp + "ºC bajo cero.");
}
}
}
Si ejecutamos este código:
> javac Ejemplo03_01.java
> java Ejemplo03_01 3
La temperatura es de 3ºC positivos.
> java Ejemplo03_01 13 T3.1
La temperatura es de 13ºC positivos.
> java Ejemplo03_01 -3
La temperatura es de -3ºC bajo cero.
T3.2
Necesitamos dominar las expresiones booleanas a la hora de establecer las condiciones
de los if, así que aprovecha los test para practicar un poco.
Varias condiciones: si no si (else if)…
A veces necesitamos más casos, no nos vale solo con dos opciones. ¿Has probado a
ejecutar el ejemplo anterior Ejemplo03_02 con 0 como argumento?
> java Ejemplo03_01 0
La temperatura es de 0ºC bajo cero.
No tiene mucho sentido, ¿verdad? Pero decir que era de 0 ºC positivos, tampoco. Vamos
a cambiar el enunciado. Añadimos que, si la temperatura es 0, queremos que diga: «ni
frío ni calor».
Con lo que sabemos hasta ahora podríamos hacer:
if (temp > 0) {
[Link](temp + "ºC positivos.");
} else {
if (temp == 0) {
[Link](temp + "ºC, ni frío ni calor.");
} else {
[Link](temp + "ºC bajo cero.");
}
}
Hay lenguajes que nos ofrecen una palabra reservada especial para conseguir varias
ramas: elsif, elif o combinaciones parecidas. Java no tiene eso, pero podemos simularlo.
Porque la verdad es que el código que hemos escrito está marcando como una jerarquía
que no nos convence. Los tres mensajes son del mismo nivel, pero al necesitar tener un
if dentro de otro, vamos anidando las líneas y cuesta leer.
Varias condiciones: si no si (else if)… 75
ADVERTENCIA:
Hasta ahora no te lo había contado porque yo lo considero una mala práctica, pero ahora necesito
contártelo. ¡Pero solo para usarlo en este caso!
Cuando un bloque en Java solamente tiene una instrucción, podemos omitir las llaves.
No se recomienda hacerlo porque poner llaves facilita la lectura y, más importante,
facilita el mantenimiento: si mañana ese bloque de una línea necesita una segunda línea,
si tenemos llaves la ponemos sin problema, pero si no pusimos las llaves y ahora nos las
olvidamos al añadir la segunda instrucción… el código podría no funcionar bien y nos
costaría descubrir por qué. Veamos unos ejemplos:
if (haceFrio())
ponerCalefaccion();
Pero la energía está muy cara, vamos a abrigarnos un poco además de poner la calefacción,
y así estaremos más a gustito gastando menos energía:
if (haceFrio())
ponerCalefaccion();
abrigarse();
Parece correcto este código, ¿no? ¡Pruébalo!
public class Ejemplo03_02Mal {
public static void main(String[] args) {
int temp = [Link](args[0]);
if (haceFrio(temp))
ponerCalefaccion();
abrigarse();
}
private static boolean haceFrio(int temp) {
return temp <= 15;
}
private static void ponerCalefaccion() {
[Link]("Calefacción a tope!!");
}
private static void abrigarse() {
[Link]("¿Dónde está mi batamanta?");
}
}
Si lo ejecutamos con 12 grados:
> java Ejemplo03_02Mal 12
Calefacción a tope!!
¿Dónde está mi batamanta?
¿Y si estamos a 35?
> java Ejemplo03_02Mal 35
¿Dónde está mi batamanta?
¿En serio estando a 35 grados quieres la batamanta?
76 Estructuras condicionales
Como en la primera versión no pusimos llaves al if, porque era mucho trabajo, cuando
evolucionamos nuestro código y le añadimos la segunda instrucción, aunque parezca
que esté dentro del if, está fuera, con lo que se ejecuta siempre, haga frío o no.
public class Ejemplo03_02 {
public static void main(String[] args) {
int temp = [Link](args[0]);
if (haceFrio(temp)) {
ponerCalefaccion();
abrigarse();
}
}
private static boolean haceFrio(int temp) {
return temp <= 15;
}
private static void ponerCalefaccion() {
[Link]("Calefacción a tope!!");
}
private static void abrigarse() {
[Link]("¿Dónde está mi batamanta?");
}
}
¡Simplemente añadiendo las llaves al if, ya funcionará bien!
> java Ejemplo03_02 12
Calefacción a tope!!
¿Dónde está mi batamanta?
> java Ejemplo03_02 35
Moraleja: no seamos vagos, ¡pongamos siempre las llaves!
¿Siempre? ¡Siempre! (Excepto en el caso siguiente).
¿A qué venía este rollo de las llaves? Estábamos con que Java no tiene una palabra especial
para elsif:
if (temp > 0) {
[Link](temp + "ºC positivos.");
} else {
if (temp == 0) {
[Link](temp + "ºC, ni frío ni calor.");
} else {
[Link](temp + "ºC bajo cero.");
}
}
Si nos fijamos en la rama else, ¿cuántas instrucciones tenemos dentro?
¡Una! El if/else. Si dentro de un bloque tenemos solo una instrucción, hemos dicho que
podemos quitar las llaves. Pues a por ello. Quitemos las llaves del else.
if (temp > 0) {
[Link](temp + "ºC positivos.");
} else
Varias condiciones: si no si (else if)… 77
if (temp == 0) {
[Link](temp + "ºC, ni frío ni calor.");
} else {
[Link](temp + "ºC bajo cero.");
}
}
Y si ahora juntamos else e if en la misma línea (dejando un espacio entre ellos para que
sigan siendo dos palabras) y retabulamos…
if (temp > 0) {
[Link](temp + "ºC positivos.");
} else if (temp == 0) {
[Link](temp + "ºC, ni frío ni calor.");
} else {
[Link](temp + "ºC bajo cero.");
}
¿A que ahora ya se lee mejor? Pues eso, para simular el elsif, pondremos un if dentro de
un else, pero sin llaves.
Y hemos hecho otra trampa. ¿Te acuerdas de cuando hablamos de las llaves? Dijimos
que la de cierre se quedaba solita en una línea, salvo con las ramas else. Podríamos dejar
E3.1 el else en la línea siguiente, pero normalmente lo ponemos en una sola línea:
} else if (...) {
E3.2 o
} else {
Constantes
En el código a veces usamos números, por ejemplo, en Ejercicio03_02, en una de las
ramas del if, tenemos un numArgs <= 4, porque eso nos pide el enunciado.
Imagínate que en nuestro código necesitáramos usar ese 4 en muchos más sitios y que,
al cabo de un tiempo, nuestro cliente cambia de opinión o de necesidades, y quiere que
ahora lo limitemos a, no sé, 5.
¿Qué hay que hacer en ese caso? Tendríamos que ir recorriendo nuestro código e ir
reemplazando todos los 4 que encontrásemos y cambiarlos por 5.
¡Pero cuidado! Quizá en nuestro código hay varios 4 que representan el número de
estaciones del año o el número de ruedas de un coche. Por tanto, el «ir reemplazando»
se convertiría en analizar todos los 4 y valorar si debemos o no cambiarlos por 5.
Tener esos 4 en el código es una mala práctica que se puede llamar números mágicos.
Se aplica a cualquier cifra que aparezca en el código, excepto -1, 0, 1 y pocos más. Es
decir, cuando escribas en tu código cifras, números… ¡alerta!
78 Estructuras condicionales
Ya, ya sé que los necesitas, que no has puesto el 4 por capricho, que de alguna forma
tienes que comparar el número de argumentos que recibes con ese 4. No te preocupes,
tienes solución: las constantes.
Las constantes son unas «variables» especiales, que no varían. Y para lograr ese efecto
en el código añadiremos las palabras static y final a su declaración.
¿Recuerdas por qué hay que poner static en el método main para no requerir un objeto
para poder usarlo? Lo mismo pasa con las constantes. Si estamos tratando círculos, nos
da igual que sea un círculo grande o pequeño, rojo o verde, PI tendrá el mismo valor
para todos ellos. Incluso si no tenemos ningún círculo, PI mantendrá su valor.
En cuanto a final, lo que le dice al compilador Java es que a esa variable no puede
asignársele otro valor, es decir, podemos hacer PI = 3,141592 cuando la declaremos, pero
más tarde no podemos hacer PI = cualquier otra cosa.
Así pues, la forma de declarar una constante sería:
<visibilidad> static final <tipo> <nombre> = <valor>;
Por ejemplo:
public static final double PI = 3.141592;
private static final String SALUDO = "Hola!";
protected static final int NUM_DIAS_SEMANA = 7;
public static final int MAX_PETICIONES = 7;
El número PI o el número de días que tiene una semana no parece que vayan a cambiar nunca,
o al menos que nosotros vayamos a verlo. Pero el saludo ese fijo, o el número máximo de
peticiones, puede que sí. Pues nada, si hay que cambiar el valor, se lo cambiamos, pero ya solo
en un punto de nuestro código y sin tener que analizar cada caso para distinguirlo, porque
ya tendremos un nombre distinto para cada significado, en vez de un único valor.
Y hablando de nombre, ¿te has fijado en los ejemplos y en cómo he escrito los nombres
de las constantes? En efecto, las escribo en mayúsculas y usando guiones bajos (_) (may
+ guion normal) para separar las palabras que lo componen. Esta es la forma como marcan
las convenciones de Java que hay que nombrar las constantes. Así, cuando las vemos en
medio del código, las identificamos perfectamente.
NOTA:
Recordemos las reglas de nombrado que hemos visto hasta ahora:
• nombreMetodo()
• nombreVariable
• NombreClase
• NOMBRE_CONSTANTE
Constantes 79
Las constantes se deben colocar dentro de la clase, pero fuera de los métodos. Y, por
convención, se ponen al principio de la clase, antes de los métodos. Con el ejemplo de
código de qué hacer cuando hace frío, Ejemplo03_02, deberíamos reemplazar ese 15 que
marcaba que ya hacía frío por una constante:
public class Ejemplo03_03 {
private static final int TEMP_FRIO = 15;
public static void main(String[] args) {
int temp = [Link](args[0]);
if (haceFrio(temp)) {
ponerCalefaccion();
abrigarse();
}
}
private static boolean haceFrio(int temp) {
return temp <= TEMP_FRIO;
}
T3.3 private static void ponerCalefaccion() {
[Link]("Calefacción a tope!!");
}
T3.4
private static void abrigarse() {
[Link]("¿Dónde está mi batamanta?");
}
E3.3 }
¡Puedes probarlo por tu cuenta!
Muchas condiciones: switch
Pueden darse casos en los que tengamos un montón de ramas en nuestro if / else if /
else if / else if / else… ¿Qué hacer si todas estas ramas son sobre una misma variable,
y lo que estamos comprobando es qué valor tiene, como por ejemplo aquí?
if (numLados == 3) {
figura = "triángulo";
} else if (numLados == 4) {
figura = "cuadrilátero";
} else if (numLados == 5) {
figura = "pentágono";
} else if (numLados == 6) {
figura = "hexágono";
} else {
figura = "este no me lo sé ;)";
}
En estos casos podemos usar una nueva estructura, el switch. No la tienen todos los
lenguajes, pero, como ves, tampoco es una estructura imprescindible… siempre podremos
usar muchas ramas else if.
Veámoslo en este mismo ejemplo:
switch (numLados) {
case 3:
figura = "triángulo";
break;
case 4:
figura = "cuadrilátero";
80 Estructuras condicionales
break;
case 5:
figura = "pentágono";
break;
case 6:
figura = "hexágono";
break;
default:
figura = "este no me lo sé ;)";
}
Estudiemos las palabras nuevas:
• switch: es la palabra reservada que inicia la estructura. Irá seguida de unos paréntesis
con el nombre de la variable sobre la que hacemos las evaluaciones y luego unas
llaves, dentro de las que meteremos todos los casos.
• case: palabra reservada para cada caso. Irá seguida del valor de ese caso y dos puntos.
• break: ay, el break, la guerra que da. En los case ves que no usamos las llaves, si no
que metemos todas las instrucciones tras cada caso. Y, en este ejemplo, en todas las
ramas hemos añadido la instrucción break. Lo hemos hecho porque, si no, el switch
seguiría ejecutando el caso siguiente. Por ejemplo, si olvidáramos el break del caso
5, si ejecutamos con un 5, ejecutaría dos instrucciones, figura = "pentágono" y luego
figura = "hexágono", que machacaría el valor anterior, así que nuestro método nos
diría que una figura de 5 lados se llama hexágono.
• default: para la rama por defecto… ¿qué hacemos si nuestro caso no está cubierto
por ninguna rama? Lo que sea, pero lo tenemos que poner en la rama default, que
siempre será la última y ya no necesita break. No es obligatorio tener default, si no
lo necesitamos, pero a veces puede que no compile si no lo ponemos, ¿por qué?
Imagínate que figura no está inicializado y luego lo usamos: si no escribimos esa
rama default, figura no se inicializaría y, por eso, no compilaría. Pero no por no tener E3.4
rama default, que como hemos dicho es opcional.
Cuando no hace falta el if
Hasta ahora hemos visto cómo usar un if, un if/else, un if/else if/else e incluso el switch.
Es la primera estructura que aprendes y es normal que te haga ilusión usarla por todos
lados. Pero si de verdad quieres ser un buen programador y que nadie se escandalice al
leer tu código, es importante que también aprendas en qué casos no es necesario el if.
Quizá ahora te cueste un poco verlo. Quizás te cueste escribir el código directamente sin
el if, pero lo que me gustaría es que, aunque lo escribas, luego seas capaz de darte cuenta
de que no hacía falta y que, además, tengas el valor de quitarlo, al reconocer que ese
código es innecesario. Sí, aunque ya lo hayas escrito. Lo podemos borrar, no pasa nada.
Veamos un ejemplo para entender de qué hablo. Hagamos un método que nos diga si
alguien es mayor de edad.
Cuando no hace falta el if 81
Será un método que reciba un entero con el número de años de una persona (recuerda que es
poco recomendable poner eñes en el código, así que tendrás que llamar a la variable anos,
anhos, agnos, anyos… como tú quieras simular la ñ). La verdad es que esto no debería ser un
problema en la programación profesional porque lo suyo es escribir el código en inglés…
El código que nos saldría de primeras sería:
private static boolean esMayorEdad(int anyos) {
if (anyos >= 18) {
return true;
} else {
return false;
}
}
¿Verdad? (Bueno, usando una constante para los 18, ¡¡claro!!).
Si se cumple la condición (anyos >= 18), devolvemos cierto. Si no se cumple, devolvemos
falso. ¿Eso no sería lo mismo que devolver la condición? ¡Pues entonces!, ¿para qué
queremos cinco líneas de código, con un if y dos return si con una línea lo resolvemos?
private static boolean esMayorEdad(int anyos) {
return anyos >= 18;
}
Léelo y piénsalo las veces que lo necesites, pero es imprescindible que veas que los dos
trozos de código hacen lo mismo y que, cuando escribas el primero, lo detectes y lo
modifiques hasta tener el segundo.
Esta es la versión más fácil. Compliquémoslo un poco. Pongamos que al escribir la
solución lo que nos ha salido es esto:
private static boolean esMayorEdad(int anyos) {
if (anyos < 18) {
return false;
} else {
return true;
}
}
Esta vez no podemos devolver la condición directamente, porque cuando se cumple la
condición devolvemos falso y, cuando no se cumple, cierto. Sin embargo, lo que sí es
posible devolver es la condición negada:
private static boolean esMayorEdad(int anyos) {
return !(anyos < 18);
}
E3.5 Por favor, ten en cuenta esto que acabamos de comentar cuando escribas o revises tu código.
Tener este tipo de if en tu obra solo es una señal de que aún no eres muy buen programador.
El operador ternario
Además del if y el switch que hemos visto hasta ahora, hay otra forma de hacer ejecuciones
condicionales. Se llama operador ternario y tiene este aspecto:
tipo variable = condicion ? caso1 : caso2;
82 Estructuras condicionales
Que sería lo mismo que hacer:
tipo variable;
if (condicion) {
variable = caso1;
} else {
variable = caso2;
}
O visto en un caso más realista:
boolean llega = ...
String saludo = llega ? "Hola" : "Adiós";
De forma que si llega es cierto, nos devolverá "Hola", pero si es falso, nos devolverá "Adiós".
Se llama operador ternario porque tiene tres parámetros (la condición, las instrucciones
si se cumple y las instrucciones si no se cumple) y porque es el único operador ternario
que nos ofrece Java. Si hubiera más, tendrían que haberse pensado un nombre mejor.
Este operador puede resultar muy útil en casos como este: T3.5
String tratamiento = isHombre ? "Sr." : "Sra.";
[Link]("Hoy la máxima será de " + grados + "º" + (grados < 0 ? "
negativos" : "");
String nombreCompleto = nombre + (segundoNombre != null ? segundoNombre : ""); T3.6
Pero si lo complicamos mucho, puede resultar difícil de leer: tanto es así que he estado
en proyectos en los que directamente estaba prohibido su uso. El pobre operador ternario E3.6
no es tan malo, si se usa bien, así que útilizalo solo en casos cortitos y claros.
Test y ejercicios
Test 3.1. ¿Cuáles de estas expresiones son booleanas?
1) a = b; siendo a y b booleanos
2) a == b; siendo a y b números
3) !a && (b ^ c) || d & (e >= f); siendo a, b, c y d booleanos y e y f números
a) todas
b) ninguna
c) 1y2
d) 2y3
e) 3y1
f) 1
g) 2
h) 3
Test 3.2. ¿Cuánto vale x?
int a = 5;
int b = 8;
int c = 3;
boolean x = !((b - c) <= a) && (c * c != a);
a) cierto
b) falso
c) esto no compila
Test y ejercicios 83
Test 3.3. Según la convención de nombrado que hemos aprendido, ¿qué
representa cada una de estas palabras?
[Link](SALUDO);
a) System: constante - out: clase - println: método - SALUDO: texto
b) System: clase - out: clase - println: variable - SALUDO: constante
c) System: clase - out: variable (atributo) - println: método - SALUDO: constante
d) System: constante - out: variable (atributo) - println: variable - SALUDO: texto
Test 3.4. ¿En cuál de estas opciones se respetan las reglas de nombrado?
a) string Saludo = SALUDO-CORTO + nombre_usuario;
b) String saludo = SALUDO_CORTO + nombreUsuario;
c) String Saludo = SALUDO_CORTO + NombreUsuario;
d) string saludo = SALUDO-CORTO + nombre-Usuario;
Test 3.5. ¿Cuál es la traducción correcta de este código a ternario?
private String preparaNombre(boolean mujer) {
String nombre;
if (mujer) {
nombre = "Mario";
} else {
nombre = "Maria";
}
}
a) String nombre = mujer ? "Maria" : "Mario";
b) String nombre = mujer ? "Mario" : "Maria";
c) String nombre = !mujer ? "Maria" : "Mario";
d) String nombre = !mujer ? "Mario" : "Maria";
Test 3.6. ¿Cuánto vale x?
int a = -3;
int b = -8;
int c = 4;
int d = 6;
int x = (a < 0 ? a : -a) * (b > c ? b : d > c ? c : d);
a) 24
b) -24
c) -12
d) 12
Ejercicio 3.1. Comprobar el número de argumentos que recibe un programa
Comprueba el número de argumentos que recibe tu programa. Si no recibe argumentos, avisa al
usuario («No se han recibido argumentos»). En caso contrario, indícale cuántos has recibido («Se
han recibido … argumentos»).
Ejercicio 3.2. Comprobar el número de argumentos que recibe un programa
o si son demasiados
Comprueba el número de argumentos que recibe tu programa. Si no recibe argumentos, avisa al
usuario. Si recibe hasta 4, indícale cuántos has recibido. Si recibe más, avisa al usuario.
Buscamos un comportamiento como el de este ejemplo:
> java Ejercicio03_02
No se han recibido argumentos
> java Ejercicio03_02 A
Se han recibido 1 argumentos
> java Ejercicio03_02 A B C D
84 Estructuras condicionales
Se han recibido 4 argumentos
> java Ejercicio03_02 A B C D E
Se han recibido demasiados argumentos
Ejercicio 3.3. Comprobar el número de argumentos que recibe un programa
o si son demasiados usando constantes
Sobre el código del ejercicio 3.2, pon el número máximo de argumentos aceptables en una constante.
Ejercicio 3.4. Calcula cuántos días tiene cada mes
Escribe un programa que reciba el número de mes y devuelva el número de días que tiene. Ignora
los años bisiestos. Solo debes hacer algo si recibes un único parámetro.
Salidas esperadas:
> java Ejercicio03_04
> java Ejercicio03_04 0
0 no es un mes válido
> java Ejercicio03_04 1
El mes 1 tiene 31 días.
> java Ejercicio03_04 2
El mes 2 tiene 28 días.
> java Ejercicio03_04 3
El mes 3 tiene 31 días.
> java Ejercicio03_04 12
El mes 12 tiene 31 días.
> java Ejercicio03_04 13
13 no es un mes válido
Ejercicio 3.5. Simplificar if
Corrige este fragmento de código evitando los if innecesarios.
Este trozo de código, que funciona, tiene (o no) if innecesarios, simplificables. Simplifícalos y explica
el porqué de tus cambios.
public class Ejercicio03_05 {
private static final String CANTA = "CANTA";
private static final String LADRA = "LADRA";
public static void main(String[] args) {
if (args != null) { // [1]
boolean hayArgumentos;
if ([Link] > 0) { // [2]
hayArgumentos = true;
} else {
hayArgumentos = false;
}
if (!hayArgumentos) { // [3]
[Link]("No tengo argumentos");
return;
}
if (hayArgumentos) { // [4]
int numArgumentos = [Link];
String primerArgumento = args[0];
if ([Link](CANTA)) { // [5]
[Link](
"Un, dos, tres, un pasito palante, María!");
} else if ([Link](primerArgumento)) { // [6]
boolean faltaNombre;
String nombrePerro = "";
Test y ejercicios 85
if (numArgumentos > 1) { // [7]
faltaNombre = false;
nombrePerro = args[1];
[Link]("Bub bub bub");
} else {
faltaNombre = true;
[Link]("Grr grr grr");
}
if (faltaNombre) { // [8]
[Link]("No sé cómo te llamas");
} else {
[Link]("Hola " + nombrePerro);
}
} else {
[Link]("No sé qué quieres que haga");
}
}
} else {
[Link]("Necesito argumentos");
}
}
}
Ejercicio 3.6. Calcula el valor absoluto de un float
Calcula el valor absoluto de un float recibido como parámetro. Enunciado simple donde los haya,
pero no uses librerías existentes. ¿Lo intentas?
Soluciones
Test 3.1. ¿Cuáles de estas expresiones son booleanas?
1) a = b; siendo a y b booleanos
2) a == b; siendo a y b números
3) !a && (b ^ c) || d & (e >= f); siendo a, b, c y d booleanos y e y f números
d) 2 y 3: La primera expresión es una asignación, por tanto, no es una expresión booleana,
mientras que las otras dos sí lo son. La segunda nos dirá si a y b tienen el mismo valor;
y la tercera, siendo muy compleja y poco probable de ver en código real, es una expresión
booleana correcta.
Test 3.2. ¿Cuánto vale x?
int a = 5;
int b = 8;
int c = 3;
boolean x = !((b - c) <= a) && (c * c != a);
b) falso: ¿Lo has calculado o lo has echado a suertes? Reemplacemos valores:
x = !((8 - 3) <= 5) && (3 * 3 != 5)
x = !(5 <= 5) && (9 != 5)
x = !cierto && cierto
x = falso && cierto
x = falso
Test 3.3. Según la convención de nombrado que hemos aprendido, ¿qué
representa cada una de estas palabras?
[Link](SALUDO);
c) System: clase - out: variable (atributo) - println: método - SALUDO: constante
86 Estructuras condicionales
Efectivamente, las clases empiezan por mayúscula, como System. Las variables o los
atributos (que aún no hemos visto), en minúsculas. Los métodos también, pero
seguidos de paréntesis. Y todo en mayúsculas lo usamos para las constantes.
Test 3.4. ¿En cuál de estas opciones se respetan las reglas de nombrado?
b) String saludo = SALUDO_CORTO + nombreUsuario;: Perfecto, siempre y cuando
SALUDO_CORTO sea una constante y nombreUsuario una variable.
Test 3.5. ¿Cuál es la traducción correcta de este código a ternario?
private String preparaNombre(boolean mujer) {
String nombre;
if (mujer) {
nombre = "Mario";
} else {
nombre = "Maria";
}
}
b) String nombre = mujer ? "Mario" : "Maria";: ¡Correcto! Aunque la respuesta
no sea muy lógica, este código hace lo mismo que el código original y evita complicaciones
innecesarias como negar las condiciones.
Test 3.6. ¿Cuánto vale x?
int a = -3;
int b = -8;
int c = 4;
int d = 6;
int x = (a < 0 ? a : -a) * (b > c ? b : d > c ? c : d);
c) -12: Como a es negativa (< 0), cogemos a y la multiplicamos por lo que nos dé la segunda
parte. Como b > c no se cumple, comprobamos d > c: como sí se cumple, el valor escogido
es c.
a * c = -3 * 4 = -12.
Ejercicio 3.1. Comprobar el número de argumentos que recibe un programa
• Para todo ejercicio, primero creamos la clase y el método main vacío.
public class Ejercicio03_01{
public static void main(String[] args) {
}
}
• Guardamos en una variable (podríamos evitarlo, pero vayamos adquiriendo buenas prácticas
desde el principio) el número de argumentos que tenemos. Lo sacamos consultando la longitud
del array args.
public class Ejercicio03_01 {
public static void main(String[] args) {
int numArgs = [Link];
}
}
• Si ese valor es igual a 0 (dos iguales, recuerda), pintamos un mensaje de error. Para distinguirlo
de uno normal podemos usar [Link] en vez de [Link]. out es para la salida estándar,
err es para la salida de error. En muchos IDE veremos en rojo la salida de error.
public class Ejercicio03_01 {
public static void main(String[] args) {
int numArgs = [Link];
Soluciones 87
if (numArgs == 0) {
[Link]("No se han recibido argumentos");
}
}
}
• Si no, pintamos cuántos argumentos hemos recibido.
public class Ejercicio03_01 {
public static void main(String[] args) {
int numArgs = [Link];
if (numArgs == 0) {
[Link]("No se han recibido argumentos");
} else {
[Link]("Se han recibido " + numArgs + " argumentos");
}
}
}
• Y, como siempre, ¡hay que probarlo!
> javac Ejercicio03_01.java
> java Ejercicio03_01
No se han recibido argumentos
> java Ejercicio03_01 a b c
Se han recibido 3 argumentos
Ejercicio 3.2. Comprobar el número de argumentos que recibe un programa
o si son demasiados
Para no resolver este caso de cero, tomamos el ejercicio 3.1 y renombramos clase y fichero.
public class Ejercicio03_02 {
public static void main(String[] args) {
int numArgs = [Link];
if (numArgs == 0) {
[Link]("No se han recibido argumentos");
} else {
[Link]("Se han recibido " + numArgs + " argumentos");
}
}
}
• simplemente, aplicamos lo aprendido y añadimos una rama else if. Nos quedarán entonces
tres ramas:
• si no tenemos argumentos
• si tenemos cuatro o menos
• si tenemos más
public class Ejercicio03_02 {
public static void main(String[] args) {
int numArgs = [Link];
if (numArgs == 0) {
[Link]("No se han recibido argumentos");
} else if (numArgs <= 4) {
[Link]("Se han recibido " + numArgs + " argumentos");
} else {
[Link]("Se han recibido demasiados argumentos");
}
}
}
Importante, no solo probar, sino probar bien nuestros programas. Hay que probar los
casos extremos de cada opción y los que se quedan fuera.
88 Estructuras condicionales
• Como no podemos probar con un número negativo de argumentos, nuestra primera prueba
será con cero argumentos, es decir, sin. Nos tiene que dar «No se han recibido argumentos».
> java Ejercicio03_02
No se han recibido argumentos
• Segunda prueba, con menos de cuatro argumentos (uno, por ejemplo):
> java Ejercicio03_02 a
Se han recibido 1 argumentos
• Y con cuatro (justo el límite de nuestro else if):
> java Ejercicio03_02 a b c d
Se han recibido 4 argumentos
• Y con más de cuatro, es decir, con cinco:
> java Ejercicio03_02 a b c d e
Se han recibido demasiados argumentos
Si estas pruebas salen bien, ya podemos casi garantizar que nuestro programa funcionará
bien con cualquier número de argumentos. Tampoco tendría ningún sentido probar desde
cero hasta cien argumentos ni el esfuerzo aportaría nada a nuestra prueba.
Ejercicio 3.3. Comprobar el número de argumentos que recibe un programa
o si son demasiados usando constantes
• Tomamos el ejemplo base y renombramos clase y fichero.
• Declaramos la constante, privada, porque no la necesitamos fuera de nuestro código y luego
la hemos usado en la rama del if que tocaba, remplazando el 4 que era un número mágico.
public class Ejercicio03_03 {
private static final int MAX_ARGS = 4;
public static void main(String[] args) {
int numArgs = [Link];
if (numArgs == 0) {
[Link]("No se han recibido argumentos");
} else if (numArgs <= MAX_ARGS) {
[Link]("Se han recibido " + numArgs + " argumentos");
} else {
[Link]("Se han recibido demasiados argumentos");
}
}
}
Acuérdate de probarlo.
Ejercicio 3.4. Calcula cuántos días tiene cada mes
En esta solución tenemos dos métodos: el main y calculaDias, que se encargará de darnos la cifra
de días correspondiente a cada mes. En el método main nos aseguraremos de llamar a calculaDias
solo con datos buenos, según las condiciones de nuestro enunciado.
public class Ejercicio03_04 {
public static void main(String[] args) {
}
private static int calculaDias(int mes) {
}
}
Soluciones 89
• Así pues, en el main, lo primero que hacemos es comprobar que únicamente recibimos un
parámetro y, si es así, lo convertimos a número (si no fuera un número, explotaría todo,
pero aún es pronto para aprender a tratar excepciones). A continuación, comprobaremos
si el número recibido está dentro del rango deseado. Como ves, practicamos el if, el or y
los comparadores.
public class Ejercicio03_04 {
public static void main(String[] args) {
if ([Link] == 1) {
int mes = [Link](args[0]);
if (mes < 1 || mes > 12) {
[Link](mes + " no es un mes válido");
} else {
}
}
}
private static int calculaDias(int mes) {
}
}
• Una vez estamos seguros de que tenemos un número entre uno y doce, ya llamamos a
calculaDias.
public class Ejercicio03_04 {
public static void main(String[] args) {
if ([Link] == 1) {
int mes = [Link](args[0]);
if (mes < 1 || mes > 12) {
[Link](mes + " no es un mes válido");
} else {
int dias = calculaDias(mes);
[Link]("El mes " + mes + " tiene " +
dias + " días.");
}
}
}
private static int calculaDias(int mes) {
}
}
• Es en calculaDias que vamos a usar el switch. En esta solución he dejado unos cuantos [Link]
en el código, solo para que al ejecutar se vea lo que está pasando, pero lo realmente importante
es el resultado que devuelve (return dias). Puedes quitar todos esos [Link].
public class Ejercicio03_04 {
public static void main(String[] args) {
if ([Link] == 1) {
int mes = [Link](args[0]);
if (mes < 1 || mes > 12) {
[Link](mes + " no es un mes válido");
} else {
int dias = calculaDias(mes);
[Link]("El mes " + mes + " tiene " +
dias + " días.");
} // if mes
} // if args
}
90 Estructuras condicionales
private static int calculaDias(int mes) {
int dias;
switch (mes) {
case 2:
[Link]("Febrero");
dias = 28;
break;
case 4:
[Link]("Abril");
case 6:
[Link]("Junio");
case 9:
[Link]("Septiembre");
case 11:
[Link]("Noviembre");
dias = 30;
break;
default:
[Link]("Mes de los largos");
dias = 31;
break;
} // switch
return dias;
}
} // clase
Esta solución es un poco compleja de leer, pero lo he hecho adrede.
El primer caso que se trata es el más raro de todos: febrero, que es el único mes que tiene
28 días. Así pues, si recibimos un 2, devolvemos un 28. break. Y ya hemos terminado.
Luego tratamos los meses de 30 días, que son unos cuantos, concretamente abril, junio,
septiembre y noviembre. Como en estos casos la respuesta debe ser la misma, ponemos
dias = 30 y el break en el último caso, y en el resto, simplemente dejamos que siga
ejecutándose (y metiéndose en el siguiente). Si pruebas el código, verás que, por ejemplo,
con un 4, escribe "Abril", "Junio", "Septiembre" y "Noviembre" por esos [Link] que
hemos puesto para ver qué pasa, pero que finalmente devuelve el 30 que queríamos.
Para terminar, tenemos los meses de 31, que son todos los demás, así que los ponemos en
la rama default. Esto lo podemos hacer porque ya hemos controlado en el main que el
mes estará entre 1 y 12, si no, tendríamos que haber puesto las ramas correspondientes a
cada uno de esos meses y haber dejado el caso default para tratar el error.
Como ya empieza a haber muchas llaves en el código, puedes ir marcando con comentarios
qué cierra cada llave. No es muy profesional dejar el código así, pero ahora que estamos
aprendiendo sí lo podemos hacer.
Muy probablemente tu código sea distinto al que yo propongo. No pasa nada. Las cosas
se pueden escribir de muchas maneras. Si has comprobado los resultados con los números
del -1 al 13, en principio podemos afirmar que tu código funciona bien. Por cierto, mi
código debería usar constantes, ¿las pones tú, por favor?
Soluciones 91
Ejercicio 3.5. Simplificar if
• Código simplificado:
public class Ejercicio03_05Sol {
private static final String CANTA = "CANTA";
private static final String LADRA = "LADRA";
public static void main(String[] args) {
boolean hayArgumentos = [Link] > 0;
if (!hayArgumentos) { // (3)
[Link]("No tengo argumentos");
return;
}
int numArgumentos = [Link];
String primerArgumento = args[0];
if ([Link](CANTA)) { // (5)
[Link]("Un, dos, tres, un pasito palante, María!");
} else if ([Link](primerArgumento)) { // (6)
// negamos la condición o giramos el >
boolean faltaNombre = numArgumentos <= 1;
if (!faltaNombre) { // (7)-(8)
String nombrePerro = args[1];
[Link]("Bub bub bub");
[Link]("Hola " + nombrePerro);
} else {
[Link]("Grr grr grr");
[Link]("No sé cómo te llamas");
}
} else {
[Link]("No sé qué quieres que haga");
}
}
}
• ¿Por qué?
Hemos pasado de 48 líneas a 32, reduciendo en un 33 % las líneas de código. Eso no es
un objetivo, pero sí una pista de que el código ha quedado más manejable.
Partíamos de ocho if y nos hemos quedado con cuatro, ¡la mitad!
Veamos el porqué en cada uno:
• [1]: if (args != null): args en el main nunca será nulo. Si pruebas el código sin
pasarle parámetros, verás que te dice "No tengo argumentos" y no "Necesito argumentos".
Podemos suprimir este if y su else, y quitarle a todo el código un nivel de tabulación. Es
un caso imposible.
• [2]: if ([Link] > 0): lo único que estamos haciendo dentro de cada rama
es darle un valor cierto o falso a un booleano. Todo este if se puede simplificar en:
boolean hayArgumentos = [Link] > 0;
Si se cumple la condición tomará el valor cierto y, si no, falso (como en el código original,
pero mucho más simple y legible).
• [3]: if (!hayArgumentos): este lo mantenemos, está bien.
• [4]: if (hayArgumentos): como este if tiene la condición inversa al if [3], lo suyo es
que fuera un else del if [3]. Sin embargo, si te fijas, el if [3] termina con un return, es decir,
que al if [4] solo podremos llegar si no entramos en el [3]. Podemos suprimir el if [4].
• [5] if (CANTA): lo necesitamos para la lógica de nuestro programa.
92 Estructuras condicionales
• [6] if (LADRA): lo seguimos necesitando.
• [7] if (numArgumentos > 1): dentro de este if rellenamos un booleano (faltaNombre)
a falso en el if, a cierto en el else... Eso nos podría hacer pensar que existe la posibilidad
de cargarnos el if y hacer boolean faltaNombre = argumentos <= 1 y ya está. Pero fíjate
en que dentro de ambas ramas se hacen cosas, así que del if no nos libramos, aunque
podríamos sacar fuera la inicialización de faltaNombre y cambiar la condición a
!faltaNombre.
• [8] if (faltaNombre): si hacemos lo dicho en el if [7], nos encontraremos con otro if
con las mismas condiciones pero giradas. ¡Vaya lío! Mejor reemplazamos los if [7] y [8] por
este fragmento muchos más claro:
boolean faltaNombre = numArgumentos <= 1; // negamos la condición o giramos el >
if (!faltaNombre) { // (7)-(8)
String nombrePerro = args[1];
[Link]("Bub bub bub");
[Link]("Hola " + nombrePerro);
} else {
[Link]("Grr grr grr");
[Link]("No sé como te llamas");
}
Ejercicio 3.6. Calcula el valor absoluto de un float
• Primero, preparamos la clase y el main.
public class Ejercicio03_06 {
public static void main(String[] args) {
}
}
• Recuperamos el argumento y, esta vez, en vez de transformarlo a entero, lo hacemos a número
decimal (float).
public class Ejercicio03_06 {
public static void main(String[] args) {
float num = [Link](args[0]);
}
}
• Y ahora, ¿cómo calculamos el valor absoluto? Realmente Java tiene clases con esto implementado,
pero como queremos practicar, vamos a hacerlo nosotros. Recordemos que el valor absoluto
de un número positivo es ese mismo número, y de un valor negativo es el mismo número,
pero cambiándole el signo.
public class Ejercicio03_06 {
public static void main(String[] args) {
float num = [Link](args[0]);
float abs = num > 0 ? num : -num;
}
}
• Y lo pintamos:
public class Ejercicio03_06 {
public static void main(String[] args) {
float num = [Link](args[0]);
float abs = num > 0 ? num : -num;
[Link]("El valor absoluto de " + num + " es " + abs);
}
}
También podríamos haberlo hecho con un if. Si quieres intentarlo, puede ser una buena práctica.
Soluciones 93
4 Estructuras iterativas
En este capítulo aprenderás a:
• Identificar la sintaxis de los cuatro tipos de bucle que tiene Java.
• Trabajar con los criterios para escoger uno u otro.
• Usar la clase Scanner.
• Manejar caracteres especiales en Java.
Introducción
Ahora que ya tenemos controladas las estructuras condicionales, con las que ejecutamos
unas instrucciones u otras en función de una condición, veamos los bucles, que nos van a
permitir repetir una y otra vez las mismas instrucciones, mientras se cumpla una condición.
Con el switch ya vimos que solo aportaba «otra forma» de hacer las cosas, pero que
únicamente con if podíamos hacerlo. Con los bucles nos pasará lo mismo. Veremos cuatro
formas de hacerlo, pero todo lo podríamos resolver con una sola de ellas, jugando con
las condiciones. Sin embargo, ya que tenemos cuatro formas de hacerlo, las veremos
todas y aprenderemos cuándo y por qué es mejor usar cada una de ellas.
Bucle «para cada» elemento de una colección
(for each)
Empezaremos por el for each, que es la última que se incluyó en Java. Esto quiere decir
que si algún día trabajas en un entorno en el que haya una versión de Java anterior a la
5 no podrás usarlo. Si coges un libro antiguo o lees código escrito hace muchos años,
esta estructura no la encontrarás. Pero en código nuevo, es posible usarla sin problemas,
cuando creas que es la mejor opción para tu caso.
«for each» significa ‘para cada’. Está pensado para aplicar unas instrucciones para cada
elemento de una lista o colección. Por tanto, solo lo usaremos cuando necesitamos hacer algo
a todos los elementos. Si no es a todos, ya lo haremos con las otras estructuras que veremos.
Su sintaxis es la siguiente:
for (<tipo> <variable_elemento> : <variable_lista>) {
instrucciones;
}
Siendo:
• for: la palabra clave para esta estructura.
• <variable_lista>: el array, lista, colección… que contiene todos esos elementos que
queremos recorrer.
• <tipo>: el tipo de dato de los elementos a tratar.
• <variable_elemento>: la variable que usaremos dentro del bucle para manejar cada
uno de esos elementos.
Veámoslo más claro en un ejemplo: vamos a recorrer todos los argumentos que recibamos
en nuestro método main y a pintarlos por pantalla, uno a uno.
public class Ejemplo04_01 {
public static void main(String[] args) {
for (String s : args) {
[Link](s);
}
}
}
96 Estructuras iterativas
Por cada elemento del array args, que será un String y que llamaremos s, hacemos un
[Link] de s.
> javac Ejemplo04_01.java
> java Ejemplo04_01 hola cómo estás
hola
cómo
estás
T4.1
Aunque lo llamemos «for each», solo se usa la palabra «for» en el código, al igual que para
el siguiente ejemplo que veremos, el «for de toda la vida». Pero no te preocupes, que el
resto de la sintaxis hace muy fácil distinguirlos. En el caso del «for each», tras el for, dentro E4.1
de los paréntesis, tendremos esos dos puntitos que no encontraremos en el for clásico.
Bucle «para» (for)
Vamos a ver ahora el otro for, el que existe «desde siempre». Su sintaxis:
for (<inicialización>; <condición>; <avance>) {
instrucciones;
}
Siendo:
• for: de nuevo la misma palabra clave. ¿Cómo distinguirlo del for each? Lo hemos
avanzado antes, pero mira lo que hay dentro del paréntesis: si es como lo que estamos
viendo ahora, con sus puntos y coma, es un for; si tiene los dos puntitos que hemos
visto antes, es un for each. Fácil, ¿no?
• <inicialización>: instrucciones que declaran e inicializan las variables con las que
trabajaremos en el bucle. Ejemplo tipiquísimo: int i = 0;
• <condición>: instrucciones para comprobar si seguimos iterando una vez más o toca
salir ya. Siguiendo con el ejemplo tipiquísimo: i < NUM;
• <avance>: instrucciones que harán que avance nuestro bucle. Si no lo hacemos bien,
tendríamos un bucle infinito que no terminaría hasta hacer reventar la memoria.
Siguiendo con lo clásico: i++;
Así pues, el ejemplo superclásico completo sería:
public class Ejemplo04_02 {
private static final int NUM = 3;
public static void main(String[] args) {
for (int i = 0; i < NUM; i++) {
[Link](i);
}
}
}
Vemos en este ejemplo que ya no estamos recorriendo una lista, si no que estamos
repitiendo el número de veces que queramos unas instrucciones. En este caso, empezamos
dándole el valor 0 a i, como NUM vale 3 y como cero es menor que tres, ejecutamos lo
Bucle «para» (for) 97
de dentro del bucle: pintamos un 0. Seguimos: i++, es decir, incrementamos el valor de
i en 1 (i++ es lo mismo que hacer i = i + 1). Ahora i vale 1. Continúa siendo menor que
tres, pues entramos en el bucle e imprimimos un 1. Seguimos: i++, ahora i vale 2. Sigue
siendo menor que tres, pues entramos en el bucle e imprimimos un 2. ¿Me estoy repitiendo
un poco, no? ¡Pues justo para eso son los bucles! Venga, seguimos: i++, ahora i vale 3.
Pero ahora 3 ya no es menor (estricto) que tres. ¡Hemos terminado! Salimos del bucle y
podemos seguir con el resto del programa. Nada más en este caso.
Comprobémoslo ejecutando este código:
> javac Ejemplo04_02.java
> java Ejemplo04_02
0
1
2
Dijimos que el for each antes no existía y que, de hecho, no hacen falta todos los tipos
de bucle que tenemos, ya que las cosas se pueden hacer con uno u otro… Volvamos al
ejemplo 4.1, en el que imprimíamos por pantalla uno a uno los argumentos recibidos,
pero ahora lo haremos con un for clásico:
public class Ejemplo04_03 {
public static void main(String[] args) {
for (int i = 0; i < [Link]; i ++) {
String s = args[i];
[Link](s);
}
}
}
> javac [Link]
> java Ejemplo0403 hola cómo estás
hola
T4.2 cómo
estás
Es quizá un pelín más engorroso, por eso se inventaron el for each, pero, como ves,
T4.3
funciona exactamente igual. En este caso, necesitamos llevar un contador, i, sobre por
qué elemento de la lista vamos (int i = 0), ir avanzando ese contador en cada iteración
E4.2 (i++), recuperar cada elemento (String s = args[i]) y controlar cuándo tenemos que terminar
(i < [Link]).
E4.3 En otros casos vas a descubrir una situación en la que te convendrá más usar el for que
el for each. Con for each quedaría demasiado engorroso, ¡ya verás!
Bucle «mientras» se cumpla la condición (while)
Veamos otro tipo de bucle, el while, leído ‘mientras’, en castellano. En los ejemplos y
ejercicios que hemos visto con el for recorríamos nuestra lista desde el principio hasta el
final, pero hay veces que bien no tenemos una lista ni un número predeterminado
indicando el número de veces que tenemos que ejecutarlo o, aun teniéndolo, no queremos
llegar siempre hasta el final. En este tipo de casos, lo suyo es usar el bucle while que
98 Estructuras iterativas
aprenderemos ahora. Casi todo se puede hacer con todos los tipos de bucle, pero lo suyo
es buscar para cada caso el que deja un código más fácil de entender y mantener, el que
mejor se ajusta a cada situación.
<inicialización>;
while (<condición>) {
instrucciones;
<avance>;
}
La sintaxis se va simplificando:
• <inicialización>: si lo necesitamos, tendremos que inicializar nuestras variables
fuera del bucle.
• while: la palabra clave.
• <condición>: condición que se debe cumplir para entrar dentro del bucle, es
decir, si la condición es cierta, entramos y, si es falsa, no entramos y terminamos.
• <avance>: importante no olvidar la instrucción que haga avanzar nuestro bucle…, sino
tendremos un bucle infinito que nunca terminará y hará reventar nuestro programa.
Por ejemplo, vamos a listar todos los argumentos hasta que encontremos la palabra «fin»,
pero sin incluirla en la lista.
public class Ejemplo04_04 {
// Acuérdate de usar constantes
private static final String FIN = "fin";
public static void main(String[] args) {
[Link]("Se han recibido " + [Link]
+ " argumentos:");
// El bucle while itera hasta que se cumple la condición
// No podemos olvidar controlar la posición
int i = 0; // Punto de inicio
// CUIDADO!!! No podemos comparar strings con ==
while (i < [Link] &&
!args[i].equals(FIN)) { // condición de terminación
[Link](i + ")" + args[i]);
i++; // actualización
}
// Como la i fue declarada fuera del bucle, aquí aún podemos usarla.
[Link]("\"fin\" estaba en la posición nº " + i);
}
}
Generalmente, en los while se requieren los tres pasos que teníamos en el for igualmente,
pero se colocan en sitios distintos. Necesitamos inicializar variables fuera del bucle
(int i = 0), la condición la metemos entre los paréntesis (i < [Link] && !args[i].
equals(FIN)) y la actualización dentro del bucle (i++;). Observa las dos estructuras a la
vez:
for (int i = 0; i < NUM; i ++) {
instrucciones;
}
Bucle «mientras» se cumpla la condición (while) 99
int i = 0;
while (i < NUM) {
instrucciones;
i++;
T4.4 }
Estos dos bucles funcionarían exactamente igual.
E4.4
Cuando queremos usar un while para recorrer una lista hasta hallar un elemento que cumpla
(o deje de cumplir) cierta condición, hay que recordar que podríamos terminar de recorrer
E4.5 la lista sin haberlo encontrado… Por tanto, es importante tener dos condiciones, la que
controla si hemos hallado lo que queríamos y la que controla que no nos salgamos de la lista.
Bucle «haz mientras» se cumpla la condición
(do while)
El tipo de bucle menos frecuente, tras el for each, el for, el while, es el do while. Es casi
casi idéntico al while, pero ¿sabes eso de que es más fácil pedir el perdón que el permiso?
Pues eso hace este tipo de bucle.
Es casi como un while, pero primero hacemos y luego, si eso, ¡preguntamos!
<inicialización>;
do {
instrucciones;
<avance>;
} while (<condición>);
La sintaxis sería la siguiente:
• <inicialización>: si lo necesitamos, tendremos que inicializar nuestras variables
fuera del bucle.
• do: una de las palabras clave.
• <avance>: importante no olvidar la instrucción que haga avanzar nuestro bucle…, sino
tendremos un bucle infinito que nunca terminará y hará reventar nuestro programa.
• while: la otra palabra clave.
• <condición>: condición que se debe cumplir para seguir dentro del bucle: si la
condición es cierta, damos otra vuelta; si es falsa, terminamos.
• ; : no te olvides del ; final para terminar la instrucción do-while, los otros tipos de
bucles no lo llevan.
Este bucle lo utilizaremos para los casos en los que siempre siempre siempre queramos
ejecutar el cuerpo del bucle al menos una vez. Por ejemplo, si estamos pidiéndole un
dato al usuario, siempre se lo pediremos una primera vez, y si nos lo da mal, insistiremos
hasta cansarnos, pero de esa primera petición no nos libramos.
Para verlo en código, vamos a listar todos los argumentos hasta que encontremos la
palabra «fin», como antes, pero esta vez sí la incluiremos en la lista.
100 Estructuras iterativas
public class Ejemplo04_05a {
// Acuérdate de usar constantes
private static final String FIN = "fin";
public static void main(String[] args) {
[Link]("Se han recibido " +
[Link] + " argumentos:");
// El bucle while itera hasta que se cumple la condición
// No podemos olvidar controlar la posición
int i = 0; // Punto de inicio
do { // condición de terminación
[Link](i + ")" + args[i]);
i++; // actualización
} while (!args[i].equals(FIN) && i < [Link]);
// Como la i fue declarada fuera del bucle, aquí aún podemos usarla.
[Link]("\"fin\" estaba en la posición nº " + i);
}
}
En este programa, lo primero que hacemos es imprimir la palabra recibida, luego ya, si
eso, miramos si hemos llegado a FIN o todavía no.
> java Ejemplo04_05a patata fin tomate
Se han recibido 3 argumentos:
0)patata
"fin" estaba en la posición nº 1
¡Pero no funciona! ¡Veamos qué está pasando!
Empezamos con i = 0, imprimimos la palabra en la posición 0, patata, incrementamos
la i a 1, y entonces comprobamos si la palabra en la posición 1 es FIN. Lo es, ¡y termina!
Pero nosotros queríamos comprobar la palabra en la posición 0, primero. Si no, ¡estamos
haciendo lo de antes!
Intentémoslo de nuevo.
public class Ejemplo04_05b {
// Acuérdate de usar constantes
private static final String FIN = "fin";
public static void main(String[] args) {
// != será cierto si ambos operandos son distintos
if ([Link] != 0) {
[Link]("Se han recibido " +
[Link] + " argumentos:");
// El bucle do while se ejecuta al menos una vez,
// y hasta que se cumple la condición
int i = 0;
String palabra; // debemos declararla fuera del bucle
// para poderlo usar en la condición
do {
palabra = args[i];
[Link](i + ")" + palabra);
i++;
} while ( && i < [Link]);
} else {
[Link]("No se han recibido argumentos");
}
}
}
Bucle «haz mientras» se cumpla la condición (do while) 101
Lo ejecutamos:
> java Ejemplo04_05b patata fin tomate
Se han recibido 3 argumentos:
0)patata
1)fin
¡Esta vez sí que ha funcionado! Veamos qué hemos hecho.
El problema que hemos tenido en el primer intento es que cuando queríamos comprobar
si la palabra i-ésima cumplía la condición, ya habíamos incrementado la i. Esta vez, lo
que hacemos es guardarnos la palabra que estamos tratando en una variable. La
inicializamos con args de i, la pintamos, incrementamos la i y en la condición usamos
nuestra variable palabra para ver si es FIN o no, y la i (ya incrementada) para controlar
que no se nos haya terminado la lista.
Además, le hemos metido un control de errores, porque ¿qué pasaba antes si no le
pasábamos nada?
> java Ejemplo04_05a
Se han recibido 0 argumentos:
Exception in thread "main" [Link]: 0
at Ejemplo04_05a.main(Ejemplo04_05a.java:12)
¿Y ahora?
> java Ejemplo04_05b
No se han recibido argumentos
¡Cuidado en el manejo de bucles! Siempre debemos probar qué pasa si la lista que
recorremos está vacía, o si no tiene el elemento que estamos buscando, o si lo tiene en
primera o en última posición… Siempre hay que probar bien los extremos y nunca
suponer que todo irá bien.
Bueno, ahora ya conoces los cuatro tipos de bucles que nos ofrece Java. Y pensándolo un
poco, me parece que los he contado justo en el orden de más a menos usado, al menos
T4.5 en mi experiencia. Recuerdo muy pocos casos de do-while y, sin embargo, casi a diario
me tropiezo con algún for each.
La clase Scanner
Para practicar casos típicos de do-while necesitamos conocer la clase Scanner.
Para interactuar con el usuario, en vez de tomar todos los datos de entrada por parámetros,
veamos cómo leer lo que nos escriba el usuario por el teclado. Así podremos pedirle, por
ejemplo, que nos diga «hola». Para ello usaremos la clase Scanner que viene con el API
de Java. Aún es pronto para aprender sobre clases y esas cosas, pero, de momento,
abordaremos cómo usar esta para darle un poco más de juego a los ejemplos y ejercicios.
Veamos, entonces, uno de estos ejemplos:
102 Estructuras iterativas
import [Link];
public class Ejemplo04_06 {
public static void main(String[] args) {
Scanner scanner = new Scanner([Link]);
[Link]("¿Cómo te llamas?");
String nombre = [Link]();
[Link]("¡Hola, " + nombre + "! ¿Qué tal?");
}
Fíjate en la primera línea que hemos escrito, fuera de la clase. Es una sentencia de
importación. Es para decirle a Java que vamos a usar una cosa que no está en nuestra
clase. ¡Es la primera vez que lo utilizamos en este libro!
En este caso usaremos la clase Scanner que ha creado la gente de Java para que nos resulte
muy fácil leer de teclado o incluso de ficheros o de sitios más complicados.
import [Link]; se leería como importa la clase Scanner del paquete [Link],
pero, como hemos dicho antes, es aún pronto para eso. De momento, créetelo y céntrate
en aprender los bucles.
Dentro del main tenemos una primera línea que «crea» ese Scanner que queremos utilizar.
Sería Scanner (con la primera en mayúscula), que es el nombre de la clase que estamos
usando, como cuando empleamos String. Luego scanner en minúscula, que es el nombre
de la variable que le hemos querido poner (podríamos haber puesto cualquier otro
nombre que nos resultara significativo) y esa variable la inicializamos con new
Scanner([Link]), que viene a decir algo así como crea un nuevo scanner para mí y
quiero que lea las cosas de [Link].
Ya conoces muy bien [Link], ¿verdad? Lo usamos para sacar cosas por pantalla. Pues
bien, de momento, quédate con que usaremos [Link] para recuperar cosas del teclado;
[Link] para comunicación de salida; y [Link] para comunicación de entrada.
En la siguiente línea le decimos algo al usuario (comunicación de salida), como hemos
hecho muchas veces hasta ahora.
Luego, declaramos una variable nombre de tipo String, la inicializamos con lo que nos
dé el scanner y se lo pedimos con un [Link]();. Finalmente, cogemos eso que
hemos leído del teclado y se lo pintamos al usuario por pantalla.
Veamos cómo funciona:
> java Ejemplo04_06
¿Cómo te llamas?
Mariona
¡Hola, Mariona! ¿Qué tal? E4.6
¡Prueba a ejecutarlo tú!
La clase Scanner 103
Bucles anidados
Conociendo los cuatro tipos de bucles que tiene Java y sabiendo cómo usar uno u otro
en función de las necesidades de nuestro problema, estamos listos para dar un paso más.
En algunas ocasiones, requeriremos más de un bucle, uno dentro de otro. Por ejemplo,
cuando tengamos que hacer algo sobre los expedientes de todos los clientes o incluso si
necesitamos repetir una tarea sobre todos los expedientes de todos los clientes de todas
las oficinas de todas las provincias. Entonces, tendremos que recorrer la lista de provincias
y por cada provincia recorrer todas sus oficinas y por cada oficina recorrer todos los
clientes y por cada cliente tratar todos los expedientes. Pues esto lo haremos anidando
bucles unos dentro de otros. Bueno, seguramente habría que usar métodos auxiliares,
pero la idea sería esa.
Cuatro dimensiones, clientes y expedientes quizá sea un poco complejo. Hagamos un
ejemplo más simple: pintar un rectángulo por pantalla.
public class Ejemplo04_07 {
public static void main(String[] args) {
int ancho = [Link](args[0]);
int alto = [Link](args[1]);
String s = "";
for (int i = 0; i < alto; i++) {
for (int j = 0; j < ancho; j++) {
s += "X";
}
s += "\n";
}
[Link](s);
}
}
Como tenemos dos bucles anidados, debemos llevar dos índices, así que en el bucle de
dentro en vez de hacer un for i, haremos for j. Si tuviéramos un tercer bucle anidado, al
índice se le suele llamar k. El for del índice i controlará las filas, mientras que el for del
índice j controlará las columnas de nuestro rectángulo. Así pues, por cada una de las filas
(bucle externo, el de la i), pintaremos una fila de ancho columnas (bucle interno, el de la
j), pintando una X por cada una de esas columnas. Cuando tengamos una fila completa
(al salir del bucle de la j), pintaremos un salto de línea y volveremos a ejecutar el bucle
de la j, tras incrementar la i, para pintar la siguiente línea.
Si te has fijado, en vez de ir pintando directamente en el [Link], hemos creado una
variable de tipo String, llamada s, a la que vamos concatenando las X y los saltos de línea.
Sí, eso raro de la barra al revés (la situada a la izquierda del 1, Alt + cerito) seguido de
una n es un salto de línea (new line). Ahora veremos algunos caracteres especiales más,
con la barrita, pero por ahora con \n nos vale.
¿Lo probamos?
> javac Ejemplo04_07.java
> java Ejemplo04_07 10 3
E4.7 XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
104 Estructuras iterativas
Figura 4.1. Ubicación de la contrabarra en el teclado español.
Caracteres especiales
Hasta el momento hemos mencionado o visto accidentalmente cómo poner caracteres
especiales en algunos de los ejemplos, pero centrémonos en ellos.
Estamos hablando de caracteres especiales que queramos poder incluir en un String, por
ejemplo, para escribir por pantalla. El mecanismo que usa Java para ellos es añadirles la
contrabarra (la que está en la tecla del º, al lado del 1, no te confundas con la del 7), que
se aprecia en la figura 4.1. Podríamos considerar dos tipos de caracteres especiales: los
que imprimen caracteres no visibles y los que necesitamos «escapar» porque si no Java
los confundiría con signos que usa para sus cosas.
Los siguientes son los más comunes:
• Salto de línea: \n (de new line). Este ya lo conocemos, lo usamos para meter saltos
de línea en nuestras salidas al usuario.
• Retorno de carro: \r (de return). Esta lindeza solo la encontraremos en el sistema
operativo Mac, versiones antiguas y Windows, donde los saltos de línea se representan
con dos caracteres: \r y \n. Cuando nosotros queramos escribir un salto de línea, \n
nos vale. Pero sí es verdad que al leerlos nos podemos encontrar con algún \r.
• Tabulador: \t. Para meter tabulaciones en nuestros textos. Un simple tabulador bien
colocado puede ayudarnos a que nuestra interfaz de usuario sea mucho más bonita
(¡o mucho más fea si no está tan bien colocado!).
• Comillas: \". Cuando queríamos escribir un texto literal en Java, lo poníamos entre
comillas, ¿no? ¿Has intentado meter comillas dentro de alguno de los textos? Si las
pones tal cual e intentas compilar, verás que Java interpreta que estás cerrando ahí
tu literal, y me temo que eso no es lo que querías. Si tienes que poner comillas dentro
de un fragmento ya metido entre comillas, tendrás que «escaparlas».
• Apóstrofo: \'. Lo mismo sucede con las comillas simples o apóstrofo. En Java se usa,
por ejemplo, para los literales de caracteres. Si queremos representar el carácter
apóstrofo, tendremos que poner '\'' (apóstrofo, contrabarra, apóstrofo, apóstrofo).
Caracteres especiales 105
• Contrabarra: \\. Cuando necesitemos escribir una contrabarra en nuestro texto (por
ejemplo, en una ruta windows), es preciso duplicar las contrabarras. Si no, Java
intentaría interpretar la barra y la letra siguiente como un carácter especial.
Hay que practicarlo que, si no, esto solo explicado es un lío. Escribamos en Java el
texto siguiente:
Debe copiar el texto "I'm happy" dentro del fichero:
C:\app\nube\recursos\[Link]
Inténtalo y, si no lo logras, ¡ya te ayudo!
Vemos cómo lo hacemos:
public class Ejemplo04_08 {
public static void main(String[] args) {
[Link](
"Debe copiar el texto \"I'm happy\" dentro del fichero:\n" +
"\tC:\\app\\nube\\recursos\\[Link]");
}
}
Hemos tenido que escapar las dobles comillas alrededor de I'm happy, pero no el
apóstrofo, porque nuestro texto va entre comillas dobles, no entre comillas simples.
También hemos puesto un salto de línea al final de la primera línea y un tabulador al
principio de la segunda, para que nos quedara como en el ejemplo, y finalmente hemos
duplicado cada una de las contrabarras de la ruta.
Resultado: ¡el esperado!
> javac Ejemplo04_08.java
> java Ejemplo04_08
Debe copiar el texto "I'm happy" dentro del fichero:
C:\app\nube\recursos\[Link]
¿Qué hubiera pasado si no escapamos las contrabarras de la ruta? ¡Probémoslo!
> javac Ejemplo04_08.java
[Link]: error: illegal escape character
"\tC:\app\nube\recursos\[Link]");
^
1 error
Parece que tenemos un error de compilación. Nos dice que no reconoce \a como un
carácter escapado. Claro, es que no existe, por eso no lo mencionamos en la lista. Se lo
añadimos ahí y volvemos a probar:
> javac Ejemplo04_08.java
> java Ejemplo04_08
Debe copiar el texto "I'm happy" dentro del fichero:
C:\app
ecursos [Link]
Ahora sí compila, pero queda un poco raro, ¿no te parece? Ha considerado el \n de la
carpeta nube, \r de la de recursos y \t del fichero textos como salto de línea, retorno de
carro y tabulador. Así que casi es más peligroso olvidarse de escapar la contrabarra
T4.6 cuando detrás vienen esas letras que cuando toca una que no reconoce. Es mejor que no
compile y tengamos que arreglarlo, a que funcione, pero haga cosas raras.
106 Estructuras iterativas
Test y ejercicios
Test 4.1. ¿Cuándo usaremos un for each?
a) Nunca, es una mala práctica.
b) Cuando necesitemos recorrer todos los elementos que cumplan una condición (por ejemplo, las
posiciones pares).
c) Siempre, sirve para todo.
d) Cuando necesitemos recorrer todos los elementos de una colección.
Test 4.2. ¿Cuántas veces se ejecutará este for?
for (int i = 0; i > 3; i++)
a) 3
b) 4
c) ninguna
d) 2
Test 4.3. ¿Es un for o un for each?
1) for (Integer i : numerous)
2) for (int i = 0; i < j; i--)
a) 1) es for, 2) es for each
b) 1) es for, 2) es for
c) 1) es for each, 2) es for each
d) 1) es for each, 2) es for
Test 4.4. ¿Son equivalentes estos dos fragmentos de código?
for (int i = 0; i < NUM; i ++) {
[Link](i);
}
int i = 0;
while (i++ < NUM) {
[Link](i);
}
a) No
b) Sí
Test 4.5. ¿Qué resultado dará este do-while?
int i = 0;
do {
[Link](i++);
} while ( i > 3);
a) 0
b) 012
c) 123
d) nada
Test 4.6. ¿Qué imprimirá esa línea?
[Link]("\"\\\"" + 3 + "+" + 2 + "=" + 3 + 2);
a) \\ 3 + 2 = 5
b) "\"3+2=32
c) "\"3+2=5
d) \" 3+2=32
Test y ejercicios 107
Ejercicio 4.1. Imprimir todos los argumentos en mayúsculas o minúsculas
Imprime por pantalla todos los argumentos que recibas. Si el argumento tiene menos de cinco
caracteres, escríbelo en mayúsculas. Si no, en minúsculas.
NOTA:
La clase String tiene métodos para convertir a mayúsculas (uppercase, en inglés) o minúsculas
(lowercase, en inglés). Puedes buscarlos en [Link]
[Link] (o preguntarle a tu buscador favorito: api java string).
Ejercicio 4.2. Imprimir todos los argumentos mostrando su posición
Lista todos los argumentos que recibas, pero mostrando su posición:
java Ejercicio04_02 hola cómo estás
0) hola
1) cómo
2) estás
Como siempre que nos dan ejemplos en el enunciado, observa con detenimiento lo que nos piden.
Hay que numerar cada palabra. Bien, ¿en qué valor empezamos? En cero, como Java, bien, más
fácil. ¿Y con qué formato? El número seguido de un cierre de paréntesis y luego un espacio.
En la programación profesional, en la que tenemos que satisfacer los requisitos de un cliente, es
importante fijarse en todos estos detalles, para hacer exactamente lo que nos están pidiendo.
Ahora que ya está claro… ¡a teclear!
Ejercicio 4.3. Listar los argumentos cumpliendo ciertas reglas
Vamos a entrenar un poco más con los bucles, que son muy importantes.
Lista todos los argumentos que recibas, pero cumpliendo las siguientes reglas: si la palabra es corta (cinco
o menos caracteres), la imprimes cuatro veces en la misma línea; si es larga, la repites solo dos veces.
> java Ejercicio04_03 hola mis queridos amigos ¿cómo estáis?
hola hola hola hola
mis mis mis mis
queridos queridos
amigos amigos
¿cómo ¿cómo ¿cómo ¿cómo
estáis? estáis?
No sé si vas notando que se van complicando un poco las cosas… ¿Te atreves?
Ejercicio 4.4. Traducción de for a while
¡Tengo un reto para ti! Traduce este código de for a while.
public class Ejercicio04_04 {
public static void main(String[] args) {
if ([Link] == 1) {
int res = 1;
for(int num = [Link](args[0]); num > 0; num --) {
res *= num;
}
[Link]("Resultado: " + res);
} else {
[Link]("Necesito un argumento, ni más ni menos");
}
}
}
108 Estructuras iterativas
Ya que estamos, ¿qué hacemos en este código?
NOTA:
Antes déjame explicarte el *=. Cuando en Java queremos incrementar una variable a en 1, hacemos
a++. Si queremos incrementarla en 2, podemos hacer a += 2, que sería lo mismo que a = a + 2. Pues lo
mismo nos vale con el resto de operaciones. Así que res *= num es lo mismo que hacer res = res * num.
Ejercicio 4.5. Encontrar palabras largas entre los argumentos
Conociendo tres tipos de bucles (for-each, for y while), con este ejercicio practicaremos el while.
Indica la posición del primer argumento recibido que sea una palabra demasiado larga (más de
10 caracteres): "La Xª palabra es demasiado larga." (empezando por la primera) o "Todas las
palabras son correctas.", si no hay ninguna que sobrepase dicha longitud.
> javac Ejercicio04_05.java
> java Ejercicio04_05 aeiou abcdefghij abcdefghijklmn aaa
La 3ª palabra es demasiado larga.
> java Ejercicio04_05 aeiou abcdefghij
Todas las palabras son correctas.
¡Ya sabes que tienes que observar bien los detalles antes de empezar!
Ejercicio 4.6. Conseguir la respuesta deseada del usuario
Este ejercicio es un dos en uno, vamos a practicar el do-while y el escáner.
Pídele al usuario que escriba «abracadabra» e insiste hasta que lo haga bien.
> java Ejercicio04_06
¡Hola!, por favor, escribe 'abracadabra':
sfewe
¡Hola!, por favor, escribe 'abracadabra':
fwew
¡Hola!, por favor, escribe 'abracadabra':
abradacabrea
¡Hola!, por favor, escribe 'abracadabra':
abradacabra
¡Hola!, por favor, escribe 'abracadabra':
abracadabra
TU: abracadabra
YO: ¡pata de cabra!
Observa bien en el ejemplo, las partes en negrita son las respuestas del usuario, que podría
escribirnos cualquier cosa.
Ejercicio 4.7. Pintar los números de cero a noventa y nueve en una matriz de 0 a 99
Pintar los números de cero a noventa y nueve en una matriz de 10 x 10 en Java.
El resultado esperado es el siguiente:
> java Ejercicio04_07
00 01 02 03 04 05 06 07 08 09
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99
Test y ejercicios 109
Observa que tenemos dos cifras por cada número, que van separados por un espacio, y que cada
10 saltamos de línea.
Se podría resolver de muchas formas, pero me gustaría que lo intentaras usando dos bucles
anidados, ya sabes, for i y for j.
Soluciones
Test 4.1. ¿Cuándo usaremos un for each?
d) Cuando necesitemos recorrer todos los elementos de una colección.
Test 4.2. ¿Cuántas veces se ejecutará este for?
for (int i = 0; i > 3; i++)
c) ninguna: efectivamente, i no es mayor que 3.
Test 4.3. ¿Es un for o un for each?
1) for (Integer i : numerous)
2) for (int i = 0; i < j; i--)
d) 1) es for each, 2) es for: Exacto, el 1) lleva los dos puntos y los elementos de una lista. El
2) lleva inicialización, punto y coma, condición, punto y coma y avance.
Test 4.4. ¿Son equivalentes estos dos fragmentos de código?
for (int i = 0; i < NUM; i ++) {
[Link](i);
}
int i = 0;
while (i++ < NUM) {
[Link](i);
}
a) No: En el for, la i se incrementará después de pintarla, y en el while, antes.
Test 4.5. ¿Qué resultado dará este do-while?
int i = 0;
do {
[Link](i++);
} while ( i > 3);
a) 0: El do-while siempre se ejecuta al menos una vez, así que, aunque i no sea mayor que
tres, se ejecutará la primera vez mostrando un cero.
Test 4.6. ¿Qué imprimirá esa línea?
[Link]("\"\\\"" + 3 + "+" + 2 + "=" + 3 + 2);
b) "\"3+2=32: ¡Correcto! No hemos puesto espacios dentro de las comillas y, para conseguir
el 5, habríamos necesitado poner 3 + 2 entre paréntesis. El lío de comillas y contrabarras
del principio es una comilla escapada, una contrabarra escapada y una comilla escapada,
todo ello entrecomillado.
110 Estructuras iterativas
Ejercicio 4.1. Imprimir todos los argumentos en mayúsculas o minúsculas
• Empezamos por lo primero, creando la clase y el método main:
public class Ejercicio04_01 {
public static void main(String[] args) {
}
}
• Hemos dicho que teníamos que hacerles algo a todos los argumentos recibidos, pues
como hemos visto en el ejemplo:
public class Ejercicio04_01 {
public static void main(String[] args) {
for (String s : args) {
}
}
}
• ¿Qué teníamos que hacer? Eso dependía de la longitud de cada argumento:
public class Ejercicio04_01 {
private static final int LIM = 5;
public static void main(String[] args) {
for (String s : args) {
if ([Link]() < LIM) {
} else {
}
}
}
}
¡No te olvides de las constantes!
• Bien, en función de la longitud, escribíamos el argumento en mayúsculas o en minúsculas:
public class Ejercicio04_01 {
private static final int LIM = 5;
public static void main(String[] args) {
for (String s : args) {
if ([Link]() < LIM) {
[Link]([Link]());
} else {
[Link]([Link]());
}
}
}
}
¡Listo! ¡Ya lo puedes probar!
Realmente, la parte del for each es idéntica a la vista en el ejemplo. Quizá el mayor reto aquí
era no asustarse por tener que meter un if dentro del bucle y encontrar los métodos que pasan
a mayúsculas y minúsculas.
Observa que, aunque hayamos hecho cosas distintas según la longitud de cada palabra recibida,
hemos tratado todas las palabras y hemos recorrido la lista entera. Por eso, podemos y debemos
usar un for each en este ejemplo.
Soluciones 111
Ejercicio 4.2. Imprimir todos los argumentos mostrando su posición
• Venga, que esta parte ya te la sabes, clase y main:
public class Ejercicio04_02 {
public static void main(String[] args) {
}
}
• Y ahora ya pensamos… Nos han pedido iterar todos los argumentos usando un for, bien,
iteraremos sobre su posición i, desde 0 hasta el tamaño de la secuencia menos uno e
incrementando de uno en uno:
public class Ejercicio04_02 {
public static void main(String[] args) {
for (int i = 0; i < [Link]; i ++) {
}
}
}
• Y para cada posición, recuperamos su elemento y lo imprimimos, con su posición y su paréntesis:
public class Ejercicio04_02 {
public static void main(String[] args) {
for (int i = 0; i < [Link]; i ++) {
String s = args[i];
[Link](i + ") " + s);
}
}
}
El código es casi idéntico al del ejemplo 4.3, simplemente en el [Link] tenemos que
añadir información sobre por qué línea vamos. Esto que con el for clásico es superfácil,
con el for each hubiera supuesto un pequeño reto… ¿De dónde sacas el contador de líneas?
¿Quieres intentarlo?
• Usando un for each, habríamos hecho esto:
public class Ejercicio04_02ForEach {
public static void main(String[] args) {
int i = 0;
for (String s : args) {
[Link](i + ") " + s);
i ++;
}
}
}
Cuando hagas un for each y necesites llevar una i como en este caso… plantéate si de
verdad el for each es la mejor elección o si no sería mejor usar un for clásico u otro tipo
de bucle.
Ejercicio 4.3. Listar los argumentos cumpliendo ciertas reglas
• Preparamos nuestra clase y su main:
public class Ejercicio04_03 {
public static void main(String[] args) {
}
}
• Y empezamos a pensar. ¿Necesito recorrer todos los argumentos? ¿Necesito llevar un contador
de por cuál voy? Sí, tengo que recorrerlos todos, pero no, no hay que contarlos. Por tanto,
utilizaré un for each para esto:
112 Estructuras iterativas
public class Ejercicio04_03 {
public static void main(String[] args) {
for (String s : args) {
}
}
}
• Ahora… ¿Qué tengo que hacer con cada elemento? Depende de su longitud, pues como antes:
public class Ejercicio04_03 {
private static final int LIM = 5;
public static void main(String[] args) {
for (String s : args) {
if ([Link]() <= LIM) {
} else {
}
}
}
}
• En el caso de que sea corta, ¿qué hacemos? Pintar la palabra cuatro veces, en la misma línea.
public class Ejercicio04_03 {
private static final int LIM = 5;
private static final int REP_CORTA = 4;
public static void main(String[] args) {
for (String s : args) {
if ([Link]() <= LIM) {
for (int i = 0; i < REP_CORTA; i++) {
[Link](s + " ");
}
// no te olvides del salto de línea
[Link]();
} else {
}
}
}
}
• ¿Y si era larga? Pintarla solo dos veces:
public class Ejercicio04_03 {
private static final int LIM = 5;
private static final int REP_CORTA = 4;
private static final int REP_LARGA = 2;
public static void main(String[] args) {
for (String s : args) {
if ([Link]() <= LIM) {
for (int i = 0; i < REP_CORTA; i++) {
[Link](s + " ");
}
// no te olvides del salto de línea
[Link]();
} else {
for (int i = 0; i < REP_LARGA; i++) {
[Link](s + " ");
}
// no te olvides del salto de línea
[Link]();
}
}
}
}
Soluciones 113
¿Lo pruebas?
Resumiendo, en esta solución he combinado un for each, un if-else y un par de for clásicos.
La verdad es que no estoy demasiado orgullosa de este código, porque, no sé si lo has notado,
pero se repite un poco… Y que el código se repita es señal de que algo no está muy bien hecho.
Pero veamos primero qué he hecho y luego ya discutimos cómo mejorarlo.
Empiezo declarando constantes para cada uno de los «números mágicos» que aparecen en el
enunciado. Luego ya dentro del main, hago un for each para recorrer todos los argumentos.
Dentro del for each, con un if decido si estamos en el caso de palabra corta o larga. Si es corta,
hago un bucle for que se repita REP_CORTA veces, es decir, 4. Si es larga, hago un bucle for
que se repita REP_LARGA veces, 2. En ambos casos dentro del bucle imprimo la palabra con
un [Link]() para que no me meta salto de línea, pero le pongo a mano un espacio.
Al salir del bucle hago un [Link]() sin argumentos para imprimir esa línea en
blanco que me va a separar una palabra de otra.
Ahora que ya hemos entendido este código, veamos por qué no me gusta demasiado… El
tratamiento que tenemos que hacer de las palabras largas y las cortas se parece mucho, ¿no?
¿Cuál es la diferencia? Solo el número de veces que repetimos la palabra… ¿No te sugiere eso
que deberíamos haber creado un método que recibiera, entre otros, esa cifra como parámetro?
Así en el if simplemente tendríamos que asignarle a una variable un valor u otro, y luego ya
fuera del if podríamos llamar a nuestro método que imprimiera las palabras las veces que
hiciera falta. Te lo dejo como reto adicional. Inténtalo.
Otra cosa que no me convence es que al final de línea también hay ese espacio en blanco. Eso
es fácil de resolver también (puedo controlar con un if si estoy en la última iteración y entonces
no meter el espacio), pero la verdad es que no he querido complicar más el código de la
solución.
Salvedades aparte, reflexionemos sobre los tipos de bucles usados. Para recorrer todos los
elementos de la lista de argumentos he usado un for each. No necesito llevar la cuenta de por
cuál voy, porque recorreré la lista entera… un caso «de libro» para el for each. Sin embargo,
ya dentro de los if, he usado un for clásico. En esta ocasión quería repetir la ejecución un
número determinado de veces, un buen ejemplo para el for clásico.
Ejercicio 4.4. Traducción de for a while
El resultado de la traducción podría ser este:
public class Ejercicio04_04Sol {
public static void main(String[] args) {
if ([Link] == 1) {
int num = [Link](args[0]);
int res = 1;
while (num > 0) {
res *= num;
num --;
}
[Link]("Resultado: " + res);
} else {
[Link]("Necesito un argumento, ni más ni menos");
}
}
}
114 Estructuras iterativas
Como habíamos comentado, necesitamos inicializar num fuera del bucle, dentro de los paréntesis
dejamos la condición, y dentro del bucle, al final, ponemos la condición de avance. Fíjate en que
esta vez estamos yendo de atrás hacia adelante (num --), pues vale, ¡no pasa nada! ¡No es obligatorio
avanzar siempre con el ++!
La segunda pregunta planteada es ¿qué está haciendo este código? Veamos qué hace si le pasamos
un 3. Tenemos un argumento, así que entramos en el if, lo convertimos a número: mientras sea
mayor que cero, ¿lo es?, sí, pues entonces multiplicamos res, que lo teníamos inicializado a 1, valor
neutro del producto, por num, con lo que tendríamos res = 1 * 3 = 3. Siguiente paso: decrementamos
num, que valdrá 2. ¿Sigue siendo mayor que cero?, pues multiplicamos res = 3 * 2 = 6 y
decrementamos, num valdrá 1. Y como sigue siendo mayor que cero, multiplicamos 6 * 1 = 6 y
decrementamos. Ahora num valdrá 0. Ya no es mayor que cero, así que imprimimos el resultado.
Lo que hemos hecho recibiendo un tres es 3*2*1. Si hubiéramos recibido un cinco: 5*4*3*2*1. ¿Te
suena? ¡Factorial! Estamos calculando el factorial del número recibido. Para tres, 3!.
¡Reto conseguido!
Ejercicio 4.5. Encontrar palabras largas entre los argumentos
• El esqueleto de siempre… ¡Seguro que ya sabes escribir de memoria un main, con todas sus
partes! Si no te las sabes, deja de usar el Crtl + Espacio y escríbelo unas cuantas veces a mano.
public class Ejercicio04_05 {
public static void main(String[] args) {
}
}
• Como solo queremos saber cuál es la posición de la primera palabra larga que hallemos, no
tiene mucho sentido comprobarlas todas, sino que podemos parar en cuanto demos con la
primera. Por eso, utilizaremos un while y una variable booleana llamada encontrada que será
cierta solo cuando una palabra tenga una longitud mayor que el límite marcado (en una
constante, ¡claro!).
public class Ejercicio04_05 {
public static void main(String[] args) {
int i = 0;
boolean encontrada = false;
while () {
i ++;
}
}
}
• En el bucle while pondremos dos condiciones, que deben ser ambas ciertas, para seguir iterando:
que no se nos haya acabado la lista (i < [Link]) y (&&) que no hayamos encontrado aún
ninguna palabra larga (!encontrada).
public class Ejercicio04_05 {
private static final int LIM = 10;
public static void main(String[] args) {
int i = 0;
boolean encontrada = false;
while (i < [Link] && !encontrada) {
encontrada = args[i].length() > LIM;
i ++;
}
}
}
Soluciones 115
• En cuanto salgamos del bucle, en función de si hemos encontrado o no una palabra, sacaremos
un mensaje u otro. En caso de hallarla, usaremos el contador i para indicar en qué posición la
encontramos. Estamos incrementando la i después de comprobar la longitud de la palabra,
así que cuando salgamos del bucle, si la hemos localizado, en la i tendremos el valor de la
posición empezando a contar desde uno (como los humanos) en vez de desde cero (como los
ordenadores), así que se mostrará directamente el valor de i.
public class Ejercicio04_05 {
private static final int LIM = 10;
public static void main(String[] args) {
int i = 0;
boolean encontrada = false;
while (i < [Link] && !encontrada) {
encontrada = args[i].length() > LIM;
i ++;
}
if (encontrada) {
[Link]("La " + i +
"ª palabra es demasiado larga.");
} else {
[Link]("Todas las palabras son correctas.");
}
}
}
Si eres una persona observadora, te estarás preguntando el porqué de la diferencia entre estos
dos fragmentos de código:
i < [Link]
args[i].length() > LIM
Los dos sirven para calcular la longitud, pero uno lleva paréntesis y el otro no. Y si juegas a
poner o quitar paréntesis, si no lo haces así, no compilará. ¿Pero por qué? Lo primero en lo
que debemos fijarnos es en sobre qué elemento estamos intentando calcular la longitud.
En el caso a) es sobre args y en el caso b) sobre args[i]. La diferencia es que args es un array (de
String), mientras que args[i] es String (de esos de los del array).
En Java, los arrays tienen un atributo length que nos indica su longitud, que es fija. Como es
un atributo, no hay que poner paréntesis para acceder a su valor. Sin embargo, los String son
objetos con un método length() para calcular su longitud, por eso necesitamos los paréntesis.
¿Misterio resuelto?
116 Estructuras iterativas
Ejercicio 4.6. Conseguir la respuesta deseada del usuario
• Preparamos la base:
public class Ejercicio04_06 {
public static void main(String[] args) {
}
}
• Para solucionar esto, hay que usar el Scanner para comunicarnos con el usuario y pedirle que
nos escriba una palabra. Y el bucle do-while para insistir hasta que lo haga bien. En este caso
nos interesa el bucle do-while porque siempre se lo tenemos que pedir, al menos la primera
vez, recoger lo que nos diga y hacer lo que queramos con ello (en este caso, reproducir ese
fragmento de diálogo).
A menudo, al practicar con bucles, siempre recorremos los elementos de una lista o hacemos
algo un número determinado de veces. Sin embargo, en este ejercicio, no tenemos ni idea de
cuántas veces vamos a ejecutarlo… Hasta que el usuario responda bien… puede lograrlo a la
primera o necesitar un millón de intentos. Da igual. Con el while y el do-while podemos insistir
hasta el infinito sin cansarnos.
import [Link];
public class Ejercicio04_06 {
private static final String CLAVE = "abracadabra";
public static void main(String[] args) {
Scanner scanner = new Scanner([Link]);
String palabra;
do {
[Link]("¡Hola!, por favor, escribe '" +
CLAVE + "':");
palabra = [Link]();
} while ();
[Link]("TU: " + palabra);
[Link]("YO: pata de cabra!");
}
}
Por eso, es importante controlar que la condición se actualiza bien en cada iteración porque,
si no, podríamos quedarnos en un bucle infinito. Cuando contamos posiciones, tenemos que
asegurarnos de actualizar el índice. En casos como este, solo hay que utilizar la nueva respuesta
del usuario en cada comprobación.
Soluciones 117
Ejercicio 4.7. Pintar los números de cero a noventa y nueve en una matriz de
10 x 10
• Preparamos la base:
public class Ejercicio04_07 {
public static void main(String[] args) {
}
}
• Y la constante del tamaño de la matriz, TAM = 10.
public class Ejercicio04_07 {
private static final int TAM = 10;
public static void main(String[] args) {
}
}
• Y ya con la base preparada, para evitar el síndrome del papel en blanco, veremos cómo hacer
para pintar esto. Nos han pedido usar dos bucles anidados para pintar una matriz cuadrada.
Es posible utilizar uno para las filas y otro para las columnas. Como tenemos el mismo número
de filas que de columnas, ambos bucles tendrán el mismo límite.
public class Ejercicio04_07 {
private static final int TAM = 10;
public static void main(String[] args) {
for (int i = 0; i < TAM; i ++) {
for (int j = 0; j < TAM; j++) {
}
}
}
}
• ¿Ahora cómo pintamos los números? La i de las filas va de 0 a 9, la j de las columnas va de 0
a 9 por cada fila. Si juntamos la i y la j, ya tenemos los números de dos cifras del ejemplo del
enunciado.
public class Ejercicio04_07 {
private static final int TAM = 10;
public static void main(String[] args) {
for (int i = 0; i < TAM; i ++) {
for (int j = 0; j < TAM; j++) {
[Link](i + j + " ");
}
[Link]();
}
}
}
Así pues, pintamos por cada «casilla» la i y la j y el espacio de separación y cuando terminemos
con una fila, metemos un salto de línea y ¡a por la siguiente!
118 Estructuras iterativas
¿Lo probamos?
> javac Ejercicio04_07.java
> java Ejercicio04_07
0 1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10
2 3 4 5 6 7 8 9 10 11
3 4 5 6 7 8 9 10 11 12
4 5 6 7 8 9 10 11 12 13
5 6 7 8 9 10 11 12 13 14
6 7 8 9 10 11 12 13 14 15
7 8 9 10 11 12 13 14 15 16
8 9 10 11 12 13 14 15 16 17
9 10 11 12 13 14 15 16 17 18
¡Ups! Esto no es exactamente lo que buscábamos. ¿Qué ha pasado? Intenta averiguarlo antes
de seguir leyendo.
Al ir a concatenar la i con la j y con el espacio, hemos usado el signo más, que en Java
sirve para unir String, pero también sirve para sumar números, así que lo que ha pasado
es que se han sumado los valores de i y j, y por eso nos sale una matriz tan rara en vez
de la que esperábamos. Tenemos que indicarle a Java que queremos concatenar esos dos
valores, ¡no sumarlos!
public class Ejercicio04_07 {
private static final int TAM = 10;
public static void main(String[] args) {
for (int i = 0; i < TAM; i ++) {
for (int j = 0; j < TAM; j++) {
[Link](i + "" + j + " ");
}
[Link]();
}
}
}
Podemos lograrlo, por ejemplo, concatenando entre la i y la j una cadena vacía (comillas comillas,
sin espacio dentro). De esta forma, Java «sumará» los números con String y eso interpreta que
queremos concatenar y ya conseguimos nuestra matriz, pero ¡vamos a comprobarlo!
> java Ejercicio04_07
00 01 02 03 04 05 06 07 08 09
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99
Sí, ¡esta vez sí!
Soluciones 119
5 Proyecto
«piedra, papel, tijeras»
En este capítulo aprenderás a:
• Desarrollar un programa completo y funcional.
• Aplicar lo aprendido en los capítulos anteriores.
• Interactuar con el usuario.
• Generar respuestas aleatorias.
Introducción
En este capítulo haremos nuestro primer programa real, completo y funcional: un
juego «piedra, papel, tijeras» para jugar un usuario contra el ordenador.
El juego de manos «piedra, papel, tijeras» es muy conocido, pero, por si no sabes
las reglas o sigues algunas distintas, te explico cómo se juega. Es un juego para dos
jugadores. Ambos cantan unos versos que dependen de la versión, pero que pueden
ser algo así como «Un, dos, tres, piedra, papel, tijeras, un, dos, tres, ¡ya!», y justo al
terminar la canción, cada jugador muestra su mano haciendo uno de estos signos:
• piedra: el puño cerrado.
• papel: la mano abierta, con los dedos extendidos.
• tijeras: el dedo índice y el medio extendidos y separados haciendo una «V» y el resto
de dedos cerrados.
Piedra gana a Tijeras
Piedra Tijeras
Papel Piedra
Papel gana a Piedra
Papel
Tijeras gana a Papel Tijeras
Figura 5.1. Signos con las manos del juego «piedra, papel, tijeras».
El objetivo del juego es sacar el arma que gana a la que saque el oponente, teniendo
en cuenta que:
• Piedra gana a Tijeras: la piedra aplasta las tijeras.
• Papel gana a Piedra: el papel envuelve la piedra.
• Tijeras gana a Papel: las tijeras cortan el papel.
122 Proyecto «piedra, papel, tijeras»
Si ambos jugadores sacan el mismo elemento, hay empate, entonces, se suele repetir. Se
puede jugar al mejor de tres o de cinco, pero hay que acordarlo antes.
Aunque Java es un lenguaje orientado a objetos, de momento solo hemos aprendido
programación estructurada, por tanto, será con programación estructurada como
resolveremos este proyecto.
Presentación del programa
Una cosa es saber cómo se juega con otra persona al «piedra, papel, tijeras» y otra es cómo
vamos a jugar contra el ordenador con nuestro programa. El programa tendrá cuatro partes:
• Explicarle al jugador cómo se juega.
• Generar la jugada aleatoria del ordenador.
• Pedir al jugador su jugada mediante una letra (P para piedra, L para papel, T para
tijeras o S para salir terminando la partida).
• Decidir quién ha ganado.
Una cosa es saber cómo se juega al «piedra, papel, tijeras» y otra es saberlo programar,
así que pensemos los pasos para lograrlo.
Los pasos que seguiremos para construir el programa son:
• Preparar del esqueleto y las constantes necesarias.
• Mostrar instrucciones al usuario.
• Generar la jugada del ordenador.
• Recoger la jugada del usuario.
• Interpretar la jugada del usuario.
• Calcular el ganador de la jugada.
• Mostrar el resultado de la jugada.
• Hacer el juego repetitivo.
Preparar el esqueleto y las constantes
necesarias
Cuando tenemos que disponernos a escribir un programa podemos sentirnos un poco
bloqueados sin saber por dónde empezar. Una buena opción es comenzar por aquello
automático que no requiere que pensemos mucho:
• Crear la clase, que llamaremos PiedraPapelTijeras.
• Crear el método main.
Presentación del programa 123
• Listar algunas constantes evidentes. Aquí sí hay que pensar un poquito qué constantes
necesitaremos. Quizá luego surjan más, pero así para empezar, tenemos las letras
que el usuario usará para respondernos.
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
private static final String TIJERAS = "T";
private static final String SALIR = "S";
public static void main(String[] args) {
}
}
Con esto, ya no tenemos un fichero en blanco, así que ya no hay bloqueo que valga.
Guardamos el fichero, compilamos y ejecutamos. El programa aún no hará nada, pero
así nos aseguramos de que todo va bien.
Mostrar instrucciones al usuario
Ahora mostraremos las instrucciones del juego al usuario. Podemos darle un mensaje
de bienvenida seguido de un mensaje pidiéndole la jugada, en el que aclararemos qué
significa cada una de las letras que puede usar.
Para mostrar textos por la salida estándar usaremos [Link]. Pondremos los
textos también en constantes.
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
private static final String TIJERAS = "T";
private static final String SALIR = "S";
// Mensajes al usuario
private static final String BIENVENIDA =
"Bienvenido al juego Piedra-papel-tijeras!";
private static final String PEDIR_JUGADA = "¿Cuál es tu jugada? "
+ PIEDRA + " (piedra), " + PAPEL + " (papel), "
+ TIJERAS + " (tijeras) o " + SALIR + " (salir)";
public static void main(String[] args) {
// Instrucciones
[Link](BIENVENIDA);
[Link](PEDIR_JUGADA);
}
}
NOTA:
El mensaje de pedir jugada podría haber sido directamente «¿Cuál es tu jugada? P (piedra), L
(papel), T (tijeras) o S (salir)», pero como hemos sacado las letras a otras constantes, mejor las
usamos, y así si en un futuro queremos modificarlas, será suficiente con cambiar las constantes
de cada letra.
124 Proyecto «piedra, papel, tijeras»
De nuevo, guardamos el fichero, compilamos y ejecutamos. Esta vez ya hará algo:
mostrarnos los textos. Paso a paso.
Generar jugada del ordenador
Las jugadas del ordenador, para darle cierta gracia al juego, deben ser aleatorias. Hay
varias formas de hacerlo en Java, pero te voy a enseñar la más sencilla de entender a
estas alturas del libro.
[Link](), método random de la clase Math, devuelve un número decimal aleatorio
mayor o igual que cero y menor que uno. Si cogemos ese resultado y lo multiplicamos
por el número de opciones distintas que necesitamos, ya dispondremos del número
aleatorio deseado. Por ejemplo,
int num = (int)([Link]() * 10);
generará un número aleatorio entre cero y nueve.
En nuestro caso, queremos que nos genere uno de los tres valores en juego (piedra, papel
o tijeras). Haremos lo siguiente:
• Crear un array con esos tres elementos (aprovechando las tres constantes que ya
tenemos).
private static final String[] JUEGO = {PIEDRA, PAPEL, TIJERAS};
• Obtener una posición aleatoria de ese array.
int eleccionPC = (int)([Link]() * [Link]);
• Solo a efectos de comprobar qué estamos haciendo, imprimiremos un mensaje con
la elección del ordenador. Lo quitaremos antes de terminar el programa. En vez de
pintar directamente el número obtenido, pintaremos la letra que esté en esa posición
del array con todas las jugadas.
[Link]("*** " + JUEGO[eleccionPC]);
TRUCO:
Ponemos los tres asteriscos para acordarnos, al final, de quitar esa línea.
El código completo quedaría así:
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
private static final String TIJERAS = "T";
private static final String SALIR = "S";
private static final String[] JUEGO = {PIEDRA, PAPEL, TIJERAS};
Generar jugada del ordenador 125
// Mensajes al usuario
private static final String BIENVENIDA =
"¡Bienvenido al juego Piedra-papel-tijeras!";
private static final String PEDIR_JUGADA = "¿Cuál es tu jugada? "
+ PIEDRA + " (piedra), " + PAPEL + " (papel), "
+ TIJERAS + " (tijeras) o " + SALIR + " (salir)";
public static void main(String[] args) {
// Instrucciones
[Link](BIENVENIDA);
[Link](PEDIR_JUGADA);
int eleccionPC = (int)([Link]() * [Link]);
[Link]("*** " + JUEGO[eleccionPC]);
}
}
Llegó el momento de guardar, compilar y probar:
> javac [Link]
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** L
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** P
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** T
¡Mira qué cosas!, cada vez que lo he ejecutado me ha salido una opción distinta.
Recoger jugada del usuario
Para recoger la jugada del usuario emplearemos la clase Scanner que aprendimos en el
capítulo anterior. Los pasos serán:
• Inicializar Scanner, requiere importar la clase.
Scanner s = new Scanner([Link]);
• Leer la respuesta del usuario, que será un String.
String sEleccionUsuario = [Link]();
• Imprimirla, temporalmente, para comprobaciones durante el desarrollo.
[Link]("*** " + sEleccionUsuario);
• Cerrar lo que abrimos, es decir, el Scanner.
Todo esto hay que colocarlo bien en el código que, completo, quedaría así:
import [Link];
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
126 Proyecto «piedra, papel, tijeras»
private static final String TIJERAS = "T";
private static final String SALIR = "S";
private static final String[] JUEGO = {PIEDRA, PAPEL, TIJERAS};
// Mensajes al usuario
private static final String BIENVENIDA =
"¡Bienvenido al juego Piedra-papel-tijeras!";
private static final String PEDIR_JUGADA = "¿Cuál es tu jugada? "
+ PIEDRA + " (piedra), " + PAPEL + " (papel), "
+ TIJERAS + " (tijeras) o " + SALIR + " (salir)";
public static void main(String[] args) {
// abrimos un scanner para leer la entrada del usuario
Scanner s = new Scanner([Link]);
// Instrucciones
[Link](BIENVENIDA);
[Link](PEDIR_JUGADA);
// Jugada del ordenador
int eleccionPC = (int)([Link]() * [Link]);
[Link]("*** " + JUEGO[eleccionPC]);
// Jugada del usuario
String sEleccionUsuario = [Link]();
[Link]("*** " + sEleccionUsuario);
// cerramos lo que abrimos
[Link]();
}
}
Guardamos, compilamos y probamos:
> javac [Link]
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** T
P
*** P
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** T
X
*** X
Aún no estamos controlando qué responde el usuario, así que, de momento, el programa
acepta tanto respuestas válidas (P) como inválidas (X).
Interpretar jugada del usuario
La jugada del ordenador es un número. La jugada del usuario es una letra. Así es difícil
de comparar y saber quién gana. Aprovechemos que debemos interpretar la jugada del
usuario para traducirla a un número. Creemos un método para hacer esto y evitar recargar
demasiado el código del método main e ir familiarizándonos con las buenas prácticas
de programación.
Interpretar jugada del usuario 127
Tenemos un array con las jugadas posibles. Podemos buscar en qué posición está la letra
introducida por el usuario y usar dicha posición como valor de la jugada del usuario. Es
coherente con lo que hemos hecho para el ordenador. Si no encontramos el valor en la
lista, devolvemos un código de error. Más adelante, en los próximos capítulos aprenderás
a manejar mejor esto.
private static int convertir(String sEleccionUsuario) {
for (int i = 0; i < [Link]; i++) {
if (JUEGO[i].equalsIgnoreCase(sEleccionUsuario)) {
return i;
}
}
return ERROR_NO_ENCONTRADA; // TODO tratar esto correctamente
}
NOTA:
Usamos equalsIgnoreCase en vez de equals porque nos permite aceptar respuestas en
mayúscula o minúscula. Con este pequeño detalle nuestro programa será mucho más amigable
para el usuario.
Ahora que ya tenemos un método, mejor lo usamos:
int eleccionUsuario = convertir(sEleccionUsuario);
[Link]("*** " + eleccionUsuario);
if (eleccionUsuario == ERROR_NO_ENCONTRADA) {
[Link](MSJ_ERROR_NO_ENCONTRADA);
}
El código entero, también con las constantes, sería:
import [Link];
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
private static final String TIJERAS = "T";
private static final String SALIR = "S";
private static final String[] JUEGO = {PIEDRA, PAPEL, TIJERAS};
private static final int ERROR_NO_ENCONTRADA = -1;
// Mensajes al usuario
private static final String BIENVENIDA =
"¡Bienvenido al juego Piedra-papel-tijeras!";
private static final String PEDIR_JUGADA = "¿Cuál es tu jugada? "
+ PIEDRA + " (piedra), " + PAPEL + " (papel), "
+ TIJERAS + " (tijeras) o " + SALIR + " (salir)";
private static final String MSJ_ERROR_NO_ENCONTRADA =
"No entiendo tu jugada";
public static void main(String[] args) {
// abrimos un scanner para leer la entrada del usuario
Scanner s = new Scanner([Link]);
128 Proyecto «piedra, papel, tijeras»
// Instrucciones
[Link](BIENVENIDA);
[Link](PEDIR_JUGADA);
// Jugada del ordenador
int eleccionPC = (int)([Link]() * [Link]);
[Link]("*** " + JUEGO[eleccionPC]);
// Jugada del usuario
String sEleccionUsuario = [Link]();
[Link]("*** " + sEleccionUsuario);
// Interpretación de la jugada del usuario
int eleccionUsuario = convertir(sEleccionUsuario);
[Link]("*** " + eleccionUsuario);
if (eleccionUsuario == ERROR_NO_ENCONTRADA) {
[Link](MSJ_ERROR_NO_ENCONTRADA);
}
// cerramos lo que abrimos
[Link]();
}
private static int convertir(String sEleccionUsuario) {
for (int i = 0; i < [Link]; i++) {
if (JUEGO[i].equalsIgnoreCase(sEleccionUsuario)) {
return i;
}
}
return ERROR_NO_ENCONTRADA; // TODO tratar esto correctamente
}
}
Y el resultado de guardar, compilar y probar:
> javac [Link]
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** P
P
*** P
*** 0
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** P
T
*** T
*** 2
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** P
X
*** X
No entiendo tu jugada
Con estas pruebas vemos que, según el valor introducido por el usuario, devuelve una
posición u otra del array, o da un mensaje de error.
Interpretar jugada del usuario 129
Calcular el ganador de la jugada
Para calcular el ganador de la jugada, necesitamos definir un algoritmo. Podríamos poner
muchos if para ir comparando todas las opciones, pero te propongo otro, sacándole
provecho al array JUEGO.
• Calculamos la diferencia entre ambas jugadas, restándole a la elección del usuario
la elección del ordenador.
• Si son iguales, si la diferencia es cero, empate.
• Si el resultado es negativo, le sumamos el tamaño del array, para rotar los valores.
• Si el resultado es 1, gana el usuario. Si es 2, gana el ordenador.
Quizá con unas tablas lo veas mejor:
Tabla 5.1. Resultado de la partida desde el punto de vista del usuario.
usuario \ ordenador piedra papel tijeras
piedra empate pierde gana
papel gana empate pierde
tijeras pierde gana empate
Tabla 5.2. Diferencia entre el valor de la jugada del usuario y la jugada del ordenador.
usuario \ ordenador 0 (piedra) 1 (papel) 2 (tijeras)
0 (piedra) 0 -1 -2
1 (papel) 1 0 -1
2 (tijeras) 2 1 0
Tabla 5.3. Diferencia entre el valor de la jugada del usuario y la jugada del ordenador,
tras sumar el tamaño de la tabla.
usuario \ ordenador 0 (piedra) 1 (papel) 2 (tijeras)
0 (piedra) 0 2 1
1 (papel) 1 0 2
2 (tijeras) 2 1 0
130 Proyecto «piedra, papel, tijeras»
Si comparas las tablas 5.1 y 5.3 verás que cuando hay empate tenemos siempre un cero;
cuando hay victoria del usuario, tenemos siempre un uno; y en caso de derrota, un dos.
La implementación de este algoritmo es:
private static int usuarioGana(int eleccionPC, int eleccionUsuario) {
int res = eleccionUsuario - eleccionPC;
if (res < 0) {
res += [Link];
}
return res;
}
Este método nos devolverá unos números difíciles de interpretar, salvo si usamos estas
constantes:
private static final int EMPATE = 0;
private static final int GANAS = 1;
private static final int PIERDES = 2;
De momento, llamamos al método y, en el siguiente paso, ya lo procesamos y se lo
mostramos al usuario:
int resultado = usuarioGana(eleccionPC, eleccionUsuario);
Por tanto, el código completo queda así:
import [Link];
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
private static final String TIJERAS = "T";
private static final String SALIR = "S";
private static final String[] JUEGO = {PIEDRA, PAPEL, TIJERAS};
private static final int EMPATE = 0;
private static final int GANAS = 1;
private static final int PIERDES = 2;
private static final int ERROR_NO_ENCONTRADA = -1;
// Mensajes al usuario
private static final String BIENVENIDA =
"¡Bienvenido al juego Piedra-papel-tijeras!";
private static final String PEDIR_JUGADA = "¿Cuál es tu jugada? "
+ PIEDRA + " (piedra), " + PAPEL + " (papel), "
+ TIJERAS + " (tijeras) o " + SALIR + " (salir)";
private static final String MSJ_ERROR_NO_ENCONTRADA =
"No entiendo tu jugada";
public static void main(String[] args) {
// abrimos un scanner para leer la entrada del usuario
Scanner s = new Scanner([Link]);
// Instrucciones
[Link](BIENVENIDA);
[Link](PEDIR_JUGADA);
Calcular el ganador de la jugada 131
// Jugada del ordenador
int eleccionPC = (int)([Link]() * [Link]);
[Link]("*** Ordenador " + JUEGO[eleccionPC]);
// Jugada del usuario
String sEleccionUsuario = [Link]();
[Link]("*** Usuario " + sEleccionUsuario);
// Interpretación de la jugada del usuario
int eleccionUsuario = convertir(sEleccionUsuario);
[Link]("*** Interpretación " + eleccionUsuario);
if (eleccionUsuario == ERROR_NO_ENCONTRADA) {
[Link](MSJ_ERROR_NO_ENCONTRADA);
}
// Calcular el ganador de la jugada
int resultado = usuarioGana(eleccionPC, eleccionUsuario);
[Link]("*** Resultado: " + eleccionUsuario);
// cerramos lo que abrimos
[Link]();
}
private static int convertir(String sEleccionUsuario) {
for (int i = 0; i < [Link]; i++) {
if (JUEGO[i].equalsIgnoreCase(sEleccionUsuario)) {
return i;
}
}
return ERROR_NO_ENCONTRADA; // TODO tratar esto correctamente
}
private static int usuarioGana(int eleccionPC, int eleccionUsuario) {
int res = eleccionUsuario - eleccionPC;
if (res < 0) {
res += [Link];
}
return res;
}
}
NOTA:
He añadido algo de texto a cada [Link] con asteriscos para distinguirlos en las
pruebas, pues empieza a haber demasiados mensajes y podrían resultar confusos.
Y el resultado de unas cuantas pruebas:
• Primero, fuerzo un empate, respondiendo lo mismo que el ordenador:
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** Ordenador L
L
*** Usuario L
*** Interpretación 1
*** Resultado: 0
132 Proyecto «piedra, papel, tijeras»
Interpreta un 1 porque la L de papel es la segunda opción del array. El resultado es
0 porque es un empate.
• Luego, fuerzo una victoria:
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** Ordenador L
T
*** Usuario T
*** Interpretación 2
*** Resultado: 1
¡Si el ordenador dice papel, yo digo tijeras, así que resultado 1, gano!
• Finalmente, una derrota:
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
*** Ordenador L
P
*** Usuario P
*** Interpretación 0
*** Resultado: 2
Si el ordenador dice papel, yo digo piedra, que quiero perder, y pierdo, resultado: 2.
Mostrar el resultado de la jugada
Para mostrar el resultado de la jugada, usaremos un switch sobre el resultado, con un
mensaje distinto en cada caso:
switch (resultado) {
case GANAS:
[Link]("¡Enhorabuena! Tu "
+ JUEGO[eleccionUsuario] + " gana a "
+ JUEGO[eleccionPC]);
break;
case PIERDES:
[Link]("¡Lo siento! Tu "
+ JUEGO[eleccionUsuario] + " pierde ante "
+ JUEGO[eleccionPC]);
break;
case EMPATE:
[Link]("¡Empate a " + JUEGO[eleccionPC] + "!");
break;
}
Llegó el momento de quitar las trazas que pusimos para ver qué iba pasando, pues el
resultado ya nos lo cuenta. Eso sí, ahora, es imposible hacer trampas, así que puede
costarnos más probar todos los casos.
Mostrar el resultado de la jugada 133
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
P
¡Enhorabuena! Tu P gana a T
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
P
¡Lo siento! Tu P pierde ante L
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
P
¡Empate a P!
Hacer el juego repetitivo
Ya casi hemos terminado. Solo nos queda hacer el juego iterativo, que tras cada partida
ofrezca otra, hasta que la respuesta del usuario sea S de salir.
No es complicado de hacer, pero requiere unos cuantos cambios.
• Metemos dentro de un bucle infinito while (true) casi todo el código del main, aunque
dejamos fuera la apertura y cierre de Scanner y el mostrar las instrucciones, tampoco
queremos ser pesados con el pobre usuario.
• Añadimos el control de salida en caso de que el usuario responda S, usando break.
• Salimos del bucle con la instrucción continue, para volver a iterar si el usuario no
dio una buena respuesta.
• Añadimos un nuevo mensaje para pedirle al usuario que vuelva a jugar.
• Y un mensaje de final de partida.
El código con todas estas modificaciones es el siguiente:
/* Proyecto – Vamos a jugar a piedra-papel-tijeras contra el ordenador.
* Tendrás que explicarle al jugador cómo se juega, pedirle que nos dé su jugada
* (Piedra, papeL, Tijeras, Salir), generar una jugada aleatoria para el ordenador
* y decidir quién ha ganado.
*/
import [Link];
public class PiedraPapelTijeras {
private static final String PIEDRA = "P";
private static final String PAPEL = "L";
private static final String TIJERAS = "T";
private static final String SALIR = "S";
private static final String[] JUEGO = {PIEDRA, PAPEL, TIJERAS};
private static final int EMPATE = 0;
private static final int GANAS = 1;
private static final int PIERDES = 2;
private static final int ERROR_NO_ENCONTRADA = -1;
134 Proyecto «piedra, papel, tijeras»
// Mensajes al usuario
private static final String BIENVENIDA =
"¡Bienvenido al juego Piedra-papel-tijeras!";
private static final String OPCIONES =
PIEDRA + " (piedra), " + PAPEL + " (papel), "
+ TIJERAS + " (tijeras) o " + SALIR + " (salir)";
private static final String PEDIR_JUGADA =
"¿Cuál es tu jugada? + OPCIONES;
private static final String PEDIR_NUEVA_JUGADA =
"¿Cuál es tu nueva jugada? " + OPCIONES;
private static final String FIN = "Fin de la partida";
private static final String MSJ_ERROR_NO_ENCONTRADA =
"No entiendo tu jugada";
public static void main(String[] args) {
// abrimos un scanner para leer la entrada del usuario
Scanner s = new Scanner([Link]);
// Instrucciones
[Link](BIENVENIDA);
[Link](PEDIR_JUGADA);
while(true) { // iteramos para siempre
// Jugada del ordenador
int eleccionPC = (int)([Link]() * [Link]);
// Jugada del usuario
String sEleccionUsuario = [Link]();
if ([Link](SALIR)) {
break; // Si nos da una S, cortamos el bucle para terminar
}
// Interpretación de la jugada del usuario
int eleccionUsuario = convertir(sEleccionUsuario);
if (eleccionUsuario == ERROR_NO_ENCONTRADA) {
[Link](MSJ_ERROR_NO_ENCONTRADA);
continue; // Seguimos en el bucle, siguiente iteración
}
// Calcular el ganador de la jugada
int resultado = usuarioGana(eleccionPC, eleccionUsuario);
// Mostar el resultado de la jugada
switch (resultado) {
case GANAS:
[Link]("¡Enhorabuena! Tu "
+ JUEGO[eleccionUsuario] + " gana a "
+ JUEGO[eleccionPC]);
break;
case PIERDES:
[Link]("¡Lo siento! Tu "
+ JUEGO[eleccionUsuario] + " pierde ante "
+ JUEGO[eleccionPC]);
break;
case EMPATE:
[Link]("¡Empate a " + JUEGO[eleccionPC] + "!");
break;
}
[Link](PEDIR_NUEVA_JUGADA);
Hacer el juego repetitivo 135
}
[Link](FIN);
// cerramos lo que abrimos
[Link]();
}
private static int convertir(String sEleccionUsuario) {
for (int i = 0; i < [Link]; i++) {
if (JUEGO[i].equalsIgnoreCase(sEleccionUsuario)) {
// nos permite aceptar respuestas en mayúscula o minúscula
return i;
}
}
return ERROR_NO_ENCONTRADA; // TODO tratar esto correctamente
}
private static int usuarioGana(int eleccionPC, int eleccionUsuario) {
int res = eleccionUsuario - eleccionPC;
if (res < 0) {
res += [Link];
}
return res;
}
}
Y la prueba de rigor:
> java PiedraPapelTijeras
¡Bienvenido al juego Piedra-papel-tijeras!
¿Cuál es tu jugada? P (piedra), L (papel), T (tijeras) o S (salir)
t
¡Enhorabuena! Tu T gana a L
¿Cuál es tu nueva jugada? P (piedra), L (papel), T (tijeras) o S (salir)
T
¡Lo siento! Tu T pierde ante P
¿Cuál es tu nueva jugada? P (piedra), L (papel), T (tijeras) o S (salir)
t
¡Enhorabuena! Tu T gana a L
¿Cuál es tu nueva jugada? P (piedra), L (papel), T (tijeras) o S (salir)
t
¡Empate a T!
¿Cuál es tu nueva jugada? P (piedra), L (papel), T (tijeras) o S (salir)
s
Fin de la partida
NOTA:
Observa que tanto cuando respondí con una t minúscula como con una T mayúscula, el
programa lo entiende igualmente. Eso es gracias al equalsIgnoreCase.
136 Proyecto «piedra, papel, tijeras»
Mejoras y evoluciones
Al principio de este libro hablamos de gatear antes de andar, y gateamos un montón
haciendo ejercicios aparentemente ni muy útiles ni muy realistas. Pero con todo ese
entrenamiento hemos logrado dar ahora nuestros primeros pasos, siendo capaces de
crear un programa real y útil.
Ahora, eso sí, como todos los programas, este es mejorable. Ahora que lo ves funcionar
puedes pensar: «Me gustaría enseñar las palabras enteras y no las letras cuando doy la
respuesta al usuario, o limitar la partida a los intentos que me pase el usuario como
argumento al lanzar el programa, o…». Bien, ¡pues hazlo!
Puedes aprender mucho mejorando o haciendo evolucionar este programa.
Espero que no pierdas muchas horas jugando al «piedra, papel, tijeras» contra tu ordenador.
Quizá te apetezca enseñárselo a tu abuela o a tus compañeros de clase o de trabajo.
Con este proyecto cerramos la primera parte del libro dedicada a las estructuras básicas,
con lo que ya podemos dar por «controlada» la programación estructurada, una base
necesaria para seguir aprendiendo, ahora con la programación orientada a objetos.
Mejoras y evoluciones 137
2
Parte
Parte
Orientación a
objetos
6 Diseño orientado a objetos
En este capítulo aprenderás a:
• Analizar los requisitos de un proyecto.
• Extraer los principales conceptos.
• Esbozar un diagrama conceptual.
• Dibujar un diagrama conceptual usando UML.
Introducción
En esta segunda parte del libro intentaré ayudarte a entender los conceptos básicos de la
programación orientada a objetos. La orientación a objetos es un paradigma de programación,
una forma de programar, que pretende modelar la realidad del problema que tiene que resolver
representando en forma de objetos los conceptos de dicha realidad. Estos conocen sus propias
características (cada Persona sabe cuál es su fecha de nacimiento o su nombre) y pueden
ejecutar acciones (cada Persona sabe decir/calcular qué edad tiene o firmar un documento).
Trabajaremos sobre un caso práctico, la gestión de sOOPer, es decir, un supermercado orientado
a objetos. Y a partir de este ejemplo, veremos la auténtica magia de la orientación a objetos.
Para no hundirnos en la teoría, voy a darle un enfoque eminentemente práctico, aunque
explicaré, a medida que los vayamos utilizando, cada uno de los conceptos que veamos.
Para entender esta parte del libro necesitas saber programar un poco en Java, saber crear clases
y métodos, ejecutar (mediante un método main) y sentirte más o menos cómodo con tu entorno
de desarrollo Java, por ejemplo, Eclipse. Si aún no tienes estos conocimientos, los puedes
adquirir en la primera parte del libro.
Requisitos del proyecto
Cuando nos enfrentamos a cualquier proyecto, lo primero es entender sus requisitos. Si no
comprendemos qué nos están pidiendo, qué tiene que hacer nuestro programa, malamente
lograremos hacerlo bien.
Esta es la documentación que hemos recibido de nuestro cliente:
Se pide una aplicación para gestionar el embolsado de los productos de los pedidos del
supermercado online sOOPer.
Actualmente hay dos tipos de contenedores: bolsas y cajas. Las cajas son rectangulares
y aguantan «cualquier peso», mientras que las bolsas tienen una resistencia máxima.
En ambos casos, tenemos un volumen determinado por sus dimensiones. Hay varios
tamaños disponibles.
Los productos del supermercado se dividen en varias categorías: Alimentación, Higiene,
Droguería y Mascotas. Los productos de Alimentación, a su vez, se subdividen en
Congelados, Frescos y No Perecederos.
Cada producto tendrá un volumen y un peso determinado, que tendremos que considerar
«ocupado» cuando lo embolsemos.
Los productos de alimentación no pueden ser mezclados con los de las otras categorías.
Los de higiene no se pueden mezclar con los de alimentación, y los de droguería, ni con
los de alimentación ni con los de mascotas.
En la primera versión de esta aplicación no es necesario optimizar la distribución ni tener
en cuenta temperaturas ni caducidades.
142 Diseño orientado a objetos
Ante este enunciado, lo primero es analizar este texto con detenimiento.
Extracción de conceptos
El primer paso al recibir los requisitos del cliente, de cara a desarrollar un proyecto orientado
a objetos, es analizar el texto y detectar los sustantivos y los verbos que aparecen en el mismo.
Los sustantivos podrán o no ser nuestras futuras clases y sus atributos, mientras que los verbos
podrán o no ser nuestros futuros métodos.
Este proceso quizás parezca un poco tedioso, pero para extraer los conceptos orientados a
objetos que pueden representar nuestro negocio, tenemos que leer con detenimiento y atención
el enunciado, e ir subrayando las palabras clave.
Vamos a por la primera frase:
Se pide una aplicación para gestionar el embolsado de los productos de los pedidos del
supermercado online sOOPer.
Encontramos dos verbos (o acciones, más bien): gestionar (que suele ser lo que hacen todas
las aplicaciones) y «el embolsado», que no es un verbo, pero sí una acción, que parece que
puede ser el principal objetivo de nuestra aplicación. Los marcamos en negrita.
En cuanto a nombres, hay «productos», «pedidos» y «supermercado», que marco con su
subrayado.
Actualmente hay dos tipos de contenedores: bolsas y cajas.
Parece que tendremos que manejar «contenedores», y que hay dos «tipos»: «bolsas» y «cajas».
Los destacamos con el subrayado, son nombres.
Las cajas son rectangulares y aguantan «cualquier peso», mientras que las bolsas tienen
una resistencia máxima.
Parece que será interesante tratar conceptos como «peso» y «resistencia» de los contenedores:
nombres, subrayados. También detectamos un adjetivo, que podría ser un posible valor:
«rectangulares». Esta palabra la marcaremos en cursiva.
En ambos casos, tenemos un volumen determinado por sus dimensiones.
También hay que tener en cuenta «volumen» y «dimensiones». Son nombres: subrayado.
Hay varios tamaños disponibles.
Extracción de conceptos 143
Y además, habrá distintos «tamaños», suponemos que de contenedores. Si tenemos
cualquier duda, en un proyecto real, consultaremos con nuestro equipo y con el cliente,
para clarificar si esos tamaños disponibles son de contenedores o son de cualquier otra
cosa. En este caso, la consulta es conmigo misma y, sí, son tamaños de los contenedores.
Los marcamos subrayándolos.
Los productos del supermercado se dividen en varias categorías: Alimentación, Higiene,
Droguería y Mascotas. Los productos de Alimentación, a su vez, se subdividen en
Congelados, Frescos y No Perecederos.
En este párrafo nos aparece un concepto nuevo «categorías»: nombre, subrayado. Y unos
cuantos de los valores que puede tomar: «alimentación», «higiene», «droguería», «mascotas»,
«congelados», «frescos» y «no perecederos», que marcaremos en cursiva, como la forma
rectangular de las cajas.
Cada producto tendrá un volumen y un peso determinado, que tendremos que
considerar «ocupado» cuando lo embolsemos.
En esta frase reaparecen conceptos ya conocidos como «producto», «volumen», «peso»,
todos subrayados. De nuevo aparece el «verbo» embolsar, en negrita. Y un valor/
concepto, ya veremos qué es «ocupado». De momento lo marcamos en cursiva.
Los productos de alimentación no pueden ser mezclados con los de las otras categorías.
Los productos de higiene no se pueden mezclar con los de alimentación, y los de
droguería, ni con los de alimentación ni con los de mascotas.
En este párrafo no hay términos nuevos, salvo el verbo «mezclar», que pondremos en
negrita. Se mencionan productos y categorías, indicando qué categorías se pueden mezclar
con otras. Desde luego, será información interesante para nuestro sistema, pero no ahora
mismo.
En la primera versión de esta aplicación no es necesario optimizar la distribución,
ni tener en cuenta temperaturas ni caducidades.
Finalmente, aunque nos especifican algunos conceptos más como optimizar (verbo,
negrita), la distribución (nombre, subrayado), tener en cuenta (verbo, negrita), temperatura
y caducidades (nombres, subrayados), a la vez nos dicen que de momento nos podemos
olvidar de ellos. Quizá no del todo… Tendríamos que estudiar las posibilidades de que
esos conceptos fueran incluidos en la segunda versión y qué impacto acarrearía tenerlos
ya en cuenta o aún no. Es decir, no hemos de tratar cosas que no están incluidas en nuestro
proyecto, ni dejarlo preparado para enviar una expedición a la luna, pero, desde luego,
T6.1 si ya tenemos esa pista… bueno, dejemos abierto el modelo para que pueda tolerarla
bien, si llega, sin comprometer nuestro diseño.
144 Diseño orientado a objetos
Boceto del modelo conceptual
Partiendo de la lista de nombres, verbos, valores extraídos de la lectura detallada de los
requisitos del cliente, intentemos ordenarlos de forma que representen nuestro sistema.
La lista es la siguiente:
Tabla 6.1. Conceptos del sOOPer.
Nombres Verbos Valores
• producto • gestionar • rectangular
• pedido • embolsar • alimentación
• supermercado • mezclar • higiene
• tipo de contenedor • droguería
• contenedor • mascotas
• bolsa • congelados
• caja • frescos
• peso • no perecederos
• resistencia • ocupado
• volumen
• dimensiones
• tamaños
• categorías
NOTA:
Vamos a hacer un esbozo, si tienes papel y lápiz a mano, no lo dudes, es la mejor opción.
Empezamos por los nombres: parece que el supermercado contará con pedidos que
tendrán contenedores con productos.
Figura 6.1. Relación entre supermercado, pedidos, contenedores y productos.
Los contenedores pueden ser de dos tipos: bolsas o cajas.
Figura 6.2. Relación entre contenedores, bolsas y cajas.
Boceto del modelo conceptual 145
Peso, resistencia, volumen, dimensiones, tamaño… tienen pinta de ser atributos de
algunos de estos objetos: un producto tendrá un peso y un volumen, un contenedor
tendrá una resistencia (el peso que soporta) y unas dimensiones (que le darán el
volumen que admite).
Y dentro de los contenedores, imaginaremos que cajas y bolsas cuentan con un
comportamiento ligeramente distinto a la hora de embolsar los productos. Si
necesitamos saber su volumen, por ejemplo, la forma de calcularlo, aunque en ambos
casos dependa de sus dimensiones, no será exactamente igual. Tampoco parece que
se vayan a comportar igual en cuanto a la gestión del peso, pues nos dicen que las
bolsas tienen una resistencia máxima, mientras que las cajas aguantan cualquier
peso. Por eso los pintamos como hermanos en nuestro boceto… Se parecen,
comparten cosas con su padre, pero no son por completo iguales.
Las categorías pueden ser un atributo del producto, pero veremos cómo hay
diferencias significativas entre ellas. Así que la experiencia, y también el enfoque
que le quiero dar a este ejercicio, me llevan a pintar también una jerarquía de
productos en función de sus categorías:
Figura 6.3. Jerarquía de productos.
Con estos bocetos ya disponemos de una primera versión del modelo conceptual de
nuestra aplicación, los juntamos todos y más o menos ya nos apañamos. Sin embargo,
en un entorno profesional, lo suyo, si hay presupuesto (es decir, tiempo), es hacer las
cosas bien y, en este caso, hacer bien el diagrama sería utilizar un formato estandarizado
para dibujarlo, en otras palabras, trabajar con UML.
146 Diseño orientado a objetos
Representación del modelo conceptual en UML
UML es un lenguaje gráfico para la representación de modelos bastante extenso y
complejo. No es objetivo de este libro estudiarlo con detalle, ni siquiera usarlo con extrema
exactitud. Nuestro propósito solo es representar de forma clara pero sencilla cómo
queremos que sea nuestro sistema. Es por ello por lo que el diagrama que saquemos
ahora puede no ser perfecto, pero espero que sí sea útil.
Figura 6.4. Modelo conceptual de sOOPer en UML.
Para representar una clase se utiliza un rectángulo con varios departamentos. En el
primero pondremos el nombre de la clase y, en los otros, los atributos y métodos,
pero nosotros vamos a hacer la versión simplificada. No requerimos tanto detalle.
En la primera fila tenemos los rectángulos para las clases Supermercado, Pedido,
Contenedor y Producto. Están relacionados entre ellos por medio de una línea
continua sin flechas que representa una relación de tipo asociación. Decimos que un
Supermercado puede tener 0..n Pedidos. Es decir, puede tener uno, muchos o incluso
ninguno si el negocio va realmente mal. Pero cada Pedido pertenecerá a un solo
Supermercado, pero para existir un Pedido, necesitamos que exista un supermercado,
por eso la relación es 1..1.
Decimos que cada Pedido incluirá entre ninguno y muchos Contenedores, pero sí
es posible disponer de contenedores (pues son físicos, reales) sin tener aún un pedido,
¿no? Entonces relación 0..n entre Pedido y Contenedor y relación 0..1 entre Contenedor
y Pedido.
Y ¿qué meteremos en esos Contenedores? ¡Pues Productos! Que también existen de
antes de meterlos en el Contenedor: una misma botella de aceite solo la meteremos
en una bolsa, pero en la bolsa guardaremos varias botellas de aceite, paquetes de
arroz… y lo que haga falta, ¿verdad? De nuevo, la relación entre Contenedor y
Producto será que contiene 0..n, y 0..1 en el otro sentido.
Representación del modelo conceptual en UML 147
Bien, vamos avanzando. Centrémonos ahora en los Contenedores. En nuestro boceto
hay dos tipos de contenedores, Bolsas y Cajas, y que conceptualmente nos interesa
distinguirlos porque sus características y/o su comportamiento no son los mismos.
Creamos entonces las dos cajitas para Bolsa y Caja (¡vaya lío tanta caja!). Y, a continuación,
lo relacionamos con el Contenedor, pero esta vez ya no es una asociación (la línea sin
flechas), sino una herencia: pues una Bolsa o una Caja son un tipo de Contenedor, son
como un Contenedor, pero más concretas. Las herencias se representan en UML con una
línea continua con un extremo con un triángulo vacío. El triángulo lo pondremos en el
lado del padre y, por lo general, la clase padre se pinta por encima de las hijas, de forma
que quede el dibujo como una pirámide.
Seguimos con los Productos. El cliente en los requisitos habló de cuatro categorías de
Producto distintas, y así está reflejado en el boceto. Bien, también serán «hijas» de
Producto, así que añadiremos esos cuatro rectángulos: Alimentación, Mascotas, Higiene
y Droguería, por debajo de Producto.
Como en el caso de Cajas y Bolsas, las categorías de Producto heredan de Producto, pues
cada una representará a un Producto, pero con algunas peculiaridades. Usaremos la
flecha con el triángulo vacío para representar esa herencia.
Pero la cosa no era tan simple, existe una categoría con subcategorías. Añadamos, por
debajo de Alimentación, tres cajas para Congelado, Fresco y NoPerecedero. Como ya
empezamos a hablar de clases y no de simples conceptos como en el boceto, pondremos
NoPerecedero en una sola palabra (sin el espacio) y mediante camelCase (inicial de cada
palabra intermedia en mayúscula), siguiendo las convenciones de Java. Estas tres cajitas
comparten una relación de herencia (flecha con triángulo) de Alimentación.
T6.2 Ahora, tanto nosotros como cualquier otro compañero, comprenderá con facilidad cuáles
serán los objetos de nuestro sistema y cómo se relacionan entre ellos.
Test
Test 6.1. Busca nombres, verbos y adjetivos en este requisito:
Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos
nombre, apellidos y fecha de nacimiento.
a) Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos nombre, apellidos
y fecha de nacimiento.
b) Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos nombre, apellidos
y fecha de nacimiento.
c) Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos nombre, apellidos
y fecha de nacimiento.
d) Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos nombre, apellidos
y fecha de nacimiento.
148 Diseño orientado a objetos
Test 6.2. ¿Qué clase hereda de cuál en este diagrama UML?
Figura 6.5. Modelo conceptual del test 6.2 en UML.
a) Libro hereda de Autor.
b) Autor hereda de Escritor e Ilustrador.
c) Escritor e Ilustrador heredan de Autor.
d) Autor hereda de Libro.
Soluciones
Test 6.1. Busca nombres, verbos y adjetivos en este requisito:
Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos
nombre, apellidos y fecha de nacimiento.
b) Los profesores darán de alta sus alumnos en cada grupo. De cada alumno tendremos
nombre, apellidos y fecha de nacimiento: la única acción relevante de este fragmento es
dar de alta. Profesores, alumnos, grupo, nombre, apellidos y fecha de nacimiento son
nombres que podrán ser clases o atributos. Y valores, adjetivos, no hay. Los hallaríamos,
por ejemplo, si se hubiera hablado de alumnos adultos o menores de edad.
Test 6.2. ¿Qué clase hereda de cuál en este diagrama UML? (figura 6.5)
c) Escritor e Ilustrador heredan de Autor.
Soluciones 149
7 Clases y objetos
En este capítulo aprenderás a:
• Identificar las clases y las interfaces.
• Trabajar con la paquetización.
• Usar los enumerados.
• Aplicar los enumerados en un ejemplo práctico.
Introducción
Los programas que escribimos en la primera parte del libro usan Java, pero no orientación
a objetos. Cada uno de esos programas lo escribimos en un fichero, con una clase. En los
programas orientados a objetos por lo general disponemos de muchas clases, es decir,
muchos ficheros. Veamos rápidamente algunos de los conceptos más habituales de la
orientación a objetos sobre los que luego profundizaremos, a la vez que los aplicamos sobre
el ejemplo del sOOPer.
Las clases describen las propiedades (campos o atributos) y las habilidades (métodos) de un
objeto de la vida real. Por ejemplo, los pedidos del proyecto que estamos haciendo tienen
atributos, como su referencia o la lista de productos, y métodos, como añadir un producto.
Podemos considerar las clases como una plantilla. Una plantilla, ¿para qué? Para crear objetos.
Los objetos son instancias de las clases. Una cosa es definir qué es un pedido y la otra es mi
pedido del supermercado de esta semana, de media docena de huevos y un litro de leche, y
que será un pedido distinto al tuyo de ayer, de un kilo de manzanas y un paquete de galletas.
Para crear objetos de una clase utilizaremos los constructores, que son métodos especiales de
las clases. En Java no hay destructores, en otros lenguajes orientados a objetos sí.
Con frecuencia hallamos muchas clases en nuestros programas, así que hay que organizarlas
bien. Para ello recurriremos a los paquetes, que son colecciones de clases que encajan
lógicamente y que pueden interactuar entre ellas.
A veces, cuando implementamos una clase, no contamos con toda la información necesaria
para saber cómo implementar cada uno de sus métodos. Quizá sabemos que en un pedido
es posible añadir productos, pero desconocemos cómo añadirlos. Las interfaces nos permiten
declarar los métodos que debe tener una clase, pero sin implementarlos. Las clases abstractas
están en un punto intermedio entre una interfaz y una clase «normal».
Finalmente, tenemos otro tipo de «clase especial» que son los enumerados como, por ejemplo,
los días de la semana.
Paquetes
Nos dice Wikipedia, al menos en el momento que la consulté yo al preparar este libro (ya
sabemos que puede cambiar en cualquier momento y por cualquier motivo), que «un paquete
en Java es un contenedor de clases que permite agrupar las distintas partes de un programa
y que por lo general tiene una funcionalidad y elementos comunes, definiendo la ubicación
de dichas clases en un directorio de estructura jerárquica».
No está mal la definición, ¿cómo lo definiría yo? Los paquetes son una forma de agrupar las
clases, para organizarlo todo mejor. Piensa que en un proyecto empresarial «de verdad» puede
haber cientos de clases. Si las agrupamos por paquetes, que en Eclipse son representados como
si fueran carpetas, y en el disco duro también lo veremos así, será todo mucho más manejable.
152 Clases y objetos
Además, dentro de un paquete, podemos crear más paquetes. Si organizamos bien nuestra
jerarquía , será muy fácil en un proyecto, por grande que sea, por muchas clases que contenga
y por muchos programadores que intervengan en su desarrollo, que todo el mundo encuentre
rápidamente la clase que está buscando.
El nombre del paquete en el que está una clase sería como los apellidos de una persona, así
pues, el nombrado completo de una clase siempre incluirá su paquete. Los nombres de las
clases han de ser únicos, pero teniendo en cuenta el nombre del paquete. Por dicho motivo
hay clases con el mismo nombre, en distintos paquetes. Nosotros no lo requeriremos en este
proyecto. Si lo necesitas en otros, adelante, pero con cuidado, que puede resultar confuso.
¿Cómo se nombra un paquete? En un proyecto profesional, hay unas reglas comunes para
empezar a nombrarlos y evitar colisiones a nivel mundial. Solo se usa el dominio web de
la empresa (¿de quien escribe el código o de quien lo encarga?, depende), pero escrito en
orden inverso. Digamos que trabajamos para Empresa Guay que tiene el dominio
[Link]. Nuestros paquetes deberán llamarse [Link]. Luego
añadiremos el nombre del proyecto y lo que vayamos necesitando en nuestra jerarquía de
paquetes, todos separados con puntos. Cada punto será un nivel más.
Puedes mirar en el API de Java cómo es su organización por paquetes. Es buena, es una
referencia muy clara.
Como nuestro proyecto no saldrá de nuestros equipos, salvo que decidas montar un sOOPer
tras terminar este curso, omitiremos la parte del [Link] y comenzaremos a partir
del nombre del proyecto: sooper.
Los nombres de paquetes van enteritos en minúsculas y sin signos de separación. Si nos fijamos
en nuestro diagrama conceptual (figura 7.1), asoman dos paquetes bastante claros: uno para
toda la parte de contenedores, otro para toda la parte de productos.
Figura 7.1. Paquetes en el diagrama conceptual.
Paquetes 153
En las clases que están dentro de un paquete, indicaremos, en la primera línea no
comentada, en qué paquete están. Por ejemplo:
package [Link];
NOTA:
En los ejercicios y ejemplos de la primera parte no hemos utilizado paquetes, porque estábamos
aprendiendo cosas más básicas. Sin embargo, siempre hay que contar con una buena estructura
de paquetes y las clases siempre deberían estar metidas en alguno.
Para el proyecto sOOPer, de momento, consideramos los siguientes paquetes (ver figura 7.2).
Figura 7.2. Paquetes de sOOPer vacíos, vistos en Eclipse.
Quizá creemos alguno más, si lo requerimos más adelante.
Esta misma jerarquía de paquetes es la de nuestro sistema de archivos. Si creas un proyecto
en un IDE, por lo general creará las carpetas correspondientes a los paquetes dentro de
una carpeta src, en la que guardará el código (los ficheros *.java), y en otra carpeta aparte,
al mismo nivel que src, irán los ficheros generados (*.class), de nuevo con la estructura
de carpetas correspondiente a la paquetización establecida. Esa carpeta se suele llamar
classes o target, depende de cómo configuremos el IDE.
Para utilizar clases de otro paquete, importaremos la clase usando la sentencia import
entre la sentencia package y la declaración de la clase, excepto cuando se trate del paquete
[Link], que Java ya lo importa por sí mismo (por eso no necesitamos importar nada
para usar la clase String, por ejemplo):
import [Link];
entre el package y la clase.
TRUCO:
Los entornos de desarrollo modernos nos ayudan a gestionar las importaciones. En Eclipse podemos
hacer Ctrl-Mayús-O o Command-Mayús-O para ejecutar el comando Organize Imports.
Antes de que estas herramientas tuvieran esta capacidad, las importaciones eran más engorrosas
y se solía importar todas las clases de un paquete a la vez, usando un asterisco:
import [Link].*;
T7.1 Hoy en día, importar paquetes enteros se considera una mala práctica, así que dejemos que el
IDE nos ayude e importemos única y exclusivamente lo que necesitamos.
154 Clases y objetos
Interfaces
Nos dice Wikipedia que «una interfaz en Java es una colección de métodos abstractos y
propiedades constantes. En las interfaces se especifica qué se debe hacer, pero no su
implementación. Serán las clases que implementen estas interfaces las que describen la
lógica del comportamiento de los métodos». ¿Qué significa esto? Una de las ventajas de
la programación orientada a objetos respecto a la programación estructurada es que nos
permite aumentar el grado de abstracción, lo que hace que lo que programamos se parezca
más a la realidad humana y menos a la de los ordenadores. Así pues, las interfaces son
como un tipo especial de clase en Java que nos dice qué deben hacer aquellas clases que
las implementan, pero no cómo.
Se suele decir que tener una interfaz por encima de la clase sirve para disponer de distintas
implementaciones de la misma clase. Cierto. Sin embargo, en mi carrera profesional,
pocas veces he requerido implementar de dos formas diferentes una clase. Pero es verdad
que, en caso de necesitarlo, todas las implementaciones responderían a los mismos
mensajes, tendría los mismos métodos, de una forma estándar.
Otra de las ventajas, que sí he disfrutado con más frecuencia, ha sido facilitar el trabajo
en equipo y entre equipos. Pongamos que debo implementar la parte de contenedores
del sOOPer y tú, la parte de productos. Unos y otros precisamos interactuar, pero ni yo
puedo esperar para empezar mi parte a que tú termines la tuya ni al revés.
Ambos comenzamos con unas interfaces IContenedor e IProducto y definimos qué
métodos tendrán, cómo se llamarán, sus parámetros, qué tipo de dato devolverán. Yo
puedo empezar a utilizar productos en mi código y tú, contenedores en el tuyo. Aún no
podremos probarlo, pero sí es posible escribir código que compile. Según vayamos
terminando las implementaciones, iremos probando, arreglando…
Para declarar una interfaz, la sintaxis sería la siguiente:
public interface InterfazEjemplo {
}
Lo veremos mejor con un ejemplo.
Las interfaces de sOOPer
Eso es la teoría, pero ¿cómo lo aplicamos a nuestro proyecto? Me parece que me salen
tres interfaces para el sOOPer. Aparte de IContenedor e IProducto que acabo de
mencionar, también haría una IPedido.
En un pedido quiero añadir contenedores y productos (que se distribuirán en esos
contenedores), pero la verdad es que podría tener pedidos online desde una web, desde
una app móvil e incluso desde el supermercado físico.
Interfaces 155
Interfaz IPedido
Creamos la primera interfaz, IPedido, o mejor dicho, [Link]. No es
obligatorio llamarlas con una I delante del nombre que tendría la clase, pero a
menudo se hace así.
Pensemos qué se requiere saber sobre un pedido: quizá conocer su referencia, qué
productos incluye, qué contenedores tiene… Este tipo de métodos, por convención,
se suelen llamar getLoQueSea, por ejemplo, getReferencia, getProductos y
getContenedores.
String getReferencia();
Set<IProducto> getProductos();
Set<IContenedor> getContenedores();
getReferencia nos devolverá un String, y los otros dos un conjunto de IProductos e
IContenedores respectivamente.
Y con los pedidos haremos cosas, como añadir contenedores:
void addContenedor(IContenedor contenedor);
No ponemos llaves, sino solo un punto y coma, porque estamos simplemente declarando
el método. No tiene cuerpo todavía.
O añadir productos, a la vez que el propio pedido nos dice en qué contenedor los ha
colocado:
IContenedor addProducto(IProducto producto);
Es decir, queremos pedidos inteligentes que sean capaces de distribuir los productos en
los contenedores.
El fichero [Link] quedaría así:
package sooper;
import [Link];
public interface IPedido {
String getReferencia();
Set<IProducto> getProductos();
Set<IContenedor> getContenedores();
void addContenedor(IContenedor contenedor);
IContenedor addProducto(IProducto producto);
}
156 Clases y objetos
Interfaz IContenedor
Seguimos con IContenedor. Lo primero, ¿qué hemos de saber de los contenedores? Su
referencia, su volumen inicial, y cuánto volumen le queda disponible teniendo en cuenta
los productos que ya hay en él, su resistencia, qué productos contiene ya, o de qué tipo es.
String getReferencia();
int getVolumen();
int volumenDisponible();
int getResistencia();
Set<IProducto> getProductos();
String getTipo();
¿Y qué cosas realizaremos? Un par, cada una, su método. El primero, meter, que recibe
un producto, comprueba si le cabe y nos lo dice. Y si puede, lo añade al contenedor, claro.
boolean meter(IProducto producto);
Y un segundo método que comprueba si el contenedor resiste a ese producto o ¿es que
quizá ves viable meter una caja de seis litros de leche en una bolsita finita de plástico?
boolean resiste(IProducto producto);
Interfaz IProducto
Le toca el turno a IProducto. ¿Qué nos gustaría saber de un producto? Su referencia, su
peso, su volumen y su categoría. Bueno, quizá también su precio y mil millones de cosas
más, pero para este ejemplo no lo precisaremos, aunque si quieres añadirlo, todo tuyo.
String getReferencia();
int getPeso();
int getVolumen();
String getCategoria();
¿Y qué va a hacer el producto? ¡Recuerda que nuestros objetos son inteligentes! Hay que
saber si puede ir en el mismo contenedor que otro producto (¡no mezclaremos las lechugas
con la lejía!), entonces:
boolean esCompatible(IProducto p);
Si cabe o no en el contenedor:
boolean tengoEspacio(IContenedor contenedor);
Y un método realmente «productivo» que, en vez de comprobar cosas, hará que el
producto sepa que ya está metido en un contenedor, y en cuál:
void meter(IContenedor contenedor);
Ya hemos terminado, pues, las tres primeras interfaces, con sus primeros métodos que T7.2
ya van dando forma a nuestro sOOPer y especifican cómo se irán relacionando unos
objetos con otros.
Interfaces 157
Clases abstractas
Las interfaces en Java dicen qué hay que hacer, pero no cómo. Las clases que implementen
esas interfaces tendrán la obligación de implementar todos esos métodos y decidirán cómo
hacer las cosas. Pero ¿qué pasa cuando sabemos cómo hacer algunas cosas, pero no otras?
Entre las interfaces que son pura abstracción y las clases que son pura concreción, en
Java existe un elemento intermedio, la clase abstracta, que implementará lo que sepa
cómo hacer y dejará que sean clases hijas (abstractas o concretas) quienes terminen de
implementar lo que aún se desconoce.
Podríamos utilizar el símil de las fases de la luna (ver figura 7.3): una interfaz sería una
luna nueva, está vacía; la clase es una luna llena; y a medio camino se hallan las clases
abstractas, que ni están llenas del todo ni vacías por completo.
Figura 7.3. Las clases y las fases de la luna.
Para declarar una clase abstracta, la sintaxis sería la siguiente:
public abstract class ClaseAbstractaEjemplo implements InterfazEjemplo {
T7.3 }
Lo veremos mejor en el ejemplo del sOOPer.
Las clases abstractas de sOOPer
Clase abstracta Contenedor
Nos centramos en los contenedores. Creamos una clase, de momento normal, llamada
Contenedor y que implemente IContenedor, que meteremos dentro del paquete sooper.
contenedores.
package [Link];
public class Contenedor implements IContenedor {
}
Pero si dejamos el código así, habrá un error de compilación: es imposible resolver el
tipo Contenedor. Aunque ya lo habíamos definido, está en otro paquete, así que
necesitamos importar esa clase para que sea visible desde aquí. Al añadir esta línea al
principio del fichero, desaparece ese error:
import [Link];
158 Clases y objetos
Pero ahora surge otro error, en Contenedor, porque no hemos implementado los métodos
abstractos heredados de IContenedor. Si utilizamos un IDE, nos lo marca gráficamente
e incluso nos da opciones para resolverlo, dos en este caso: implementar esos métodos
o hacer la clase abstracta. De momento, escogeremos la opción de implementar, y el IDE
nos generará la estructura de los ocho métodos. Siempre estaremos a tiempo de hacer la
clase abstracta si se requiere.
@Override
public String getReferencia() {
// TODO Auto-generated method stub
return null;
}
@Override
public int getVolumen() {
// TODO Auto-generated method stub
return 0;
}
@Override
public int volumenDisponible() {
// TODO Auto-generated method stub
return 0;
}
@Override
public int getResistencia() {
// TODO Auto-generated method stub
return 0;
}
@Override
public Set<IProducto> getProductos() {
// TODO Auto-generated method stub
return null;
}
@Override
public String getTipo() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean meter(IProducto producto) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean resiste(IProducto producto) {
// TODO Auto-generated method stub
return false;
}
Nos fijamos en uno de ellos, uno cualquiera porque de momento se parecen mucho.
El IDE ha puesto una primera línea con una anotación, @Override, que significa que
el método que hay justo debajo sobrescribe la declaración del padre. No es obligatoria,
pero nos ayuda a evitar liarla cambiado la declaración del método de forma
Clases abstractas 159
accidental. La podemos dejar puesta. (Si quieres, juega con ella, cambia los parámetros
del método y mira qué pasa si está o no puesta).
Debajo de la anotación, el IDE ha puesto la declaración del método tal cual estaba
en la interfaz, pero esta vez con un public delante y llaves en vez de punto y coma.
Añade public porque, en la interfaz, todo lo que pongamos es público y, por tanto,
no es necesario ponerlo, pero a este nivel ya sí hay que ponerlo.
Además, el IDE, que es muy majete, ha rellenado el método para conseguir que todo
compile. Guarda y verás que ya no quedan «cosas rojas» ni señales de error. Si el
método devuelve un String u otro tipo de objeto, meterá un return null, si es un
número, return 0, ¿un booleano? return false. Todo para hacer callar al compilador,
pero eso no es una implementación adecuada del método, al menos en la mayoría
de los casos. Por eso, nos añade también un comentario con un TO DO (por hacer)
para recordarnos que ese método está generado automáticamente y tal vez no sea
lo que queramos. El TODO deberíamos eliminarlo cuando hayamos implementado
nuestro método. Si tenemos demasiados TODO en el código y encima algunos ya
no están «TO DO», no servirán para nada.
Veamos cómo implementar todos estos métodos. Empecemos por los get. Los get
solo devuelven algo. ¿Qué? Cada uno lo suyo. Por ejemplo, para getReferencia,
declaramos un atributo en la clase que se llame referencia, de tipo String, y privado
(pues no queremos que nadie acceda directamente a él: quien necesite conocer la
referencia que la pida en la ventanilla correspondiente, es decir, que use el método
getReferencia).
private String referencia;
Dentro del método, cambiaremos el return null por un return referencia. Como ya tenemos
resuelto el método, borramos el comentario del TODO.
@Override
public String getReferencia() {
return referencia;
}
Seguimos con getVolumen. ¿Sabemos calcular el volumen de cualquier recipiente?
Mmmm… ¡Eso depende de la forma! En una caja parece fácil: ancho por largo por
alto. ¿Y para una bolsa? Pues eso es más difícil, pero lograremos una aproximación
pensando en la bolsa llena de líquido y apoyada sobre la mesa… tendría una forma
cilíndrica, más o menos, luego, podríamos hacer algo así como la superficie del
círculo (cómo era, π * r2, ¿no?) por la altura. ¡Anda! ¡Superficie por altura! ¡Como
para la caja! La superficie de la caja es ancho por largo. ¡Pues mira que bien nos va
a quedar! Podemos decir que todos los contenedores tendrán un alto:
private int alto;
160 Clases y objetos
Y su volumen será ese alto, por la superficie, que la hallaremos en otro método.
@Override
public int getVolumen() {
return alto * getSuperficie();
}
¿Y de dónde sacamos eso de getSuperficie? Según sea una bolsa o una caja, la
superficie se calculará de forma distinta… Pues ya contamos con un buen ejemplo
de un método que nos hemos olvidado en IContenedor, ¡añadámoslo en el padre!
int getSuperficie();
Este método no lo sabremos implementar en Contenedor, porque eso lo sabrán hacer
Bolsa y Caja.
Al guardar los cambios en IContenedor, en Contenedor dejamos de tener el error
en getVolumen de no saber qué es getSuperficie (ya lo sabe, está declarado en el
padre), pero ahora hay un error en la declaración de la clase porque no estamos
implementando todos los métodos del padre. Nos falta el nuevo. Nos sugiere las
dos opciones de antes: implementarlo: ¡no podemos! ¡Nos es imposible calcular la
superficie de un contenedor abstracto sin saber qué forma tiene! ¡Uy! ¿He dicho
abstracto? ¿Contenedor abstracto? Esto de programar en orientación a objetos resulta
ser más natural de lo que imaginabas. Justo, la segunda sugerencia es hacer
Contenedor abstracto. ¡Justo es eso lo que queremos!
El IDE añade la palabra abstract en la declaración de la clase, justo entre public y
class y ¡desaparecen los errores!
Como decíamos, una clase abstracta está a medio camino entre una implementación
completa y una interfaz pura. Lo que estamos haciendo es decir que Contenedor
sabe hacer parte del trabajo de calcular el volumen, pero que no sabe calcular la
superficie y que eso se lo deja a sus hijos.
Bueno, hemos visto lo importante, seguimos con el resto de métodos…
volumenDisponible de momento lo dejaremos con el TODO. getResistencia y
getProductos te los dejo a ti. A modo de ejercicio, inspírate en la referencia, pero de
todas maneras dispones del código a cada paso para consultarlo.
meter y resiste, de momento, permanecen con el TODO, y nos queda getTipo. ¿Sabe
Contenedor decirnos de qué tipo es? Me temo que eso también será cosa de los hijos.
Y como ya hemos declarado que esta es una clase abstracta, es posible quitar este
método de aquí. Cuando hagamos las clases hijas ya lidiaremos con ello. Así pues,
T7.4
contamos ya con la clase Contenedor, que implementa la interfaz IContenedor, pero
solo un poquito, así que le ha dejado un par de tareas a los hijos (getSuperficie y
getTipo) y a nuestro yo futuro otros tres métodos. Esto va tomando forma.
Clases abstractas 161
Clases «normales»
Hemos visto las interfaces (en las que declaramos todo y no implementamos nada)
y las clases abstractas (en las que implementamos algunos métodos, pero otros no).
Nos falta ver el otro extremo: las clases «normales», que ya tienen que implementar
todo (lo que falte).
Profundizaremos en la herencia en el capítulo siguiente, pero, de momento, te
adelantaré que cuando una clase (abstracta o normal) implementa una interfaz,
utilizamos la palabra clave implements; pero cuando extiende de otra clase,
utilizamos la palabra clave extends.
public class ClaseEjemplo extends ClaseMadreEjemplo
implements InterfazEjemplo1, InterfazEjemplo2 {
}
Una clase implementa tantas interfaces como necesite, pero solo puede extender de
una clase. Lo veremos mejor en el ejemplo del sOOPer.
Las clases de sOOPer
Clase Caja
Partimos de una interfaz IContenedor implementada (en parte) por una clase
abstracta Contenedor, que ha dejado parte del trabajo pendiente para sus hijos que,
según enunciado y modelo conceptual, son dos y se llaman Caja y Bolsa.
Dentro del paquete contenedores creamos una clase Caja, que queremos que herede,
que extienda de Contenedor, es decir, que herede todas sus características y
comportamientos, pero que los extienda, si lo necesita con más cosas propias
de una Caja.
package [Link];
public class Caja extends Contenedor {
Si estás haciendo esto en tu Eclipse, da error. Sin mirar qué error da, ¿por qué lo da?
Porque le falta implementar algunos métodos. Hay que añadirlos, pues permitamos
que Eclipse nos ayude. Habíamos dejado pendiente decir de qué tipo era un
contenedor y cómo calcular su superficie. Normal que nos lo pida ahora.
¿De qué tipo es una caja? Pues "caja". Ya está.
@Override
public String getTipo() {
return "caja";
}
162 Clases y objetos
¿Y cómo se calcula la superficie de una caja? Ancho por largo, ¡fácil!
@Override
public int getSuperficie() {
return ancho * largo;
}
¡Pero esto aún no compila! No hemos definido ni lo que es ancho ni lo que es largo.
Pensemos un poco en ello. Cuando creamos una nueva caja en nuestro sistema (o
cuando vamos al proveedor de cajas y le compramos una nueva caja), ¿qué datos
hay que saber de esa caja? Sus medidas (alto, ancho, largo) y, además, cuando una
caja concreta va a un pedido, si queremos poder hacerle un seguimiento y no perderla
por el camino, precisaremos su referencia. En Java disponemos del constructor de
una clase para proporcionarle toda esa información de inicialización. El constructor
es un método un poquito especial. Se suele decir que es un método que se llama
como la clase, porque sería una cosa así:
public Caja() {
}
Pero a mí me gusta más verlo como un método sin nombre (por eso es especial) que
devuelve una Caja. Elige el punto de vista que te parezca más claro.
Ahora pondremos como parámetros de ese constructor lo que hay que saber de una
Caja. En el código del que partimos, ya hemos hablado de la referencia y el alto en
el padre (la clase Contendor) y, sin embargo, ancho y largo son nuevos. Luego,
algunos atributos serán propios de las cajas y deberemos manejarlos «a nivel interno»,
y otros los pasaremos al padre porque son comunes a todos los contenedores:
private int ancho;
private int largo;
public Caja(String referencia, int alto, int ancho, int largo) {
super(referencia, alto);
[Link] = ancho;
[Link] = largo;
}
• Creamos un atributo para el ancho y otro para el largo.
Si dentro del constructor escribimos ancho, Java y Eclipse entenderán que nos
referimos a los argumentos recibidos. Para hacer referencia a los atributos, cuando
el nombre colisiona con los argumentos, basta con aclarar que queremos el
[Link], es decir, el ancho de este objeto, el atributo. Lo mismo para largo.
La palabra reservada this hace referencia al propio objeto, por eso la utilizamos
para acceder a sus atributos.
Clases «normales» 163
TRUCO:
Si lo estás viendo en Eclipse, apreciarás que los colorea en azul, mientras que los
argumentos están en marrón. También se suele ver la asociación con un clic sobre una de
las palabras, pues nos sombrea sus referencias… Colores y formas de marcarlo dependen
de cada entorno, son una ayuda importante, pero también se puede programar sin ellos.
De hecho, en este libro impreso no se aprecian esos colores.
• Para la referencia y el alto hemos de pasárselos al padre, por su constructor, que aún
no hemos creado. Bueno, primero lo llamamos y ahora lo creamos: super es la forma
de referirnos al padre (en este caso, al método ese sin nombre). Nos da error porque
aún no lo hemos definido, dejemos a Eclipse que nos ayude y cree el constructor para
nosotros, en la clase Contenedor:
public Contenedor(String referencia2, int alto2) {
// TODO Auto-generated constructor stub
}
Aunque a veces parece que nos ayuda a caer. ¿Por qué pone esos doses? No los
necesitamos, ¡fuera! Nosotros ya sabemos distinguir argumentos de atributos:
public Contenedor(String referencia, int alto) {
[Link] = referencia;
[Link] = alto;
}
También los productos aparecen como atributos, pero eso no son valores iniciales de un
contenedor, sino que irá adquiriéndolos durante la ejecución del programa. Entonces,
ya está, guardamos Contenedor. Y vemos que Caja ya compila sin problemas.
Clase Bolsa
Crea la clase Bolsa, hermana de Caja e hija de Contenedor, e implementa los mismos
métodos que hemos hecho para la Caja. Ahora vemos el constructor y getSuperficie:
package [Link];
public class Bolsa extends Contenedor {
private int ancho;
public Bolsa(String referencia, int alto, int ancho) {
super(referencia, alto);
[Link] = ancho;
}
@Override
public String getTipo() {
return "bolsa";
}
@Override
public int getSuperficie() {
int radio = getDiametro() / 2;
return (int)([Link] * radio * radio);
}
164 Clases y objetos
private int getDiametro() {
return (int)((2 * ancho) / [Link]);
}
}
• getSuperficie de una bolsa. La superficie de un círculo es π radio cuadrado. Y radio
es la mitad del diámetro.
Podemos usar la constante PI que nos da Java en la clase Math, que no tenemos que
importar porque es del paquete [Link].
• getDiametro: para calcular el diámetro dependemos del ancho… Puede ser un
atributo de la Bolsa.
• Y ahora ya vamos con el constructor. ¿Qué nos define una bolsa? Alto, ancho y
referencia, entonces, esos serán los tres atributos del constructor.
Ya contamos con dos clases hijas de Contenedor: las Cajas y las Bolsas, con cosas
parecidas y con cosas propias.
Enumerados
Los enumerados, en Java, son un tipo especial de clases que sirve para agrupar una
serie de constantes. En el ejemplo de sOOPer que veremos a continuación se utiliza
la versión más básica de los enums, pero su potencial es mucho mayor, fuera del
alcance de este libro. Como pista, te digo que pueden tener atributos, constructores
(para dar valor a esos atributos) y métodos (para sacarles el máximo partido a esos
atributos). Te recomiendo el tutorial oficial de Java «Enum Types». Su ejemplo de los
días de la semana sería el equivalente al uso que abordaremos aquí, lo nuevo lo
descubrirás con los planetas.
Los enumerados de sOOPer
@Override
public String getTipo() {
return "bolsa";
}
En el código que hemos escrito hasta ahora para el sOOPer, tanto para las categorías
de los productos como para los tipos de los contenedores, usamos un atributo de
tipo String y, a la hora de implementarlo, lo escribimos «a pelo». Eso está muy feo
y, además, puede dar problemas de mantenimiento (por ejemplo, si necesitamos
escribirlo en otros sitios y en algunos ponemos la inicial en mayúscula y en otros
en minúscula, con o sin acentos, por no hablar de las erratas involuntarias).
La verdad es que nos vendría muy bien un tipo de datos cuyos valores posibles
fueran solamente esos que nos interesan, por ejemplo, para los tipos de contenedor,
algo que nos diga bolsa o caja. Podría ser un booleano: si es cierto es bolsa y, si no,
caja. ¿O era al revés? No, no es buena idea un booleano, es posible liarnos, y como
Enumerados 165
necesitemos añadir un tercer tipo de contenedor, ya tenemos un problema. Pero sí
podemos usar los enumerados en Java.
Empecemos creando un paquete para ellos, por eso de intentar mantener el código
ordenado. Creemos el paquete [Link] (también podríamos meter cada enum
dentro del paquete de la jerarquía en la que se usará, no es obligatorio esto del
paquete enums, pero he optado por hacerlo así).
Creamos un nuevo fichero, como hemos hecho con las clases, para crear el enum y
lo llamaremos TipoContenedor. ¿Qué tipos requerimos? Simplemente los
enumeramos, (¿enumeramos?, ¿para hacer un enum?, ¡todo tiene su lógica!).
NOTA:
Los valores de los enumerados siguen la misma convención de nombrado que las constantes,
es decir, todo en mayúsculas; y si tienen varias palabras, las separamos con un subrayado
o guion bajo para facilitar la legibilidad.
Escribimos todos nuestros valores, separados por comas, poniendo un punto y coma
al final.
Enumerado TipoContenedor
package [Link];
public enum TipoContenedor {
BOLSA, CAJA;
}
Enumerado Categoria
Ya que estamos, hacemos lo mismo con las categorías, aunque no las usaremos hasta el
próximo capítulo:
package [Link];
public enum Categoria {
ALIMENTACION, DROGUERIA, HIGIENE, MASCOTAS;
}
Uso de los enumerados
En IContenedor tenemos declarado un método getTipo que devuelve un String.
Pero como ahora lo haremos bien, cambiaremos ese String por TipoContenedor. En
el código, los enums se usan como si fueran cualquier otra clase o tipo de datos.
Como TipoContenedor no está en el paquete contenedores, hay que importarlo.
166 Clases y objetos
En estos momentos, todo deja de compilar. Es normal. Hemos cambiado la declaración
de un método en una interfaz que está implementada por una clase abstracta que
está extendida por un par de clases más. Normal que un cambio provoque problemas,
pero los corregimos en un pispás.
Contenedor no tiene problemas porque delegó eso en los hijos. Son Bolsa y Caja quienes
no compilan. Hagamos una cosa, yo arreglo Caja y luego tú arreglas Bolsa.
• El error está en el String de getTipo, hemos de cambiarlo por TipoContenedor. Si lo
haces a mano, has de añadir la importación; si no, Eclipse te puede ayudar.
• Ahora el problema está en la línea siguiente, porque estamos devolviendo un String y
no un TipoContenedor. Lo cambiamos y devolvemos [Link].
@Override
public TipoContenedor getTipo() {
return [Link];
}
¿Lo haces tú en Bolsa?
No olvides actualizar IProducto mediante la variación del valor de retorno de getCategoria:
Categoria getCategoria();
Objetos
Los objetos de sOOPer
Una vez tenemos montada la jerarquía de clases e implementados algunos de sus métodos
(puedes descargarte el código, si quieres), llega el momento de empezar a hacer funcionar
esto. Y lo primero que haremos, ahora que ya hemos representado nuestro mundo,
nuestro dominio, en clases, es crear objetos.
Clase Supermercado
Para ello, creamos una clase [Link] en la que meteremos un método main
y allí empezaremos «a jugar».
package sooper;
public class Supermercado {
public static void main(String[] args) {
}
}
Lo primero que queremos añadir en ese main es nuestro primer pedido, que es un IPedido,
que llamaremos miPedido y que será igual a new… ¡Ups! Error.
IPedido miPedido = new IPedido();
Objetos 167
Clase Pedido
Es imposible instanciar IPedido porque es una interfaz. Se requiere una clase que la
implemente. ¿No disponemos de una clase Pedido? Vaya, aún no, no pasa nada, creémosla
en un momento: Pedido implementa IPedido, y le pedimos al Eclipse que nos lo cree con
métodos y constructores pero sin main, también en el paquete sooper.
package sooper;
import [Link];
public class Pedido implements IPedido {
public Pedido() {
// TODO Auto-generated constructor stub
}
@Override
public String getReferencia() {
// TODO Auto-generated method stub
return null;
}
@Override
public Set<IProducto> getProductos() {
// TODO Auto-generated method stub
return null;
}
@Override
public Set<IContenedor> getContenedores() {
// TODO Auto-generated method stub
return null;
}
@Override
public void addContenedor(IContenedor contenedor) {
// TODO Auto-generated method stub
@Override
public IContenedor addProducto(IProducto producto) {
// TODO Auto-generated method stub
return null;
}
}
Todo este código lo genera el entorno de desarrollo a partir de lo que ya sabe. Como
Pedido implementa IPedido, el IDE ya sabe que tiene que preparar el esqueleto de todos
estos métodos. Pero no nos puede leer la mente, así que los atributos que precisemos los
pondremos nosotros. Vamos a pensar, ¿qué ha de tener un pedido? Una referencia y
contenedores. ¡Y productos!, me dirás. No, productos no, que esos ya los hay en los
contenedores. ¡El pedido tendrá un conjunto de contenedores llenos de productos!
• Añadimos los atributos:
private String referencia;
private Set<IContenedor> contenedores;
168 Clases y objetos
• Completamos el constructor, que recibirá una referencia que guardemos en el atributo
referencia. También inicializamos el conjunto de contenedores. Lo declaramos como
conjunto genérico (Set), pero como eso es una interfaz nos pasa lo mismo que con el
IPedido: hay que concretar un poco. En este caso he escogido la implementación
HashSet, no entraré en detalles sobre ello ahora, lo vemos en el capítulo 15, pero las
colecciones de Java son muy diversas e interesantes… Bueno, estábamos creando el
conjunto de contenedores vacío, para que cuando añadamos uno haya donde ponerlo:
public Pedido(String referencia) {
[Link] = referencia;
[Link] = new HashSet<>();
}
• Como hemos añadido el atributo referencia ya es posible implementar getReferencia,
que devolverá, cómo no, la referencia.
@Override
public String getReferencia() {
return referencia;
}
• getContenedores devolverá los contenedores:
@Override
public Set<IContenedor> getContenedores() {
return contenedores;
}
• addContenedor añadirá el contenedor recibido al conjunto de contenedores:
@Override
public void addContenedor(IContenedor contenedor) {
[Link](contenedor);
}
Con las cosas de productos ahora no nos metemos.
Estábamos en el main de Supermercado intentado crear nuestro primer pedido, y ahora ya
contamos con una clase concreta Pedido, a la que le hemos implementado todos los métodos.
IPedido miPedido = new Pedido();
Estábamos llamando al constructor de Pedido, sin pasarle parámetros, pero eso no
compila, porque el constructor de Pedido que hemos creado requiere una referencia.
Estamos preparando un pedido de ejemplo, así que le ponemos cualquier referencia:
IPedido miPedido = new Pedido("pedido001");
Si lo estás haciendo en tu Eclipse, ahora ya compila, no hay errores en rojo, pero hay
warnings en amarillo. Eclipse nos avisa de que hemos creado un objeto que no estamos
usando. Resolvamos esto ya mismo. Metamos un par de contenedores a ese pedido,
concretamente una bolsa de 40 x 25 cm y una caja de 30 por 50 por 75 cm. Primero las
creamos, luego las añadimos al pedido:
IContenedor bolsa1 = new Bolsa("B111", 40, 25);
IContenedor caja1 = new Caja("C222", 30, 50, 75);
[Link](bolsa1);
[Link](caja1);
Esperaremos al próximo capítulo para agregar productos a nuestro pedido.
Objetos 169
Los objetos en la máquina virtual de Java
Veamos un poco qué podría estar pasando dentro de la máquina virtual de Java.
Imaginemos el espacio de trabajo de Java como una pizarra.
IPedido miPedido = new Pedido("pedido001");
Figura 7.4. Un pedido vacío en la memoria de trabajo.
• Cuando creamos un nuevo objeto, se reserva espacio en memoria para él. En este
caso, al crear un pedido, dispondremos de espacio para la referencia y los contenedores.
En la referencia se guardará el valor recibido, contenedores será un puntero a un
HashSet de momento vacío, pues así lo hemos escrito en el constructor.
IContenedor bolsa1 = new Bolsa("B111", 40, 25);
[Link](bolsa1);
Figura 7.5. La memoria de trabajo tras crear el objeto bolsa1 y añadirlo al pedido.
170 Clases y objetos
• Cuando creemos la bolsa1, pasará más o menos lo mismo, se rellena el resto de los atributos,
pero productos se queda a nulo porque no lo hemos inicializado en el constructor.
• Cuando añadimos bolsa1 al pedido, «lo único» que pasa es que el conjunto de
contenedores que se generó al crear el pedido tendrá un puntero hacia el objeto bolsa.
Esto se interpreta como que el conjunto contiene la bolsa.
[Link](bolsa1);
IContenedor caja1 = new Caja("C222", 30, 50, 75);
Figura 7.6. La memoria de trabajo tras crear el objeto caja1 y añadirlo al pedido.
• El proceso al crear la caja1 es muy parecido al de la bolsa1.
• También el añadir la caja al pedido es lo mismo que la bolsa. Lo he puesto como 0 y
1 como si fueran posiciones de una secuencia, pero en verdad los conjuntos no siguen
ningún orden (a efectos lógicos, físicamente, estará ordenado).
No sé si habías oído eso de que en Java no hay punteros. A mí me lo dijeron, me lo creí
y me hizo mucha ilusión porque los punteros en C me costaban la vida. Pero mira, ¡me
engañaron! En este gráfico se ven unas cuantas flechas o punteros. Bueno, la verdad es
que no me engañaron tanto, porque la ventaja de Java respecto a C, en cuanto a punteros,
es que Java los maneja él solito y no da la guerra que da C. De cara al programador, Java T7.5
no tiene punteros, aunque internamente sí los haya.
Objetos 171
Test
Test 7.1. ¿Cuál sería el paquete correcto para el proyecto Avanza de la
empresa francesa Fraternité?
a) [Link]
b) [Link]
c) [Link]
Test 7.2. ¿Qué línea de código pondríamos en el lugar de la flecha?
public interfaz ICliente() {
-->
}
a) String getNombre() { return nombre; }
b) String getNombre();
c) String getNombre() { }
d) Ninguno de los tres.
Test 7.3. Ordena de más concreto a más abstracto estos tres conceptos: clase
abstracta, interfaz y clase «normal».
a) interfaz > clase abstracta > clase «normal»
b) clase «normal» > clase abstracta > interfaz
c) clase abstracta > clase «normal» > interfaz
d) interfaz > clase «normal» > clase abstracta
Test 7.4. En la clase Contenedor, ¿por qué hemos dejado sin implementar
getSuperficie()?
a) Porque según el tipo de contenedor, la superficie se calculará de una forma u otra. Lo implementaremos
en las clases hijas.
b) Porque son fórmulas muy complejas que no queremos ver en este libro.
c) Porque son fórmulas muy complejas que veremos en próximos capítulos.
d) Porque no es necesario implementar ese método, Java ya sabe calcular las superficies.
Test 7.5. Fíjate en las figuras 7.4 y 7.5. ¿Por qué en la figura 7.4 el atributo
contenedores apunta hacia un nuevo HashSet, pero en la figura 7.5
productos es null?
a) Porque Java lo inicializa automáticamente.
b) Porque hay una errata en esas figuras.
c) Porque los pedidos ya existen y los productos aún no.
d) Porque en el constructor de Pedido inicializamos contenedores, pero en el constructor de Bolsa no
inicializamos productos.
172 Clases y objetos
Soluciones
Test 7.1. ¿Cuál sería el paquete correcto para el proyecto Avanza de la
empresa francesa Fraternité?
c) [Link]: Suponiendo que el dominio web de la empresa sea [Link],
así que le damos la vuelta y añadimos el nombre del proyecto.
Test 7.2. ¿Qué línea de código pondríamos en el lugar de la flecha?
public interfaz ICliente() {
-->
}
b) String getNombre();: Dentro de una interfaz es imposible implementar el método,
solo se declara.
Test 7.3. Ordena de más concreto a más abstracto estos tres conceptos: clase
abstracta, interfaz y clase «normal».
b) clase «normal» > clase abstracta > interfaz: ¡Exacto! Las clases más normales son
concretas, mientras que las interfaces son abstractas.
Test 7.4. En la clase Contenedor, ¿por qué hemos dejado sin implementar
getSuperficie()?
a) Porque según el tipo de contenedor, la superficie se calculará de una forma u otra.
Lo implementaremos en las clases hijas: exactamente por eso.
Test 7.5. Fíjate en las figuras 7.4 y 7.5. ¿Por qué en la figura 7.4 el atributo
contenedores apunta hacia un nuevo HashSet, pero en la figura 7.5
productos es null?
d) Porque en el constructor de Pedido inicializamos contenedores, pero en el constructor
de Bolsa no inicializamos productos: nada que añadir.
Soluciones 173
8 Relaciones orientadas
a objetos
En este capítulo aprenderás a:
• Reutilizar código extendiendo clases mediante la herencia.
• Distinguir y sacar partido de la sobrecarga y la sobrescritura.
• Expresar un proceso mediante un diagrama de secuencia UML.
• Detectar y corregir problemas.
• Usar los modificadores de Java.
Introducción
En el capítulo anterior aprendimos los fundamentos de las clases y los objetos y,
aunque empezamos a ver alguna relación entre ellas, ahora profundizaremos en ese
aspecto. Seguiremos aprendiendo al ritmo del ejemplo del sOOPer.
La herencia entre objetos facilita la reutilización y la extensión del código. Las clases
hijas (que extienden de otra clase) heredan de la clase padre (la clase extendida)
todos sus comportamientos y atributos, y en ellas podemos añadir más (extensión)
o modificarlos (sobrescritura). Aprovecharemos el ejemplo para ver cómo repartir
las responsabilidades correctamente entre los objetos, alejándonos de cómo se hace
en la programación estructurada.
Herencia
En Java todas las clases heredan de Object, que es la clase padre, clase base o
superclase de todas. No importa si lo especificamos en el código o no, si en una clase
no ponemos el extends DeAlgunaClase, Java entiende que extiende de Object.
A partir de esa herencia obligatoria, podemos extender las clases según necesitemos.
Cada nivel de clases extiende del nivel superior, de forma que es posible añadirle
más propiedades o atributos y más métodos a la clase, además de heredar todas las
propiedades y métodos de la superclase.
Si tomamos como ejemplo una clase Perro que extiende de la clase Animal que
extiende a su vez de SerVivo, podemos decir del objeto milu que es un Perro, pero
también que milu es un Animal o un SerVivo. Y no solo lo diremos, sino que también
trataremos a milu como Perro, como Animal o como SerVivo, según nos convenga.
En Java solo se extiende de una clase. No existe la herencia múltiple. En otros
T8.1 lenguajes sí, pero en Java no. Sin embargo, en cuanto a la implementación de
interfaces, Java sí admite que implementemos varias. Por ejemplo, podríamos tener
una clase que fuera Clonable, Printable, Serializable, que son interfaces del API de
Java, puedes buscar su documentación para entender mejor qué estoy diciendo, pero
básicamente significa que existe la opción de obligar a esa clase a implementar
métodos de varias interfaces, esos métodos que la harían clonable, printable, serializable.
176 Relaciones orientadas a objetos
La herencia en sOOPer
Para el sOOPer, contamos con un buen ejemplo de estas herramientas en la jerarquía
de productos. Veamos el modelo conceptual para saber qué clases crearemos y qué
relaciones se establecerán entre ellas.
Figura 8.1. Modelo conceptual de productos.
Generamos una clase Producto, que implementará IProducto, que tal vez requiramos
que sea abstracta, y cuatro clases más que extenderán el Producto: Alimentacion, Mascotas,
Higiene y Drogueria. Además, tres clases extenderán de Alimentacion: Congelado, Fresco
y NoPerecedero. Todas ellas irán al paquete [Link].
Clase Producto
Creamos la clase Producto, que implementa la interfaz IProducto. Si utilizamos Eclipse
u otro IDE, al indicarle qué clase implementa, sabrá generar el esqueleto de todos los
métodos que necesitamos, pero sin implementar, solo retornado el valor más neutro
posible y con un comentario TODO para que no se nos olvide implementarlo de verdad.
package [Link];
import [Link];
import [Link];
public class Producto implements IProducto {
@Override
public String getReferencia() {
// TODO Auto-generated method stub
return null;
}
Herencia 177
@Override
public int getPeso() {
// TODO Auto-generated method stub
return 0;
}
@Override
public int getVolumen() {
// TODO Auto-generated method stub
return 0;
}
@Override
public Categoria getCategoria() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean esCompatible(IProducto p) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean tengoEspacio(IContenedor contenedor) {
// TODO Auto-generated method stub
return false;
}
@Override
public void meter(IContenedor contenedor) {
// TODO Auto-generated method stub
}
}
¡Cincuenta líneas y aún no hace nada! ¡Suerte que no nos cobran por línea! ¡Lástima que
tampoco nos paguen por ellas!
Lo que Eclipse no ha sabido añadirnos son los atributos y el constructor. Los métodos
getReferencia, getPeso y getVolumen nos sugieren sutilmente que hemos de tener tres
atributos: referencia, peso y volumen, y que la implementación de estos métodos será
devolver esos valores. Además, nos pide a gritos un constructor para inicializar bien los
Producto.
private String referencia;
private int peso;
private int volumen;
public Producto(String referencia, int peso, int volumen) {
[Link] = referencia;
[Link] = peso;
[Link] = volumen;
}
@Override
public String getReferencia() {
return referencia;
}
178 Relaciones orientadas a objetos
@Override
public int getPeso() {
return peso;
}
@Override
public int getVolumen() {
return volumen;
}
¿Y qué pasa con getCategoria? ¿Sabe un producto de que categoría es? ¿No es mejor que
eso se lo dejemos a los hijos? Tiene pinta. Borremos, pues, este método de la clase Producto,
hagamos la clase abstracta y obliguemos así a los hijos a definirse en una Categoria.
esCompatible también parece cosa de los hijos. Fuera.
El resto de los métodos, de momento, los dejamos pendientes.
Clase Alimentacion
Hacemos la clase Alimentacion, sin tilde, pues no es buena idea emplear caracteres
especiales al programar, que extiende de Producto. Utilizando Eclipse, también
indicaremos que nos genere los constructores del padre, todo ese trabajo que nos quitamos.
• En el constructor de Alimentacion solo llamaremos al constructor del padre pasándole
los parámetros tal cual los recibimos.
• En getCategoria devolveremos el String con la categoría, que es "alimentacion".
• El método esCompatible, lo dejaremos con el TODO, aún es pronto para implementarlo.
package [Link];
import [Link];
import [Link];
public class Alimentacion extends Producto {
public Alimentacion(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
@Override
public Categoria getCategoria() {
return [Link];
}
@Override
public boolean esCompatible(IProducto p) {
// TODO Auto-generated method stub
return false;
}
}
Hay que repetir el proceso para las otras tres hermanas de Alimentacion: Mascotas,
Higiene y Drogueria. Te recomiendo que lo intentes por tu cuenta, pero a continuación
veremos el código resultante.
Herencia 179
Clase Mascotas
package [Link];
import [Link];
import [Link];
public class Mascotas extends Producto {
public Mascotas(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
@Override
public Categoria getCategoria() {
return [Link];
}
@Override
public boolean esCompatible(IProducto p) {
// TODO Auto-generated method stub
return false;
}
}
Clase Higiene
package [Link];
import [Link];
import [Link];
public class Higiene extends Producto {
public Higiene(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
@Override
public Categoria getCategoria() {
return [Link];
}
@Override
public boolean esCompatible(IProducto p) {
// TODO Auto-generated method stub
return false;
}
}
Clase Drogueria
package [Link];
import [Link];
import [Link];
public class Drogueria extends Producto {
public Drogueria(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
180 Relaciones orientadas a objetos
@Override
public Categoria getCategoria() {
return [Link];
}
@Override
public boolean esCompatible(IProducto p) {
// TODO Auto-generated method stub
return false;
}
}
Clase Fresco
Ahora hay que implementar las clases hijas de Alimentacion, que son Fresco, Congelado
y NoPerecedero.
Extienden de Alimentacion, así que, como la clase madre ya tiene implementados todos
los métodos, de momento solo implementaremos el constructor. El resto de los métodos
se heredan de Alimentacion.
package [Link];
public class Fresco extends Alimentacion {
public Fresco(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
}
Aunque a continuación tienes el código de las clases Congelado y NoPerecedero,
inténtalo tú.
Clase Congelado
package [Link];
public class Congelado extends Alimentacion {
public Congelado(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
}
Clase NoPerecedero
package [Link];
public class NoPerecedero extends Alimentacion {
public NoPerecedero(String referencia, int peso, int volumen) {
super(referencia, peso, volumen);
}
}
¡Buen trabajo! Ya tenemos toda la jerarquía de clases de Producto creadas. Aún nos
quedan unos cuantos TODO en el código, pero vamos avanzando.
Herencia 181
Clase Supermercado (continuación)
Dejamos el main de la clase Supermercado con un Pedido con una Bolsa y una Caja,
pero sin Productos. ¡Ha llegado la hora de hacer la compra!
IProducto manzanas = new Fresco("MNZ", 1000, 1500);
IProducto helado = new Congelado("HLD", 800, 1000);
IProducto papelWC = new Higiene("PWC", 500, 2500);
IProducto peras = new Fresco("PER", 800, 1200);
Por cada producto indicaremos su referencia, su peso (en gramos) y su volumen (en cm3).
Y los añadiremos al pedido, y el pedido nos dirá, porque es muy listo, en qué Contenedor
los ha puesto:
IContenedor contManzanas = [Link](manzanas);
IContenedor contHelado = [Link](helado);
IContenedor contPapel = [Link](papelWC);
IContenedor contPeras = [Link](peras);
Estos no son contenedores nuevos. Serán la caja C222 o la bolsa B111, no hay ningún
new, no son objetos nuevos, sino «punteros» (aunque en Java no hay punteros) que nos
indican en cuál de los contenedores están esos productos.
Los objetos en la máquina virtual de Java (continuación)
Veamos el efecto de la creación de los productos en la memoria de trabajo. A partir de la
figura 7.6, estudiemos qué pasa con las manzanas:
IProducto manzanas = new Fresco("MNZ", 1000, 1500);
IContenedor contManzanas = [Link](manzanas);
Figura 8.2. La memoria de trabajo tras crear el objeto manzanas y añadirlo al pedido.
182 Relaciones orientadas a objetos
• Crear un producto, un kilo de manzanas en este caso, tampoco tiene mucho misterio.
• Misterio que sí sucede al añadir las manzanas al pedido. Aparte del proceso que aún
no hemos implementado de escoger en qué contenedor las meteríamos, supongamos
que irán a la bolsa. Los productos de la bolsa ya no serán nulos, sino un nuevo HashSet
con un elemento que apuntará al producto manzanas, que a su vez sabrá que está
metido en la bolsa y, por tanto, su contenedor tendrá como valor un puntero hacia
el objeto bolsa con referencia B111. Además, esa variable cManz que hemos declarado
en el main simplemente apuntará hacia B111, pero no implicará la creación de una
nueva bolsa, como tampoco se genera una nueva bolsa por arte de magia cuando en
el supermercado metemos dentro de la bolsa un kilo de manzanas. ¡Tanto la bolsa
como las manzanas existían antes!
• Lo mismo iría sucediendo con el resto de los productos que añadiéramos a nuestro pedido.
Sobrecarga y sobrescritura
Veamos dos conceptos cuyo nombre quizás nos confunda, pero que no son lo mismo. Me
refiero a la sobrecarga (overloading) y a la sobrescritura (overwriting).
Hablamos de sobrecarga cuando hay varios métodos con el mismo nombre, pero con
parámetros distintos. Por ejemplo, el método substring de la clase String. Si te fijas en la figura
8.3, sacada del API de la clase String de Java, se aprecian dos métodos llamados igual
(substring), pero uno recibe como parámetro un entero, beginIndex, índice de inicio, mientras
que el otro recibe dos enteros, además del beginIndex, recibe endIndex, índice de fin.
Figura 8.3. Métodos substring de la clase String.
Estos métodos nos devuelven una subcadena de la cadena de texto que reciben. En el
primer caso, nos devolverá la subcadena desde la posición recibida hasta el final, mientras
que el segundo tomará la subcadena entre las dos posiciones definidas.
En efecto, es recomendable, al aplicar la sobrecarga, que todos los métodos con un mismo
nombre realicen más o menos la misma función, como en este ejemplo. Si van a hacer
cosas diversas, les pondremos nombres distintos; pero si hacen lo mismo con matices de
diferencia, podemos utilizar la sobrecarga y aprovechar el mismo nombre.
Por otro lado, hallamos la sobrescritura, que es una técnica ligada a los mecanismos de
herencia. Una subclase (o clase hija) puede implementar una versión propia de un método
heredado de una de sus superclases. Se suele utilizar de la siguiente forma: la superclase
hace una implementación del método por omisión, bastante general, y luego, a medida
Sobrecarga y sobrescritura 183
que vamos bajando por la jerarquía las clases hijas, pueden ir implementando versiones
más ajustadas a sus necesidades.
T8.2
Es recomendable utilizar la anotación @Override para marcar los métodos que sobrescriben
la implementación de su clase padre. De esta forma, el entorno de desarrollo nos avisará
T8.3 si hacemos algún cambio no permitido en ella, por ejemplo, la lista de parámetros, el
tipo de retorno… que no es que podamos hacerlos, es que dejaría de ser una sobrescritura.
Sobrescritura de toString en el sOOPer
Todo queda más claro con un ejemplo, así que constatemos estos de forma práctica sobre
el proyecto del sOOPer. Si ejecutamos el main de la clase Supermercado, no veremos
nada, no ofrece nada por la consola.
Hay dos opciones para poder curiosear que está pasando:
• Escribir trazas que nos muestren los valores que nos interesan.
• Depurar e ir viendo «las tripas» de los objetos.
Lo primero es estudiar cómo hacerlo con las trazas. En un proyecto profesional jamás lo
realizaremos de esta forma: si requerimos poner trazas, recurriremos a alguna librería
de logging y estudiaremos muy bien qué escribimos en esos logs y a qué nivel, y dónde
se vuelcan (ficheros rotativos, por ejemplo), esta opción la aprenderemos en el capítulo
13. Pero como ahora lo que queremos aprender es programación orientada a objetos,
usaremos [Link] para visualizar en consola los valores que nos interesan.
Pero solo porque estamos «jugando». En un proyecto de verdad, jamás, repito, jamás
usaremos un [Link] para mostrar trazas.
Empezamos pintando el pedido en dos puntos del main: tras añadirle los contenedores
y los productos.
IPedido miPedido = new Pedido("pedido001");
IContenedor bolsa1 = new Bolsa("B111", 40, 25);
IContenedor caja1 = new Caja("C222", 30, 50, 75);
[Link](bolsa1);
[Link](caja1);
[Link]("Mi pedido con contenedores: " + miPedido);
IProducto manzanas = new Fresco("MNZ", 1000, 1500);
IProducto helado = new Congelado("HLD", 800, 1000);
IProducto papelWC = new Higiene("PWC", 500, 2500);
IProducto peras = new Fresco("PER", 800, 1200);
IContenedor contManzanas = [Link](manzanas);
IContenedor contHelado = [Link](helado);
IContenedor contPapel = [Link](papelWC);
IContenedor contPeras = [Link](peras);
[Link]("Mi pedido con productos: " + miPedido);
Y ejecutaremos para verlo todo bien…
184 Relaciones orientadas a objetos
Mi pedido con contenedores: [Link]@70dea4e
Mi pedido con productos: [Link]@70dea4e
¡Qué es esto! ¡Vaya decepción! ¡Esto no nos dice nada! Bueno, bueno, calma… no es muy
bonito y ahora lo arreglamos, pero sí nos está dando información y en algunos casos
puede ser hasta interesante.
La primera línea empieza por [Link], que es el nombre completo de la clase del
objeto que estamos imprimiendo. Aunque lo declaramos como IPedido (que es una
interfaz), al crearlo, al hacer el new, lo hicimos de Pedido, que es una clase. Así que, de
momento, ya nos dice de qué clase es nuestro objeto. Algo es. Además, detrás de la arroba
aparece un «churro» de números y letras, más concretamente la representación hexadecimal
del código hash de ese objeto, que suena muy complicado, pero lo simplificamos como
un código que será diferente para cada objeto, a modo de identificador interno.
Si nos fijamos en la segunda línea, la salida es exactamente la misma (salvo el texto que
hemos escrito nosotros para distinguir las líneas). Eso significa que continúa siendo el
mismo objeto.
Vamos a agregar la impresión de los dos contenedores para verlo más claro. Añadimos
este par de líneas a la creación de los contenedores:
[Link]("Bolsa: " + bolsa1);
[Link]("Caja: " + caja1);
Y, al ejecutar, obtenemos este resultado:
Bolsa: [Link]@7852e922
Caja: [Link]@4e25154f
Mi pedido con contenedores: [Link]@70dea4e
Mi pedido con productos: [Link]@70dea4e
Esta vez, cada objeto tiene su clase (observa que ahora el nombre es más largo porque
existen dos niveles de paquete) y cada objeto tiene su código.
Pero, aunque todo esto sea muy interesante, estamos de acuerdo en que no era la
información que esperábamos. En Java, cuando concatenamos un objeto a un String,
como estamos haciendo en nuestros [Link], el compilador añade automáticamente
una llamada al método toString. Este es un método de la clase objeto, que nos devuelve
lo que acabamos de ver. Si nosotros queremos que la conversión a String de una de
nuestras clases sea distinta, hemos de sobrescribir este método.
Vamos a la clase Pedido y le sobrescribiremos el método toString.
TRUCO:
Es posible escribir todo nosotros o pedirle a Eclipse que nos ponga el esqueleto. Si empezamos
a escribir toS y le damos a Ctrl-espacio (o la combinación equivalente en tu sistema), nos
sugerirá autocompletar con toString y sobrescribir ese método de la clase objeto.
Sobrecarga y sobrescritura 185
@Override
public String toString() {
// TODO Auto-generated method stub
return [Link]();
}
Además de la típica línea con el comentario del TODO para que nos acordemos de
hacerlo, la implementación por defecto es return [Link](). Esto significa que
este método devolverá (hasta que cambiemos el código, claro) lo que devuelva el
método toString del padre, que se llama usando super. Es decir, que esta implementación
que nos sugiere Eclipse, básicamente, hace nada. Pongamos lo que deseamos. Quiero
que nos devuelva la referencia y los contenedores, ¿te parece bien?
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
[Link]("Pedido: " + referencia + "\n");
for (IContenedor contenedor : contenedores) {
[Link]("\t" + contenedor + "\n");
}
return [Link]();
}
Pintaremos la palabra «Pedido: », seguida de la referencia, un salto de línea y, luego,
cada uno de sus contenedores. StringBuilder es una clase que nos facilita Java para
optimizar la construcción de Strings hechas con concatenación de otros elementos.
Como estamos «jugando», también podríamos haberlo hecho concatenando Strings,
con el signo +, pero bueno, así es más correcto. ¿Lo probamos?
Guardamos Pedido y volvemos a ejecutar el main de Supermercado. Fíjate en que en
Supermercado no hemos tocado nada ahora.
Bolsa: [Link]@7852e922
Caja: [Link]@4e25154f
Mi pedido con contenedores: Pedido: pedido001
[Link]@7852e922
[Link]@4e25154f
Mi pedido con productos: Pedido: pedido001
[Link]@7852e922
[Link]@4e25154f
La salida de contenedores es igual, normal, pues no hemos modificado su
implementación. Y luego en la tercera línea, ya nos sale lo que acabamos de preparar.
Pone «Pedido», pone la referencia y luego la lista de contenedores metidos en el
pedido. Constata que cada vez que pinta la bolsa, por ejemplo, sale con el mismo
código tras la arroba, porque solo tenemos una bolsa, esté fuera o dentro del pedido.
Pero sigue quedando feo. ¿Cómo lo resolvemos? Fácil. Sobrescribiendo el toString
de los contenedores. ¿De Contenedor o de Caja y Bolsa? Bueno, cuanto más arriba
lo hagamos, menos trabajamos, así que sobrescribamos toString en Contenedor. En
186 Relaciones orientadas a objetos
IContenedor no podemos sobrescribirlo porque es una interfaz, y las interfaces no
admiten implementación. Contenedor es una clase abstracta, pero ahí sí es posible
poner código. Como ya has visto como lo he hecho yo en Pedido, puedes intentarlo
tú o seguir leyendo.
public String toString() {
StringBuilder sb = new StringBuilder("Contenedor " + referencia + " ["
+ getTipo()
+ "] (sup " + getSuperficie() + "cm2 - vol " + getVolumen()
+ "cm3 - resistencia " + getResistencia() + " g).\n");
if ([Link]()) {
[Link]("\t\tvacío\n");
}
for (IProducto p : productos) {
[Link]("\t\t" + p + "\n");
}
[Link]("\t\t>> Disponible vol " + volumenDisponible() + "cm3");
return [Link]();
}
Yo he pintado todos los datos del contenedor, seguido de los productos (atributo que
he tenido que crear). ¿Vemos qué sale? Ejecutamos el main de Supermercado:
Exception in thread "main" [Link]
at [Link]([Link])
at [Link]([Link])
at [Link]([Link])
at [Link]([Link])
¡NullPointerException! ¿A ver qué ha pasado? Si estás usando Eclipse, puedes clicar en
el link; si no, ve manualmente a la clase Contenedor línea 62. Ahí se aprecia que
intentamos acceder a productos, pero es un conjunto sin inicializar. ¡Vale! Es que en el
constructor de Pedido sí inicializamos el conjunto, pero en Contenedor no. Arreglémoslo.
public Contenedor(String referencia, int alto) {
[Link] = referencia;
[Link] = alto;
productos = new HashSet<>();
}
Y volvemos a intentarlo:
Bolsa: Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 0 g).
vacío
>> Disponible vol 0cm3
Caja: Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
vacío
>> Disponible vol 0cm3
Mi pedido con contenedores: Pedido: pedido001
Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 0 g).
vacío
>> Disponible vol 0cm3
Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
vacío
>> Disponible vol 0cm3
Sobrecarga y sobrescritura 187
Mi pedido con productos: Pedido: pedido001
Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 0 g).
vacío
>> Disponible vol 0cm3
Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
vacío
>> Disponible vol 0cm3
¡Ahora sí! Como aún no hemos implementado el meter productos en los contenedores,
tanto antes como después seguimos con el pedido idéntico. Pero, al menos, ya nos saca
información interesante, y hemos aprendido a sobrescribir un método.
Algoritmo de distribución de productos en contenedores
en sOOPer
Partimos de una estructura de clases que refleja ya el modelo conceptual diseñado.
Incluye interfaces, clases y clases abstractas, incluso enumerados. Existen unos cuantos
métodos implementados: algunos en las clases hijas, otros en las clases abstractas, pero
también hay un montón de métodos aún con su TODO. Tenemos unos cuantos objetos
instanciados y relaciones entre ellos. El código actual permite crear pedidos, contenedores
y productos; añadir contenedores a los pedidos; y visualizar que agregamos productos
al pedido, pero eso aún no hace nada porque no está implementado.
Método addProducto de Pedido en sOOPer
Descubramos el potencial de la orientación a objetos al implementar el método addProducto
del Pedido. El Pedido no sabe en qué contenedor conviene meter o no un producto. Por
eso, recorreremos todos los contenedores que tenemos hasta que hallemos uno en el que
meter el producto en cuestión.
@Override
public IContenedor addProducto(IProducto producto) {
for (IContenedor contenedor : contenedores) {
if ([Link](producto)) {
return contenedor;
}
}
return null;
}
• Entonces, empezamos por un foreach que recorra los contenedores.
• Dentro del bucle, comprobamos si el contenedor con el que estamos trabajando
admite o no el producto que queremos añadir. Llamaremos al método meter del
contenedor, que nos devolverá un booleano indicando si lo ha metido o no.
• Si nos devuelve cierto, ya hemos encontrado el contenedor, y lo devolvemos, pues
addProducto se comprometió (lo dice su declaración) a devolver el contenedor en
el que hemos colocado el producto.
188 Relaciones orientadas a objetos
• Si nos devuelve falso, continuamos en el bucle con el siguiente contenedor, a ver si
tenemos más suerte.
• Si terminamos con todos los contenedores sin haber localizado sitio para el producto,
devolvemos null. Si estuviéramos documentado el código, usando javadoc (que
deberíamos, pero no lo estamos haciendo), explicaríamos ese caso, para que quien
llame a nuestro método sea consciente de que puede recibir un null como resultado.
¡Y ya lo tenemos! Pedido ha implementado addProducto. Y me dirás… ¡pero si no hemos
aplicado ninguna de las reglas de los requisitos! Efectivamente. Pedido no sabe cómo
son las relaciones entre contenedores y productos, eso es cosa suya.
Si ejecutamos, sigue sin añadir productos al pedido ni a los contenedores. Porque no
hemos implementado el método meter, y como lo tenemos con la implementación por
defecto que nos hace el Eclipse, mucho me temo que dice siempre que no.
Método meter de Contenedor en sOOPer
Implementamos el método meter de la clase Contenedor. En principio, supongo que lo
que hagamos nos valdrá tanto para cajas como para bolsas, pero eso de momento no me
preocupa, paso a paso iremos viendo qué pasa.
Pensemos primero qué criterio debe seguir un contenedor para decidir si acepta o
no un producto:
• El producto tiene que caber en el contenedor (no pondremos el saco de comida para
el perro en una bolsita de farmacia, ¿no?).
• El contenedor ha de resistir el peso del producto (no guardaremos botellas de cristal
en bolsas de esas finísimas).
• El producto debe ser compatible con el resto de productos que ya están en ese contenedor.
Paso a paso. Hagamos ese esqueleto:
@Override
public boolean meter(IProducto producto) {
boolean resistenciaOk;
boolean volumenOk;
boolean compatibilidadOk;
boolean acepta = resistenciaOk && volumenOk && compatibilidadOk;
if (acepta) {
[Link](producto);
[Link](this);
}
return acepta;
}
• Declaramos los tres booleanos de los que hemos hablado, y otro que sea la suma
(and) de todos. Diremos que un contenedor acepta un producto si resiste su peso, si
cabe por volumen y si hay compatibilidad entre productos. Y si lo acepta, haremos
dos cosas: por un lado, añadir el producto al conjunto de productos del contenedor
Sobrecarga y sobrescritura 189
y, por otro, informar al producto que ha sido metido en un contenedor. ¿En cuál? En
este, this. Recuerda que this es el propio objeto.
Este fragmento de código no compila porque estamos intentando usar tres booleanos
para calcular el cuarto sin haberlos inicializado antes. No existen en memoria aún.
Si depuráramos, no los veríamos aún en la lista de variables que nos muestra el
depurador y, por consiguiente, no podemos usarlos. Pero no quiero inicializarlos en
vano porque ya los implementaremos de verdad.
• ¿Cómo sabemos si la resistencia es ok? Pues llamamos al método resiste del
contenedor, pasándole el producto.
boolean resistenciaOk = resiste(producto);
• ¿Cómo sabemos si el volumen es ok? Le preguntamos al producto si tiene espacio
en este contenedor.
boolean volumenOk = [Link](this);
Seguimos teniendo pendiente la implementación de esos métodos, pero ya llegará.
• ¿Cómo sabemos si un producto es compatible con los productos que ya hay en el
contenedor? Recorreremos con un bucle todos los productos que hay ya en el contenedor
y comprobaremos si son compatibles con el producto que deseamos guardar ahora.
boolean compatibilidadOk = true;
for (IProducto p : productos) {
boolean compatibleOk = [Link](p);
compatibilidadOk &= compatibleOk;
}
Solo si el nuevo producto es compatible con todos los demás saldremos del bucle con la
compatiblidadOk cierta. ¿Y si aún no hay productos? No entraremos en el bucle, pero
compatiblidadOk la hemos inicializado a cierto. Ahora ya nos compila el código. No
quedan errores. Puedes probar si funciona.
¿Realmente esperabas que ya funcionara? ¡Pero si aún no hemos implementado los
métodos que toman las decisiones de verdad! Fíjate en que todavía no hemos necesitado
saber cuándo son compatibles dos productos o cuándo un producto tiene espacio o
cuándo un contenedor resiste. Eso son detalles, pero sí hemos implementado una parte
importante del algoritmo.
Reparto de responsabilidades
Ahora veremos el potencial del reparto de responsabilidades en la orientación a objetos.
Al igual que cada persona conoce su fecha de nacimiento y somos capaces de decir
nuestra edad, cuando se nos pregunta (a veces echando cuentas de en qué año estamos
y en qué año nacimos), salvo que queramos que esa persona se acuerde de hacernos un
190 Relaciones orientadas a objetos
buen regalo por nuestro cumpleaños, solo le damos la edad y no la fecha completa de
nacimiento. Dando solo la edad no estamos ofreciendo al otro nuestros datos personales,
no le damos más detalles de los necesarios.
Queremos imitar ese comportamiento en nuestros objetos. Cada uno conoce sus atributos
y, en vez de ir mostrando al mundo sus intimidades y sus secretos, es mejor que cada
objeto use esos atributos para resolver por sí mismo lo que le preguntan.
Reparto de responsabilidades en sOOPer
En el ejemplo del sOOPer, el algoritmo para añadir productos a los contenedores de un
pedido está bastante perfilado. Pero no hace nada, no funciona, porque nos falta que los
responsables finales se mojen y decidan si dos productos son compatibles o si un producto
cabe en un contenedor o si un contenedor resiste el peso de un producto.
El método meter de la clase Contenedor nos quedó así:
@Override
public boolean meter(IProducto producto) {
boolean resistenciaOk = resiste(producto);
boolean volumenOk = [Link](this);
boolean compatibilidadOk = true;
for (IProducto p : productos) {
boolean compatibleOk = [Link](p);
compatibilidadOk &= compatibleOk;
}
boolean acepta = resistenciaOk && volumenOk && compatibilidadOk;
if (acepta) {
[Link](producto);
[Link](this);
}
return acepta;
}
Ya lo hemos terminado, no hace falta tocarlo, pero presta atención a los métodos que usa
para ir comprobando si están listos o hay que implementarlos, y me temo que ¡habrá
que implementarlos todos!
Lo primero que hace meter es comprobar la resistencia, llamando al método resiste, que
se halla justo debajo en la misma clase, con su TODO autogenerado por Eclipse. Llegó
el momento de que los contenedores se mojen y nos digan si resisten un producto o no.
Bueno, mejor que se mojen figuradamente porque como se mojen de verdad… bueno,
eso afectaría a las cajas de cartón o a las bolsas de papel, porque los contenedores de
plástico sí se pueden mojar, ¿no? A ver, Mariona, deja de divagar, que ¡el material de los
contenedores no entra en este proyecto! ¿Pero te das cuenta de que podría requerir
expandir la jerarquía?
Reparto de responsabilidades 191
Método resiste de Contenedor en sOOPer
Un contenedor resiste un producto si su resistencia es mayor que el peso del producto.
Fácil, ¿no? ¡Ya está! ¡Un TODO menos!
@Override
public boolean resiste(IProducto producto) {
return resistencia > [Link]();
}
¡O quizá no del todo! En los requisitos del proyecto (vistos al principio del capítulo 6),
nos decía algo así como:
«Las cajas son rectangulares y aguantan “cualquier peso”, mientras que las bolsas
tienen una resistencia máxima».
Si vienes de la programación estructurada quizá te acaba de pasar por la cabeza una
solución parecida a meter un if dentro de resiste, y si es una caja, return true y, si no, ya
miramos esto de la resistencia. ¡Mal! Estamos en orientación a objetos y existen dos
soluciones. Podemos quitar el método resiste de Contenedor y obligar a cada uno de los
hijos a implementarlo según sus condiciones, pero también asumiremos que lo que hemos
implementado hasta ahora es el comportamiento «normal» de cualquier contenedor y,
por tanto, podemos dejarlo en la clase padre Contenedor y solo implementar en Caja su
comportamiento específico. Escojamos esta segunda opción, sobre todo, para mostrarla,
pues quizá es menos evidente que la anterior.
Método resiste de Caja en sOOPer
Implementemos el método resiste, pero esta vez de la Caja:
@Override
public boolean resiste(IProducto producto) {
return true;
}
¿Cuándo resiste una caja? Según las especificaciones de nuestro cliente, siempre. Pues
nada, return true.
En la clase Bolsa no se requiere implementar el método resiste, porque lo hereda de
Contenedor, y esa versión ya le vale.
Método tengoEspacio de Producto en sOOPer
Volvemos al Contenedor. Después de comprobar la resistencia, nos cercioramos del
volumen. Ahora toca implementar el método tengoEspacio de la clase Producto. Vamos
a deshacernos de otro TODO:
@Override
public boolean tengoEspacio(IContenedor contenedor) {
return [Link]() > volumen;
}
192 Relaciones orientadas a objetos
Un producto tendrá espacio en un contenedor si su volumen es más pequeño que
el volumen que le queda disponible en el contenedor (si ya hay productos en un
contenedor puede que el nuevo ya no quepa). Así que aquí tenemos un poco de lío
de responsabilidades, para decidir esto se requiere conocer el volumen disponible
del contenedor y el volumen que necesita el producto. Quizá podríamos haberlo
resuelto al revés. Puedes intentarlo si quieres. Pero, en cualquier caso, presta atención
al hecho de que al contenedor le preguntamos por su volumen disponible, no le
estamos preguntando su volumen total ni el de los productos que ya contiene.
Acuérdate: damos nuestra edad, no la fecha de nacimiento.
Método volumenDisponible de Contenedores en sOOPer
Apreciemos ese método volumenDisponible de los contenedores.
@Override
public int volumenDisponible() {
return getVolumen() - volumenOcupado();
}
private int volumenOcupado() {
int res = 0;
for (IProducto p : productos) {
res += [Link]();
}
return res;
}
Volumen disponible será el volumen del contenedor menos el volumen ocupado, que lo
calcularemos en un método privado sumando el volumen de cada uno de los productos
del contenedor.
Métodos esCompatible en la jerarquía de Productos en sOOPer
Ya solo nos queda resolver la compatibilidad entre productos para saber si es posible
meter el producto en el contenedor. Recordemos la regla de compatibilidad:
«Los productos de alimentación no pueden ser mezclados con los de las otras
categorías. Los productos de higiene no se pueden mezclar con los de alimentación,
y los de droguería, ni con los de alimentación ni con los de mascotas».
Bueno, parece que la clase Producto en sí tiene poco que decir al respecto y le tocará
delegar a cada clase hija. No hay problema. En la clase Producto no se requiere
implementar el método esCompatible. Lo haremos en las clases hijas.
Método esCompatible de Alimentacion en sOOPer
Los productos de Alimentacion no pueden ser mezclados con los de las otras categorías,
es decir, nos cercioramos de que la categoría del producto con el que estamos comprobando
la compatibilidad es alimentación:
Reparto de responsabilidades 193
@Override
public boolean esCompatible(IProducto p) {
return [Link]([Link]());
}
Método esCompatible de Higiene en sOOPer
Para los productos de Higiene, justo lo contrario, nos vale cualquier producto, salvo los
de alimentación:
@Override
public boolean esCompatible(IProducto p) {
return );
}
Método esCompatible de Drogueria en sOOPer
Luego vamos a Drogueria:
@Override
public boolean esCompatible(IProducto p) {
return )
&& );
}
Método esCompatible de Mascotas en sOOPer
Los productos para las Mascotas no son compatibles con los de droguería:
@Override
public boolean esCompatible(IProducto p) {
return [Link]([Link]());
}
Método meter de Producto en sOOPer
Ya tenemos toda la implementación necesaria para decidir si un producto puede ser
metido en un contenedor o no. Pero nos falta todavía informar al producto de en qué
contenedor ha sido guardado. Vamos al método meter de Producto.
Simplemente añadimos un atributo contenedor (del tipo IContenedor) a Producto y en
el método meter le damos como valor a ese atributo lo recibido como argumento:
private IContenedor contenedor;
@Override
public void meter(IContenedor contenedor) {
[Link] = contenedor;
}
Bueno, ya lo tenemos, el Producto ya sabe en qué contenedor va. Probémoslo:
Mi pedido con productos: Pedido: pedido001
Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 0 g).
vacío
>> Disponible vol 6120cm3
Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
[Link]@5c647e05
[Link]@33909752
[Link]@70dea4e
>> Disponible vol 108800cm3
194 Relaciones orientadas a objetos
¡Ahora sí! Nos ha quedado la bolsa vacía, y todo metido en la caja… ¿Sospechas por qué?
Luego lo miramos.
Diagrama de secuencia
Los diagramas de secuencia UML sirven para representar el algoritmo de un método.
No se suelen hacer para todos los métodos, pero quizá sí para los más importantes o
complejos del proyecto. Lo suyo es hacerlo antes del desarrollo, de forma que se le pueda
pasar a un programador que lo programe. Pero la verdad es que, al menos en la mayoría
de proyectos en los que he participado, si se hace, suele ser a posteriori. Ahí también tiene
su utilidad, principalmente como documentación del proyecto, pero, claro, ¡no hay que
olvidarse de mantenerlo actualizado!
Diagrama de secuencia del método addProducto
Hagamos un boceto, sin exactitud académica, del método addProducto que permite
añadir un producto a alguno de los contenedores del pedido.
Figura 8.4. Diagrama de secuencia del método addProducto.
• Empezamos pintando el objeto supermercado desde el que haremos la llamada.
En los diagramas de secuencia, los objetos se representan con un recuadro con el nombre
y/o la clase del objeto, del que cuelga una línea discontinua, a modo «línea de vida».
Sobre esta línea, un rectángulo estrecho y largo irá marcando la duración del método o
de sus partes.
Diagrama de secuencia 195
• Desde Supermercado llamamos al método addProducto de un Pedido.
La llamada a un método se representa con una flecha que sale del rectángulo del
llamante y que apunta hacia un nuevo rectángulo, en este caso en el objeto destino.
• Dentro del método addProducto, iteramos sobre todos los contenedores del pedido.
Y por cada uno de ellos, llamamos al método meter de dicho contenedor.
Los bucles y otras estructuras se representan con un gran rectángulo que abarque
todo lo que se hará en ese bucle, indicando en una etiqueta en la esquina superior
izquierda el tipo de flujo.
• En el método meter de Contenedor, lo primero es comprobar si el contenedor resiste
al producto. El método resiste también es un método de Contenedor.
Cuando el método a llamar está en la misma clase, nos vale con retorcer un poco la flecha.
• El segundo paso de meter le pregunta al producto si tiene espacio en el contenedor, para
lo cual este último necesita a su vez preguntarle al contenedor el volumen disponible.
• La tercera comprobación es la de compatibilidad. Contenedor debe iterar sobre todos
los productos que ya contiene, y corroborar uno a uno si el producto a añadir
esCompatible con los ya añadidos.
Si tenemos un bucle dentro de otro bucle, pues pintamos un rectángulo de bucle,
dentro del rectángulo de bucle que ya tenemos.
• Finalmente, si las tres comprobaciones salen bien, Contenedor agrega a su conjunto
de productos el producto candidato.
• Y termina notificando a dicho producto en qué contenedor se ha metido.
En efecto, esta es una versión simplificada del diagrama, pues la potencia de UML podría
habernos llevado a un diagrama casi ilegible.
Y con este diagrama hemos conseguido representar en una sola pantalla todo el proceso
de añadir productos al pedido, a pesar de que en el código hemos distribuido las
T8.4 responsabilidades entre varias clases y un montón de métodos.
Dedica unos minutos a entender cómo hemos implementado este proceso de añadir
T8.5 productos a los contenedores. Es importante asimilarlo para fundar una buena base de
programación orientada a objetos en tu cabeza.
Detección y corrección de problemas
Me gustaría decirte que el mundo de la programación es mágico y que todo sale bien a
la primera y los programadores somos felices. Pero, si quieres ser un programador feliz,
has de aceptar que los errores forman parte de nuestra profesión y que hay que saber
196 Relaciones orientadas a objetos
lidiar con ellos: detectarlos, encontrarlos y corregirlos. En la parte 3 de este libro
profundizaremos en estos asuntos y aprenderemos técnicas profesionales para lograrlo,
pero, mientras tanto, necesitamos que el sOOPer funcione bien.
Detección y corrección de problemas en sOOPer
Desde la clase Supermercado estamos creando un pedido, con una bolsa y una caja, y
cuatro productos, de distintos tipos. Al ejecutar, de momento, vemos que:
• La bolsa se quedó vacía.
• La caja solo tiene tres productos, un congelado y dos frescos, es decir, que el papel
higiénico parece que se ha quedado fuera.
Puede ser un resultado válido, pero nos resulta sospechoso, así que mejor asegurarnos
de que todo está bien.
Problema de la bolsa vacía en sOOPer
Hay un contenedor que se queda vacío, así que debemos estudiar el método que agrega
productos a los contenedores para comprobar si hay algún problema ahí. Clase Pedido,
método addProducto:
@Override
public IContenedor addProducto(IProducto producto) {
for (IContenedor contenedor : contenedores) {
if ([Link](producto)) {
return contenedor;
}
}
return null;
}
Este método itera sobre todos los contenedores, entre los que está el objeto bolsa, y por
cada uno de ellos se llama al método meter del contenedor:
@Override
public boolean meter(IProducto producto) {
boolean resistenciaOk = resiste(producto);
boolean volumenOk = [Link](this);
boolean compatibilidadOk = true;
for (IProducto p : productos) {
boolean compatibleOk = [Link](p);
compatibilidadOk &= compatibleOk;
}
boolean acepta = resistenciaOk && volumenOk && compatibilidadOk;
if (acepta) {
[Link](producto);
[Link](this);
}
return acepta;
}
Detección y corrección de problemas 197
Este método va comprobando todas las condiciones para meter un producto en un
contenedor y, en función de eso, decide. Primera comprobación, la resistencia. Veámoslo.
Tomamos el método resiste de Contenedor, pues Bolsa no tiene implementación propia.
Gracias al polimorfismo, Java sabrá escoger qué método resiste debe ejecutar.
@Override
public boolean resiste(IProducto producto) {
return resistencia > [Link]();
}
¿Recuerdas cuándo hemos inicializado la resistencia de un contenedor? De hecho, en la
salida de la ejecución en la que hemos visto que la bolsa está vacía, también apreciamos
que la resistencia de la caja y de la bolsa es de «0 g». ¡Va a estar aquí el problema! No
hemos establecido una resistencia, así que, si una bolsa tiene resistencia cero, jamás podrá
ser estrictamente mayor que el peso del producto (que será positivo). Para las cajas no
nos importa, porque la clase Caja ha sobrescrito el método resiste y no toma en cuenta
el atributo resistencia. Ya habíamos detectado un posible problema y acabamos de localizar
dónde está. Son dos pasos muy importantes. Nos queda el tercero: corregirlo.
Corrección del constructor de Bolsa
Nos aseguramos de la implementación de Bolsa y, en efecto, ¡no estamos dándole por
ningún lado la información sobre la resistencia! Vaya despiste. Añadamos un parámetro
más al constructor y pasémoselo al padre.
public Bolsa(String referencia, int alto, int ancho, int resistencia) {
super(referencia, alto, resistencia);
[Link] = ancho;
}
Corrección del constructor de Contenedor
Como es evidente, nos da error porque el padre no espera recibir ese parámetro.
Pongámoselo también. El padre de Bolsa es Contenedor:
public Contenedor(String referencia, int alto, int resistencia) {
[Link] = referencia;
[Link] = alto;
[Link] = resistencia;
productos = new HashSet<IProducto>();
}
Como ya habíamos definido el atributo resistencia, incluso lo estábamos usando, no hace
falta declararlo ahora.
Corrección del constructor de Caja
Aunque Caja no necesita el dato de la resistencia, en el constructor de Caja estamos
llamando al constructor de Contenedor que acabamos de modificar, así que también
hemos de arreglarlo. Como dijimos que la caja no tenía resistencia, es posible pasarle un
cero y despreocuparnos.
198 Relaciones orientadas a objetos
public Caja(String referencia, int alto, int ancho, int largo) {
super(referencia, alto, 0);
[Link] = ancho;
[Link] = largo;
}
Corrección del main de Supermercado
Ahora, en la clase Supermercado, está fallando la llamada al constructor de Bolsa, porque
tenemos que pasarle el nuevo parámetro, la resistencia de la bolsa. Le damos una
resistencia de 900 gramos.
IContenedor bolsa1 = new Bolsa("B111", 40, 25, 900);
En el caso de la caja, como no hemos cambiado la lista de parámetros de su constructor,
no hay nada que variar.
Volvamos a ejecutar para ver cómo queda ahora nuestro pedido.
Mi pedido con productos: Pedido: pedido001
Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 900 g).
[Link]@5c647e05
[Link]@33909752
>> Disponible vol 3920cm3
Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
[Link]@70dea4e
>> Disponible vol 111000cm3
Observa que la resistencia de la bolsa ya no es de «0 g». Ahora se reparten los productos
entre la bolsa y la caja, pero el papel higiénico… nada, no hay forma, sigue sin aparecer.
Problema del papel higiénico en sOOPer
El segundo posible problema que detectamos en el sOOPer es el del papel higiénico,
que parece que no está ni en ninguno de los contenedores del pedido. Cuando
aprendamos a depurar, lo podremos comprobar depurando, pero ahora que estás
dando tus primeros pasos en el mundo de la programación es interesante que también
practiquemos la ejecución «a mano». Ejecutemos el código a mano, mentalmente,
leyendo el código, sin ejecutarlo de verdad. Si tienes el código en pantalla te será
más fácil seguir este ejercicio.
Si vemos el método main de Supermercado, el objeto «papelWC» es el tercer producto
que intentamos meter en el pedido, por detrás de las manzanas y el helado. Aunque el
toString de los productos no lo hayamos sobrescrito y nos esté dando poca información
en la salida, es posible intentar averiguar qué productos hay en cada contenedor.
El helado es un producto Congelado, y vemos fácilmente que ha sido metido en la
Bolsa. Las manzanas son un producto fresco, y vemos que hay un producto fresco
en cada contenedor, así que hemos de pensar en qué contenedor las ha metido. Es
el primer producto de la lista, así que se encontrará con los contenedores vacíos.
Las manzanas tienen un peso de 1000 g y un volumen de 1500 cm3.
Detección y corrección de problemas 199
Desde main llamamos a addProducto del Pedido, pasándole las manzanas como
argumento. Pedido coge el primero de sus contenedores, que podría ser la bolsa, y
entonces intenta meterle las manzanas. Lo primero que comprueba meter es la
resistencia del contenedor para ver si soporta el peso de ese producto. La resistencia
de la bolsa es de 900 g, el peso de las manzanas es de 1000 g, así que la bolsa no
resiste las manzanas. Aunque el código sigue, imaginamos que la bolsa no aceptará
las manzanas, así que regresamos a addProducto del Pedido y probamos con el otro
contenedor: la caja.
La caja lo resiste todo, así que toca comprobar si hay espacio para las manzanas,
que tienen un volumen de litro y medio. Lo podemos calcular a mano, pero en la
salida de las pruebas que hemos hecho vemos que el volumen de la caja es de más
de 100 litros, así que ahí no debería poner problemas.
La siguiente comprobación es la de la compatibilidad entre productos, pero como
las manzanas son el primer producto que metemos en el pedido, tampoco esta
comprobación pondrá problemas.
Así que ya hemos resuelto el misterio: las manzanas son el fresco que hay en la caja.
El segundo producto es el helado, un Congelado, que ha sido metido en la bolsa. Y
llega el momento del papel higiénico, que no se está metiendo ni en la bolsa ni en
la caja, y queremos descubrir el porqué.
Volvemos a addProducto del Pedido, pasándole esta vez el papel higiénico como
argumento. Este producto es del tipo Higiene, pesa medio kilo y tiene un volumen de
dos litros y medio. Pedido coge el primer contenedor, que dijimos que era la bolsa. Así
que vamos a intentar meter el papel en la bolsa. Como hemos visto hace unas líneas, lo
primero que comprueba meter es la resistencia del contenedor. La implementación de
resiste no tiene en cuenta el resto de productos ya metidos (ver nota), así que aceptará
sin problemas un producto de medio kilo en una bolsa de 900 gramos de resistencia.
NOTA:
Omitamos el detalle de la rareza de no tener en cuenta al resto de productos para el tema de la
resistencia, pero así tenemos una casuística distinta a la del volumen, lo que vuelve más
interesante nuestro ejemplo.
La segunda comprobación es la del volumen. La bolsa tiene un volumen de algo más de
6 litros, el helado ocupa al menos un litro, así que nos quedan cinco. El papel solo ocupa
dos y medio, así que esta comprobación tampoco será un obstáculo. La tercera
comprobación, la compatibilidad. Hay helado en la bolsa, ¿podemos meter papel
higiénico? Recuperamos ese fragmento de código:
for (IProducto p : productos) {
boolean compatibleOk = [Link](p);
compatibilidadOk &= compatibleOk;
}
200 Relaciones orientadas a objetos
Por cada uno de los productos que hay en el contenedor, se comprueba si ese producto
p es compatible con el producto producto recibido como parámetro, que es el papel. En
la bolsa solo está el helado, así que p será el helado y producto, el papel. Nos metemos
entonces en el método esCompatible de Higiene. Recuerda que este método no lo
implementamos en Producto, sino en sus hijos. Y al crear el papel hicimos new Higiene(…).
public boolean esCompatible(IProducto p) {
return );
}
Es compatible: recibe un argumento de tipo IProducto llamado p, que en nuestro caso
es el helado, cuya categoría es Alimentacion. ¡Ay! ¡Que va a estar ahí el problema! No
podemos meter en el mismo contenedor productos de Higiene con productos de
Alimentacion.
Y en el otro contenedor, la caja, hemos guardado las manzanas, también con categoría
Alimentacion. Misterio resuelto. El papel no lo podemos incluir en ninguno de los
contenedores del pedido porque en todos hay productos de Alimentacion. Por tanto, en
este caso, el problema no es en nuestro programa, nada que corregir. Simplemente, en
este caso concreto, siguiendo las reglas de negocio establecidas, es un problema «de
datos». Quizá deberíamos hacer más inteligente el programa para que optimice el reparto
de productos en los contenedores, pero no nos meteremos en eso. Puedes seguir
practicando con las peras.
Mejora en el toString de Producto en sOOPer
Durante el ejercicio de ejecución «a mano» hemos sufrido un poco por culpa de no haber
sobrescrito el método toString de Producto, lo hicimos para los contenedores y el pedido,
pero no para Producto. Llegó el momento de hacerlo, te invito a probarlo tú sin mirar
mi solución hasta que no llegues a la tuya (escrita y probada):
@Override
public String toString() {
return "Producto [categoria=" + getCategoria() + ", referencia="
+ referencia + ", peso=" + peso + ", volumen="
+ volumen + ", contenedor=" + [Link]() + "]";
}
Es importante que lo pruebes bien porque aquí hay un riesgo importante de meternos
en un bucle infinito. El toString de Producto se llama desde el toString de Contenedor:
aunque no lo hagamos explícitamente, al concatenar un String con un objeto, se llama al
toString de ese objeto. Por ello, si cometiéramos el error de intentar pintar el contenedor
en vez de su referencia, al pintar ese contenedor se incluirían todos sus productos, que
pintarían también su contenedor. Por lo anterior, he optado por incluir solo la referencia
al contenedor.
Detección y corrección de problemas 201
Terminando tareas pendientes en sOOPer
El proyecto sOOPer ya está casi terminado. El código fuente está disponible para descarga.
Pero quedan algunas cosillas por terminar, y mucho que probar. Hay que forzar más.
No nos vale con probar con unos pocos productos y unos pocos contenedores. Intentemos
llegar a casos «extremos», como que haya productos que no caben en ningún contenedor,
para asegurarnos cómo funciona todo.
Empecemos, pues, buscando esos trozos de código que no hemos terminado. Como los
tenemos marcados con la etiqueta TODO dentro de un comentario, será fácil dar con
ellos. En Eclipse, Ventana>Mostrar vista>Tareas nos añade una pestaña abajo con todas
las tareas pendientes, es decir, donde hay todavía un TODO. A las malas, puedes
simplemente buscar el texto «TODO» en todo el proyecto.
Método getResistencia de Contenedor en sOOPer
Tenemos que devolver la resistencia.
@Override
public String getResistencia() {
return resistencia;
}
Método getProductos de Contenedor en sOOPer
Devolvemos los productos.
@Override
public Set<IProducto> getProductos() {
return productos;
}
Método getProductos de Pedido en sOOPer
Ahora mismo está retornando un nulo siempre. No nos vale, toca implementar. Inténtalo
sin mi ayuda.
@Override
public Set<IProducto> getProductos() {
Set<IProducto> productos = new HashSet<>();
for (IContenedor c : contenedores) {
[Link]([Link]());
}
return productos;
}
¿Cómo ha ido? Parecía más fácil, ¿no? No, no era un simple get de esos que retorna el
atributo y ya está, pero tampoco es tan complicado. Mi solución ha sido preparar un
conjunto de productos nulo y, recorriendo todos los contenedores, inicializar el nuevo
conjunto con los productos del primer contenedor e ir añadiendo los del resto. También
202 Relaciones orientadas a objetos
podría haber creado un conjunto vacío (en vez de nulo) e irle añadiendo los de todos los
contenedores. Suele haber mil formas para hacer las cosas. Asegúrate de quitar el
comentario TODO si no lo has hecho. Ya no nos quedan más TODO que resolver.
Un programador novel podría pensar que ya está terminado el proyecto, pero no, hay que
probarlo bien, incluyendo casos extremos, para cerciorarnos de que no se nos escapa nada.
Probado no exhaustivo de la implementación
La forma más adecuada de asegurarnos de que todo va bien sería programar test unitarios,
los aprenderemos en el capítulo 14. Pero incluso así, probaremos un poco más desde el main.
Busquemos los límites de nuestra aplicación. Hay que sacar un poco de maldad e intentar
que las pruebas que hagamos fallen. De nada sirven las pruebas si todas salen bien.
Probado no exhaustivo de la implementación en sOOPer
Partimos de dos contenedores y cuatro productos. Si ejecutamos, se aprecia que los
productos de alimentación se incluyen bien en los contenedores y, sin embargo, el papel
higiénico no. No pasa nada. Ampliemos el pedido, tanto en productos como en
contenedores.
• Metamos, por ejemplo, tres cajas y cinco bolsas nuevas:
for (int i = 0; i < 3; i ++) {
IContenedor caja = new Caja("C23" + i, 30, 40, 30);
[Link](caja);
}
for (int i = 0; i < 5; i ++) {
IContenedor bolsa = new Bolsa("B12" + i, 30, 25, 3000);
[Link](bolsa);
}
• Y también, no sé, unos cuantos litros de leche, comida para los gatos, gel de ducha,
detergente, lejía, yogures, arroz...
for (int i = 0; i < 12; i ++) {
IProducto leche = new NoPerecedero("LCH" + i, 6600, 7000);
[Link](leche);
}
[Link](new Mascotas("GAT", 5000, 10000)); // comida gato
[Link](new Mascotas("PER1", 10000, 20000)); // comida perro
[Link](new Mascotas("PER2", 10000, 20000));
[Link](new Higiene("GEL", 1500, 1600)); // gel de ducha
[Link](new Drogueria("DET", 2000, 1600)); // detergente lavadora
[Link](new Drogueria("LEJ", 1000, 1000)); // lejía
for (int i = 0; i < 24; i ++) {
[Link](new Fresco("YOG" + i, 250, 300)); // yogur
}
[Link](new NoPerecedero("ARR", 1000, 1000)); // arroz
for (int i = 0; i < 5; i ++) {
[Link](new NoPerecedero("PAS" + i, 1000, 1200)); // pasta
}
Detección y corrección de problemas 203
Y ejecutamos a ver qué tal:
Mi pedido con productos: Pedido: pedido001
Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 900 g).
Producto [cat=ALIMENTACION, ref=YOG11, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG0, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG3, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG7, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG12, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG9, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=PER, peso=800, vol=1200, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG10, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG1, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG4, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG2, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=HLD, peso=800, vol=1000, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG6, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG5, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG8, peso=250, vol=300, cont=B111]
>> Disponible vol 20cm3
Contenedor C231 [CAJA] (sup 1200cm2 - vol 36000cm3 - resistencia 0 g).
Producto [cat=ALIMENTACION, ref=YOG22, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=MNZ, peso=1000, vol=1500, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH2, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=ARR, peso=1000, vol=1000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG23, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=PAS0, peso=1000, vol=1200, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG18, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH1, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG15, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH0, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG16, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG17, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG14, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG21, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG19, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG20, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH3, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG13, peso=250, vol=300, cont=C231]
>> Disponible vol 1000cm3
Contenedor C232 [CAJA] (sup 1200cm2 - vol 36000cm3 - resistencia 0 g).
Producto [cat=DROGUERIA, ref=LEJ, peso=1000, vol=1000, cont=C232]
Producto [cat=HIGIENE, ref=PWC, peso=500, vol=2500, cont=C232]
Producto [cat=DROGUERIA, ref=DET, peso=2000, vol=1600, cont=C232]
Producto [cat=HIGIENE, ref=GEL, peso=1500, vol=1600, cont=C232]
>> Disponible vol 29300cm3
Contenedor C230 [CAJA] (sup 1200cm2 - vol 36000cm3 - resistencia 0 g).
Producto [cat=ALIMENTACION, ref=LCH4, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH7, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH6, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH8, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH5, peso=6600, vol=7000, cont=C230]
>> Disponible vol 1000cm3
Contenedor B124 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor B122 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor B121 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
Producto [cat=ALIMENTACION, ref=LCH11, peso=6600, vol=7000, cont=C222]
Producto [cat=ALIMENTACION, ref=PAS1, peso=1000, vol=1200, cont=C222]
204 Relaciones orientadas a objetos
Producto [cat=ALIMENTACION, ref=PAS2, peso=1000, vol=1200, cont=C222]
Producto [cat=ALIMENTACION, ref=LCH9, peso=6600, vol=7000, cont=C222]
Producto [cat=ALIMENTACION, ref=PAS3, peso=1000, vol=1200, cont=C222]
Producto [cat=ALIMENTACION, ref=PAS4, peso=1000, vol=1200, cont=C222]
Producto [cat=ALIMENTACION, ref=LCH10, peso=6600, vol=7000, cont=C222]
>> Disponible vol 86700cm3
Contenedor B123 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor B120 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Parece que no hay problemas, porque hay bolsas vacías, pero si te fijas bien, hay muchas
cosas de alimentación, pero… ¿dónde está la comida de las mascotas? Creo que son
productos muy grandes y quizá no caben en las bolsas. Metamos una caja bien grande:
[Link](new Caja("C333", 50, 60, 75)); // caja grande
Ejecutamos y comprobamos que hemos conseguido meter la comida del gato, pero ¿y la
de los perros? En la caja queda mucho hueco. Depurando podríamos encontrar el
problema, pero piensa, ¿dónde está el problema? Las cajas no tienen límite de resistencia
y parece que el espacio tampoco es problema. ¿Qué condición nos queda? La compatibilidad.
La comida de perro y la de gato deberían ser compatibles, repasemos el código.
Corrección del método esCompatible de Mascotas en sOOPer
Vamos a la clase Mascotas, método esCompatible. ¡Madre! ¡Qué hemos hecho aquí! La
comida del perro es compatible con otro producto si ese otro producto es de Drogueria.
¿Queremos intoxicar a los pobres canes? ¡Era justo lo contrario!
@Override
public boolean esCompatible(IProducto p) {
return );
}
Vale, neguemos esa condición, y a ver si ahora funciona mejor.
Mi pedido con productos: Pedido: pedido001
Contenedor B111 [BOLSA] (sup 153cm2 - vol 6120cm3 - resistencia 900 g).
Producto [cat=ALIMENTACION, ref=YOG7, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG3, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=HLD, peso=800, vol=1000, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG8, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG5, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG11, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG6, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=PER, peso=800, vol=1200, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG12, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG0, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG10, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG2, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG1, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG4, peso=250, vol=300, cont=B111]
Producto [cat=ALIMENTACION, ref=YOG9, peso=250, vol=300, cont=B111]
>> Disponible vol 20cm3
Contenedor C231 [CAJA] (sup 1200cm2 - vol 36000cm3 - resistencia 0 g).
Producto [cat=ALIMENTACION, ref=YOG18, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG22, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH1, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG20, peso=250, vol=300, cont=C231]
Detección y corrección de problemas 205
Producto [cat=ALIMENTACION, ref=LCH3, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG19, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG21, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG14, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH0, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG13, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG23, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=MNZ, peso=1000, vol=1500, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG17, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG15, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=ARR, peso=1000, vol=1000, cont=C231]
Producto [cat=ALIMENTACION, ref=YOG16, peso=250, vol=300, cont=C231]
Producto [cat=ALIMENTACION, ref=LCH2, peso=6600, vol=7000, cont=C231]
Producto [cat=ALIMENTACION, ref=PAS0, peso=1000, vol=1200, cont=C231]
>> Disponible vol 1000cm3
Contenedor C232 [CAJA] (sup 1200cm2 - vol 36000cm3 - resistencia 0 g).
Producto [cat=MASCOTAS, ref=PER1, peso=10000, vol=20000, cont=C232]
Producto [cat=MASCOTAS, ref=GAT, peso=5000, vol=10000, cont=C232]
Producto [cat=HIGIENE, ref=PWC, peso=500, vol=2500, cont=C232]
Producto [cat=HIGIENE, ref=GEL, peso=1500, vol=1600, cont=C232]
>> Disponible vol 1900cm3
Contenedor C230 [CAJA] (sup 1200cm2 - vol 36000cm3 - resistencia 0 g).
Producto [cat=ALIMENTACION, ref=LCH6, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH8, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH5, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH7, peso=6600, vol=7000, cont=C230]
Producto [cat=ALIMENTACION, ref=LCH4, peso=6600, vol=7000, cont=C230]
>> Disponible vol 1000cm3
Contenedor B124 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor C333 [CAJA] (sup 4500cm2 - vol 225000cm3 - resistencia 0 g).
Producto [cat=ALIMENTACION, ref=LCH10, peso=6600, vol=7000, cont=C333]
Producto [cat=MASCOTAS, ref=PER2, peso=10000, vol=20000, cont=C333]
Producto [cat=ALIMENTACION, ref=LCH11, peso=6600, vol=7000, cont=C333]
Producto [cat=ALIMENTACION, ref=LCH9, peso=6600, vol=7000, cont=C333]
>> Disponible vol 184000cm3
Contenedor B122 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor B121 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor C222 [CAJA] (sup 3750cm2 - vol 112500cm3 - resistencia 0 g).
Producto [cat=DROGUERIA, ref=LEJ, peso=1000, vol=1000, cont=C222]
Producto [cat=DROGUERIA, ref=DET, peso=2000, vol=1600, cont=C222]
>> Disponible vol 109900cm3
Contenedor B123 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Contenedor B120 [BOLSA] (sup 153cm2 - vol 4590cm3 - resistencia 3000 g).
vacío
>> Disponible vol 762000cm3
Esto ya pinta mejor: en la caja 232 tenemos la comida de gato y uno de los paquetes
para el perro con los productos de higiene. ¡Esperemos que el gato no pille ningún
rollo de papel de váter y no nos la líe por toda la casa! Y el otro paquete para el perro
con la comida en la caja 333. Bien.
Fíjate en que si no hubiéramos probado un poco más, habríamos entregado con un error
importante y sin darnos cuenta. Pero este test no es para nada exhaustivo, así que
podríamos tener más errores y no verlos. Por eso convendría programar test unitarios.
206 Relaciones orientadas a objetos
Modificadores de Java
Nos quedan un par de conceptos a tratar que, si bien no están directamente
relacionados con la orientación a objetos, creo que ahora los entenderás mejor que
si te los hubiera contado en la parte de programación estructurada.
Java tiene unas cuantas palabras clave conocidas como modificadores que se ponen
delante de la declaración de un elemento (clase, objeto, método o atributo),
modificando la forma en la que se utiliza ese elemento.
Por un lado, tenemos los modificadores de acceso, que son cuatro:
• public: se aplica tanto a clases como a miembros.
• cada fichero *.java solo puede contener una clase pública, cuyo nombre
coincidirá con el del fichero, pero puede contener otras clases internas, que
no serán públicas. Las clases públicas estarán disponibles donde se use su
paquete.
• un atributo o un método público será accesible donde se use la clase y será
heredado por las clases hijas. Las buenas prácticas recomiendan no tener
atributos públicos (mejor tener métodos que accedan a ellos), así como reducir
a los mínimos necesarios los métodos públicos.
• en una interfaz no hay que indicar que los métodos declarados son public,
pues todos lo son pongamos o no el modificador.
• protected: solo se aplica a métodos y atributos, no a clases.
• un miembro protegido es accesible desde las clases de su paquete, y en sus
subclases (estén o no en el mismo paquete). Nos resultará muy interesante
cuando veamos los test unitarios (capítulo 12).
• friendly: este es el modificador por defecto, se le llama friendly, pero no existe
como tal, no hay que escribirlo.
• una clase friendly será accesible solo para las otras clases de su paquete.
• un miembro friendly será accesible solo desde su mismo paquete, a diferencia
de un miembro protected, no tendrá visibilidad en las clases hijas, salvo si
están dentro del mismo paquete.
• private: este modificador tampoco se aplica a clases, solo a miembros.
• un atributo o método privado es accesible exclusivamente dentro de la propia
clase. Será nuestra elección preferida para atributos, y para todos aquellos
métodos que no necesitemos que sean públicos, para lograr la encapsulación
del código, limitando el acceso a las intimidades de una clase y ofreciendo
al mundo solo los métodos públicos que nos interesen.
Modificadores de Java 207
En la herencia, hemos de tener en cuenta las siguientes reglas con respecto al acceso:
• Los métodos declarados como públicos en una superclase deben ser públicos en
todas las subclases.
• Los métodos declarados como protegidos en la superclase podrán ser protegidos
o públicos en las subclases, pero no ser privados.
• Los métodos declarados como privados no se heredan.
Por otro lado, existen otros cuatro modificadores con otras funcionalidades (no de acceso):
• static: los elementos estáticos existirán independientemente de la creación o no
de instancias de una clase. Por eso los elementos no estáticos se llaman «de
instancia».
• solo las clases internas (inner classes) son estáticas. No necesitaremos
instanciar la clase externa (outer class) para instanciar una clase interna
estática. Desde la clase interna estática solo accederemos a los elementos
estáticos de la clase externa: si la interna no fuera estática podríamos acceder
tanto a los elementos estáticos como a los no estáticos.
• los métodos estáticos no requieren una instancia de ningún objeto de la clase
en la que se definen para ser llamados, así que tampoco pueden utilizar los
elementos no estáticos de esa clase. El método random de la clase Math, que
usamos en el proyecto de Piedra-Papel-Tijeras, es un método estático:
int num = (int)([Link]() * 10);
No hay que crear un objeto de la clase Math para llamar al método random:
simplemente usamos el nombre de la clase, punto, el nombre del método.
• los atributos estáticos solo tendrán una copia independiente del número de
instancias que se creen de esa clase, podemos considerarlo como que su valor
será compartido por todos los objetos que se creen de esa clase. Si un objeto
modifica el valor, todos los demás se verán afectados por el cambio.
• final: los elementos finales no son modificables, pero veamos qué implica más
concretamente en cada caso:
• las clases finales no se pueden extender. Es imposible crear subclases que las
extiendan.
• los métodos finales no pueden ser sobrescritos por ninguna de sus subclases.
• las variables finales (atributos, parámetros) solo pueden ser inicializadas
una vez. Sí es posible cambiar los datos dentro del objeto, pero no podemos
hacer que apunte a otro objeto.
public void suma(final int a, final int b) {
a += b; // dará error, a no se puede modificar
}
208 Relaciones orientadas a objetos
• las variables estáticas y finales son constantes: no podemos modificarlas (por
el final) y solo existirá una instancia de la variable compartida entre todas
las instancias de esa clase (por el static).
• abstract: los elementos abstractos no están implementados.
• una clase abstracta no puede ser instanciada: requeriremos extenderla para
contar con una clase no abstracta que ya podremos instanciar. Si una clase
es abstracta (hay que extenderla), no puede ser final (es imposible extender).
• un método abstracto no está implementado, se implementará en una clase
hija. Si una clase contiene un método abstracto, ha de ser abstracta. Cuando
declaramos un método abstracto, no pondremos cuerpo entre llaves, solo un
punto y coma.
public abstract metodoAbstracto();
• synchronized: los elementos sincronizados solo podrán ser accedidos por una
T8.6
hebra a la vez, en programación concurrente. Se puede aplicar a métodos o a
bloques.
Ejemplo estático
Entender bien el uso de la palabra reservada static y no ponerla solo cuando Eclipse dice
que hay que hacerlo es importante para ser un buen programador. Veamos un ejemplo
para constatar en directo cómo funciona.
public class Estatica {
private static final int CONSTANTE = 0;
private static int compartida = 0;
private final int noModificable;
private int normal;
public Estatica(int n) {
noModificable = n;
}
private void incrementaTodo() {
// CONSTANTE ++;
compartida ++;
// noModificable ++;
normal ++;
}
private void pintaTodo(String titulo) {
[Link](titulo);
[Link]("Constante: \t" + CONSTANTE);
[Link]("Compartida: \t" + compartida);
[Link]("No modificable: \t" + noModificable);
[Link]("Normal: \t" + normal);
[Link]();
}
Modificadores de Java 209
public static void main(String[] args) {
[Link]("Constante: \t" + CONSTANTE);
[Link]("Compartida: \t" + compartida);
// [Link]("No modificable: \t" + noModificable);
// [Link]("Normal: \t" + normal);
[Link]();
Estatica seis = new Estatica(6);
Estatica ocho = new Estatica(8);
Estatica diez = new Estatica(10);
[Link]("SEIS A: ");
[Link]();
[Link]("SEIS B: ");
[Link]();
[Link]("SEIS C: ");
[Link]();
[Link]();
[Link]("SEIS D: ");
[Link]("OCHO: ");
[Link]("DIEZ: ");
}
}
En la clase Estatica declaramos:
• Cuatro atributos en la clase, todos números enteros, uno estático final,
CONSTANTE, otro estático, compartida, otro final, noModificable, y otro sin
modificadores, normal.
• Un constructor que recibe un entero con el que inicializaremos la variable final
de cada instancia.
• Un método incrementaTodo, que intenta incrementar el valor de cada uno de
los atributos. Con CONSTANTE y noModificable no podemos hacerlo, para lograr
ejecutar necesitamos comentar esas líneas.
• Un método pintaTodo, que muestra el valor de cada una de las variables.
• El método main para testar donde:
• Intentamos pintar todos los atributos, pero como main es un método estático
es imposible acceder ni a noModificable ni a normal, que no son estáticas.
Constante: 0
Compartida: 0
Todos los valores están a cero, aún no los hemos modificado:
• Creamos tres instancias de la clase, iniciando cada una con un valor distinto.
• Pintamos la primera instancia:
SEIS A:
Constante: 0
Compartida: 0
No modificable: 6
Normal: 0
210 Relaciones orientadas a objetos
Hemos construido la instancia, noModificable toma el valor que le dimos en el
constructor y el resto tienen su valor inicial, cero.
• Llamamos al método incrementarTodo de la primera instancia y pintamos el
resultado:
SEIS B:
Constante: 0
Compartida: 1
No modificable: 6
Normal: 1
Solo se han incrementado compartida y normal; constante y noModificable, como
sus nombres indican, no se pueden modificar.
• Llamamos al método incrementarTodo de la segunda instancia y pintamos los
valores de la primera:
SEIS C:
Constante: 0
Compartida: 2
No modificable: 6
Normal: 1
A pesar de que la llamada la hicimos sobre la segunda instancia, ocho, al pintar
la primera instancia, seis, vemos que compartida ha incrementado de valor.
• Llamamos al método incrementarTodo de la tercera instancia dos veces y pintamos
otra vez los valores de la primera, los de la segunda y los de la tercera:
SEIS D:
Constante: 0
Compartida: 4
No modificable: 6
Normal: 1
OCHO:
Constante: 0
Compartida: 4
No modificable: 8
Normal: 1
DIEZ:
Constante: 0
Compartida: 4
No modificable: 10
Normal: 2
La constante, constante ella, mantiene su valor, cero.
La compartida cuenta el número de llamadas que hemos hecho en global a incrementarTodo,
cuatro.
La noModificable tomó un valor para cada instancia, y lo mantiene.
La normal de cada instancia lleva el contador de llamadas a incrementarTodo de cada
una, por eso la de diez vale 2, y el resto se han quedado en 1.
Juega con este código, es interesante para familiarizarse con el uso de static y llegar a
dominarlo.
Modificadores de Java 211
Test
Test 8.1. ¿Cuál de estas afirmaciones son correctas, según el siguiente
fragmento de código?
Arbol abeto = new Pino();
a) Pino es un abeto.
b) Abeto es un pino.
c) Abeto es un árbol.
d) Árbol es un abeto.
Test 8.2. ¿Cuáles de las sobrecargas del método suma no son viables?
public int suma(int a, int b) { … }
a) public int suma(int a, int b, int c) { … }
b) public double suma(int a, int b) { … }
c) public double suma(double a, double b) { … }
d) public int suma(int num1, int num2) { … }
Test 8.3. La _____ del método toString nos permite personalizar la
representación en modo texto de los objetos de cada clase.
a) implementación
b) extensión
c) sobrecarga
d) sobrescritura
Test 8.4. Según la implementación del sOOPer, documentada en la figura 8.4,
¿quién decide si un producto cabe, por volumen, en un contenedor?
a) Supermercado
b) Pedido
c) Contenedor
d) Producto
Test 8.5. Según la implementación del sOOPer, documentada en la figura 8.4,
¿quién decide si un producto cabe, por peso, en un contenedor?
a) Supermercado
b) Pedido
c) Contenedor
d) Producto
Test 8.6. ¿Cómo se declara una constante que pueda ser utilizada solo en las
clases del mismo paquete?
a) static final int NUM = 3;
b) final int NUM = 3;
c) public static final int NUM = 3;
d) protected static final int NUM = 3;
212 Relaciones orientadas a objetos
Soluciones
Test 8.1. ¿Cuál de estas afirmaciones son correctas, según el siguiente
fragmento de código?
Arbol abeto = new Pino();
b) Abeto es un pino.
c) Abeto es un árbol.
Ambas respuestas son correctas: el objeto abeto es un Pino y es un Arbol.
Test 8.2. ¿Cuáles de las sobrecargas del método suma no son viables?
public int suma(int a, int b) { … }
b) public double suma(int a, int b) { … }
d) public int suma(int num1, int num2) { … }
En ambos casos el problema es el mismo: la lista de parámetros (fijándonos en los
tipos) debe ser distinta; si no, Java no será capaz de saber qué implementación tiene
que escoger… ¿Cómo saberlo con la llamada suma(2, 5)?
Test 8.3. La _____ del método toString nos permite personalizar la
representación en modo texto de los objetos de cada clase.
d) sobrescritura: si no sobrescribimos este método, se usará el de Object.
Test 8.4. Según la implementación del sOOPer, documentada en la figura 8.4,
¿quién decide si un producto cabe, por volumen, en un contenedor?
d) Producto
Test 8.5. Según la implementación del sOOPer, documentada en la figura 8.4,
¿quién decide si un producto cabe, por peso, en un contenedor?
c) Contenedor
Test 8.6. ¿Cómo se declara una constante que pueda ser utilizada solo en las
clases del mismo paquete?
a) static final int NUM = 3;: sin indicar modificador le damos acceso friendly, es
decir, solo en el mismo paquete, con static y final la hacemos constante: compartida y no
modificable.
Soluciones 213
9 Proyecto «macetohuerto»
En este capítulo aprenderás a:
• Desarrollar un programa orientado a objetos.
• Aplicar lo aprendido en los capítulos anteriores.
• Perderles el miedo al código, a las clases, a tener varios ficheros.
Introducción
En este capítulo haremos un proyecto orientado a objetos, guarda cierto parecido con el
sOOPer, pero esta vez lo haremos «del tirón», pues ya tenemos todos los conocimientos
necesarios. Mi consejo es que intentes hacer cada paso por tu cuenta antes de consultar
mi propuesta de solución.
Presentación del proyecto
Los requisitos que nos llegan del cliente son los siguientes:
Tengo un huerto urbano en las ventanas de mi casa… Y quiero un sistema que me
ayude a gestionarlo.
Voy a hacer una combinación de hierbas aromáticas (como el hinojo o el perejil),
plantas de hoja (lechuga, canónigos…), de raíz (rabanitos, zanahorias), de fruto
(tomates, pimientos)…
Algunas plantas necesitan ser sembradas en semillero, y otras se pueden plantar
directamente.
Cada especie tiene sus propios requisitos de espacio (volumen de sustrato que
necesitan, distancia entre plantas…), de riego, sus tiempos de germinación, trasplante
(si se plantaron en semillero) y recolección.
Hay cultivos que son compatibles (pueden plantarse en la misma maceta) y otros
que no. De otros, no sabemos. ;) La compatibilidad entre plantas depende de los
nutrientes que consumen y del espacio que necesitan, así que suelen ser fácilmente
combinables una planta de raíz, con una de hoja con una de fruto (lechuga con tomate
y zanahoria), pero no dos del mismo tipo (lechuga con canónigos).
En el huerto, tengo disponibles varias jardineras, de distintos tamaños y formas
(rectangular, tubular).
Nos surgen unas cuantas dudas con el cliente, le hacemos una estimación de costes y,
entre resolver dudas y un intento de perfilar una primera versión no muy extensa pero
sí más completa, nos comentan que:
• los requisitos que tendremos en cuenta de cada planta son la distancia entre ellas,
que nos dará la superficie de tierra que debemos reservar para cada planta y el
volumen de tierra que requiere cada una.
• las plantas de raíz tienen un requisito extraordinario: la profundidad de maceta
necesaria.
De momento, no tendremos en cuenta la siembra en semillero, ni tiempos (germinación,
recolección…) ni el riego.
Además, nos completan los datos, qué plantas trataremos y qué requisitos tiene cada una:
216 Relaciones orientadas a objetos
Tabla 9.1. Requisitos de las plantas.
Familia Distancia Volumen Otros requisitos
Tomate Planta de fruto 30 cm 18 litros
Tomate cherry Planta de fruto 25 cm 15 litros Mismas compatibilidades que el tomate
Hinojo Planta aromática 10 cm 8 litros
Lechuga Planta de hoja 18 cm 22 litros
Perejil Planta aromática 8 cm 5 litros
Zanahoria Planta de raíz 3 cm 15 litros Profundidad requerida: 25 cm
Tabla 9.2. Tabla de compatibilidades entre plantas.
Tomate Hinojo Lechuga Perejil Zanahoria
Tomate - X √ √ √
Hinojo X - √ X
Lechuga √ - √
Perejil √ X - X
Zanahoria √ X √ X -
Los pasos que recomiendo para resolver este problema son los siguientes:
• Crear un diagrama de clases UML para representar el huerto.
• Extracción de conceptos clave del enunciado.
• Boceto en papel del modelo conceptual.
• Si tienes compañeros de aprendizaje, comparte con ellos tu modelo y acordad uno
común.
• Implementación.
• Estructura de paquetes.
• Interfaces.
• Clases de la estructura de macetas.
• Clases de la estructura de plantas.
• El huerto y sus procesos.
• Pruebas.
Soluciones 217
Extracción de conceptos del macetohuerto
Leyendo con atención los requisitos del cliente y teniendo en cuenta las aclaraciones,
listamos nombres, verbos, valores:
Tabla 9.3. Conceptos del macetohuerto.
Nombres Verbos Valores
• huerto • especie • gestionar • lechuga
• ventanas • espacio • sembrar • zanahorias
• casa • volumen • plantar • tomates
• sistema • distancia • germinar • tomate cherry
• plantas • compatibilidad • trasplantar • hinojo
• hierbas aromáticas • nutrientes • recolectar • perejil
• plantas de hoja • jardinera • combinar • rectangular
• plantas de raíz • tamaño • tubular
• plantas de fruto • forma
Modelo conceptual del macetohuerto
Tenemos, pues, un Sistema que gestionará Huertos con Macetas, Tubulares o Rectangulares
en las que plantaremos Plantas, que pueden ser Aromáticas, de Hoja, de Raíz o de Fruto.
Más concretamente, pueden ser Tomates (normales o Cherry), Hinojo, Lechuga, Perejil
y Zanahoria.
Figura 9.1. Modelo conceptual del macetohuerto
He decidido crear una clase por cada planta, algo que quizá no te surja instintivamente,
porque luego crearé objetos de tomates, y tendré varios objetos de tipo tomate, todos con
los mismos requisitos, pero cada uno será distinto, porque surgirán de semillas distintas
218 Proyecto «macetohuerto»
plantados en macetas distintas en fechas distintas. Y hablando de tomates, el Tomate
Cherry lo he puesto como hijo del Tomate, porque heredará ciertas características (como
las compatibilidades), pero tendrá otras propias (como los requisitos de distancia o
volumen, pues son plantas más pequeñitas).
Estructura de paquetes del macetohuerto
La estructura de paquetes que propongo para este proyecto, y viendo el modelo
conceptual, serían:
• huerto: paquete raíz de todo el proyecto. Si fuera un proyecto profesional, pondríamos
el nombre completo de paquete con la url de la empresa cliente o desarrolladora en
orden inverso.
• enums: aún no hemos visto cuáles, pero tendremos enumerados, los pondremos aquí.
• macetas: en este subpaquete pondremos la implementación de las macetas y sus
subclases.
• plantas: en este otro subpaquete, se halla toda la jerarquía de plantas.
Interfaces del macetohuerto
Las interfaces irían en la parte superior de la jerarquía, así que, tomando nuestro modelo
conceptual, podríamos apostar por las siguientes:
• IHuerto
• IMaceta
• IPlanta
También necesitaremos tres enumerados:
• FormaMaceta
• Familia
• Especie
Interfaz IHuerto
En un huerto querremos poder añadir macetas y plantar plantas en alguna de las macetas.
package huerto;
public interface IHuerto {
void addMaceta(IMaceta maceta);
IMaceta plantar(IPlanta planta);
}
Estructura de paquetes del macetohuerto 219
Interfaz IMaceta
Este no es un huerto cualquiera, es nuestro macetohuerto en las ventanas de nuestra
casa, es algo cercano. Les daremos nombres a las macetas. Si fuera para una gran empresa
de gestión de huertos, quizá les daremos una referencia en vez de un nombre.
De cada maceta, querremos saber tanto su volumen total como el disponible, la superficie
total y la disponible, y su profundidad (la disponible no la necesitamos porque la
profundidad no se gasta).
Nos queda conocer la forma de la maceta, para lo que utilizaremos un enumerado,
FormaMaceta, que tendremos que importar porque está en otro paquete.
Ahora, las operaciones sobre las macetas: querremos poder plantar plantas y, por tanto,
saber qué conjunto de plantas tenemos ya.
package huerto;
import [Link];
import [Link];
public interface IMaceta {
String getNombre();
int getVolumen();
int volumenDisponible();
int getSuperficie();
int superficieDisponible();
int getProfundidad();
FormaMaceta getForma();
boolean plantar(IPlanta planta);
Set<IPlanta> getPlantas();
}
Enumerado FormaMaceta
De momento, solo trabajamos con dos formas: rectangular o tubular. Podría crecer si
fuera necesario.
package [Link];
public enum FormaMaceta {
RECTANGULAR, TUBULAR;
}
Interfaz IPlanta
De las plantas, a las que también les daremos un nombre porque vamos a mimarlas
mucho para que crezcan hermosas y sabrosas, necesitamos conocer qué superficie y qué
volumen requieren y de qué familia y especie son (para lo que usaremos enumerados).
220 Proyecto «macetohuerto»
Una planta ha de ser capaz de decirnos si es compatible con otra, si tiene espacio en una
maceta y, finalmente, plantarse (sí, ella a sí misma) en una maceta.
package huerto;
import [Link];
import [Link];
public interface IPlanta {
String getNombre();
int getSuperficieRequerida();
int getVolumenRequerido();
Familia getFamilia();
Especie getEspecie();
boolean esCompatible(IPlanta planta);
boolean tengoEspacio(IMaceta maceta);
void plantar(IMaceta maceta);
}
Enumerado Familia
Vamos a trabajar con cuatro familias de plantas distintas, así que las enumeramos: sin
tildes, claro.
package [Link];
public enum Familia {
HOJA, RAIZ, FRUTO, AROMATICA;
}
Enumerado Especie
package [Link];
public enum Especie {
LECHUGA, ZANAHORIA, TOMATE, PEREJIL, ESPINACA, REMOLACHA, HINOJO;
}
Clases de la estructura de macetas del
macetohuerto
Dentro del paquete [Link] se hallan la clase abstracta Maceta y sus dos
implementaciones (MacetaRectangular y MacetaTubular).
Clase abstracta Maceta
La clase Maceta implementará la interfaz IMaceta, pero no será capaz de implementar todos
los métodos. Algunos dependerán de la forma de la maceta, así que tendremos que relegarlos
a la implementación de las clases hijas. Por eso, Maceta será una clase abstracta.
Clases de la estructura de macetas del macetohuerto 221
Al crear la clase Maceta, al decir que implementa IMaceta, el propio entorno de desarrollo
nos genera un esqueleto de la clase con los métodos que tenemos que implementar. Pero
hay que implementarlos.
Cuando creamos una Maceta o cuando vamos al centro de jardinería a comprar una
maceta, tomamos en cuenta sus características, quizá materiales y colores, pero, en nuestro
proyecto, tamaño y forma.
Las dimensiones de una maceta rectangular son alto, ancho y largo. Las dimensiones de
una maceta tubular son alto y diámetro. Será común de todas las formas de maceta: el
alto. Y, naturalmente, el nombre que decidimos darles a nuestras macetas para encariñarnos
más con ellas.
También, sea cual sea la forma de la maceta, su misión es la misma, acoger plantas.
Esto implica:
• atributos: nombre, alto y conjunto (Set) de plantas.
NOTA:
Hablaremos de conjuntos (Set), listas (List)… en el capítulo 15.
• constructor: recibe nombre y alto. Además de inicializar estos dos atributos con los
valores recibidos, inicializa el conjunto de plantas, para que esté listo para recibirlas.
La primera línea del constructor, super(), es una llamada al constructor del padre, es
decir, de Object. No es necesario ponerla, la escribamos o no, Java llamará al constructor
del padre antes de poder crear nuestro nuevo objeto. Sin embargo, en este ejemplo la
dejo para que seas consciente de que ese proceso se produce.
• getters: los siguientes métodos que encontramos en esa clase son los get de nombre,
volumen, profundidad y plantas. getNombre devuelve el nombre, sin misterio.
getProfundidad devuelve el alto, cambia un poco el nombre, pero tampoco tiene
mucho misterio. getPlantas devuelve las plantas, el conjunto de plantas, evidente.
Falta getVolumen, que es calculado (un poco simplificado, pues la forma de las
macetas no suele ser recta, la base suele ser más pequeña el borde, pero no lo
tendremos en cuenta para no complicarlo). El cálculo del volumen de una maceta es
su alto por su superficie. Lo que aún no sabemos es cuál es la superficie de la maceta,
porque eso dependerá de su forma, da igual. getSuperficie es un método que está
declarado en IMaceta, que en algún momento hemos de implementar, pero aún no.
No implementar getSuperficie es lo que nos obliga a hacer abstracta la clase Maceta,
pero no nos impide llamarlo desde aquí.
• superficieDisponible: es otro método que nos exige la interfaz, y la tenemos que
calcular. La superficie disponible de una maceta será la superficie total (getSuperficie)
menos la superficie ocupada, que calcularemos en un método aparte, privado, que
recorrerá todas las plantas y sumará la superficie que requiere cada una de ellas.
222 Proyecto «macetohuerto»
• volumenDisponible: más de lo mismo, pero con el volumen, en vez de con la superficie.
• plantar: este método prefiero dejarlo para más adelante, TODO.
• toString: para facilitar el seguimiento de los algoritmos durante las pruebas mejor
implementamos este método, en el que mostraremos los detalles de las macetas que
más nos interesen.
NOTA:
StringBuilder es una clase de Java que nos ayuda a construir Strings de forma más eficiente.
Puedes aprender más sobre ella en el capítulo 15.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public abstract class Maceta implements IMaceta {
private String nombre;
private int alto;
private Set<IPlanta> plantas;
public Maceta(String nombre, int alto) {
super();
[Link] = nombre;
[Link] = alto;
plantas = new HashSet<>();
}
@Override
public String getNombre() {
return nombre;
}
@Override
public int getVolumen() {
return alto * getSuperficie();
}
@Override
public int getProfundidad() {
return alto;
}
@Override
public Set<IPlanta> getPlantas() {
return plantas;
}
@Override
public int superficieDisponible() {
return getSuperficie() - superficieOcupada();
}
private int superficieOcupada() {
int res = 0;
for (IPlanta p : plantas) {
res += [Link]();
Clases de la estructura de macetas del macetohuerto 223
}
return res;
}
@Override
public int volumenDisponible() {
return getVolumen() - volumenOcupado();
}
private int volumenOcupado() {
int res = 0;
for (IPlanta p : plantas) {
res += [Link]();
}
return res;
}
@Override
public boolean plantar(IPlanta planta) {
// TODO Auto-generated method stub
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Maceta " + nombre + " [" + getForma() +
"] (sup " + getSuperficie() + "cm2 - vol " + getVolumen() + "cm3).\n");
if ([Link]()) {
[Link]("\t\tvacía\n");
}
for (IPlanta p : plantas) {
[Link]("\t\t" + p + "\n");
}
[Link]("\t\t>> Disponible sup " + superficieDisponible() + "cm2 - vol "
+ volumenDisponible() + "cm3");
return [Link]();
}
}
Clase MacetaRectangular
La mayor parte del trabajo ya está hecho en la clase de la que extendemos, Maceta, así
que MacetaRectangular será una clase pequeña, aunque totalmente necesaria, pues
recuerda que como Maceta es abstracta, es imposible crear objetos Maceta, por eso
necesitaremos las hijas.
Cuando hablamos de las dimensiones de las macetas, dijimos que las rectangulares se
definían con alto, ancho y largo. Por tanto:
• atributos: ancho y largo (alto se hereda).
• constructor: recibe nombre, alto, ancho y largo. nombre y alto se le pasan a la clase
madre, con super(nombre, alto).
• getSuperficie: el método que nos faltaba en Maceta. En el caso de una maceta
rectangular, su superficie es ancho por largo, fácil.
• getForma: simplemente devuelve uno de los valores del enumerado FormaMaceta.
No necesitamos guardarlo en un atributo.
224 Proyecto «macetohuerto»
package [Link];
import [Link];
public class MacetaRectangular extends Maceta {
private int ancho;
private int largo;
public MacetaRectangular(String nombre, int alto, int ancho, int largo) {
super(nombre, alto);
[Link] = ancho;
[Link] = largo;
}
@Override
public int getSuperficie() {
return ancho * largo;
}
@Override
public FormaMaceta getForma() {
return [Link];
}
}
Clase MacetaTubular
Hermana de MacetaRectangular, MacetaTubular es muy parecida. Simplemente,
consideraremos distinta lista de atributos para definir sus dimensiones, distinta forma
de calcular su superficie y distinta forma.
Para el cálculo de la superficie, π * r2, es decir, [Link] * (diametro/2) * (diametro/2).
Pero este número no es entero, sino decimal, más concretamente double. Por eso debemos
hacer un casting a entero poniendo (int) delante para transformarlo.
La constante PI la tomamos de la clase Math, del paquete [Link], que para estos casos
la crearon.
package [Link];
import [Link];
public class MacetaTubular extends Maceta {
private int diametro;
public MacetaTubular(String nombre, int alto, int diametro) {
super(nombre, alto);
[Link] = diametro;
}
@Override
public int getSuperficie() {
return (int)([Link] * (diametro/2) * (diametro/2));
}
@Override
public FormaMaceta getForma() {
return [Link];
}
}
Clases de la estructura de macetas del macetohuerto 225
Clases de la estructura de plantas del
macetohuerto
En el paquete [Link] tenemos la implementación de Planta, las cuatro clases de
las distintas familias de plantas y las clases para cada una de las plantas que
implementaremos.
Clase abstracta Planta
La clase Planta implementará la interfaz IPlanta y podrá implementar todos los métodos.
Sin embargo, Planta será una clase abstracta, porque queremos evitar que se puedan
crear instancias de la Planta sin saber concretamente qué planta es.
Al crear la clase Planta, al decir que implementa IPlanta, el propio entorno de desarrollo
nos genera un esqueleto de la clase con los métodos que hemos de implementar. Pero
tenemos que implementarlos.
Pensemos primero en los atributos de las plantas:
• nombre: una cadena de texto que nos ayudará a personalizar cada plantita.
• superficieRequerida: un número entero que nos indicará, en cm2, la superficie de la maceta
que debemos reservar para cada planta.
• volumenRequerido: otro entero que nos indicará, en cm3, el volumen de sustrato que
reservaremos.
• fechaSiembra: una fecha (clase Date del paquete [Link]) en la que registraremos en qué
momento se plantó una semilla (o planta en estado embrionario).
• maceta: una IMaceta en la que guardaremos en qué maceta ha sido plantada esa planta.
• especie: un valor del enumerado Especie que indica de qué especie es cada planta.
• familia: un valor del enumerado Familia que especifica de qué familia es cada planta.
• incompatibles: conjunto (clase Set de [Link]) de Especie incompatibles.
• compatibles: conjunto (clase Set de [Link]) de Especie compatibles.
NOTA:
Cuando declaramos un atributo, un parámetro, una variable, intentaremos ser lo más genéricos
posible, por eso declaramos la maceta como IMaceta y los conjuntos como Set. En el momento
de crear el objeto (al hacer el new), concretaremos la clase (MacetaRectangular o Tubular, o
HashSet, por ejemplo).
Siempre intentaremos darles la mínima visibilidad posible a los atributos, así que
empezaremos por private, pero algunos necesitaremos manipularlos desde las clases
hijas, así que han de ser protected.
226 Proyecto «macetohuerto»
fechaSiembra y maceta se establecerán en el momento en el que se siembre la planta,
mientras que nombre, superficieRequerida y volumenRequerido los inicializaremos en
el constructor. Estos se pueden mantener como privados.
incompatibles y compatibles se inicializarán como conjuntos vacíos en el constructor de
planta, que será llamado antes que el constructor de las clases hijas. Será en el constructor
de las clases concretas en el que aprovecharemos para añadir los valores correspondientes.
especie y familia se establecerán en el constructor en el que radique esa información.
Estos atributos serán protegidos.
Pero, por convención, se suelen ordenar los atributos de mayor a menor visibilidad, así
que pondremos los protegidos por encima de los privados.
El constructor, que no podrá ser llamado directamente, porque la clase es abstracta, pero
sí por los constructores de las clases hijas, lo implementaremos así:
• protected: pues no necesitamos que sea público, solo las hijas podrán llamarlo.
• superficieRequerida: la establecemos como el cuadrado de la distancia recibida. Lo
convenimos así con el cliente.
• volumenRequerido: cuidado con las unidades, recibimos el volumen en litros,
pero lo guardamos para facilitar las cuentas en cm3, así que multiplicaremos la
cifra recibida por mil.
• incompatibles y compatibles: inicializamos el conjunto como un new HashSet().
Entre los métodos que declaramos en la interfaz y que hay que implementar se hallan:
• esCompatible, lo veremos un poco más adelante.
• tengoEspacio, también lo dejamos para luego.
• plantar, este es fácil, cuando una planta recibe el mensaje de ser plantada en una
maceta, ponemos la fecha de siembra a la fecha actual (new Date()) y, guardando en
el atributo maceta (this), la maceta recibida como parámetro.
• getters y setters: de cada uno de los atributos nombre, superficieRequerida,
volumenRequerido, especie y familia.
• toString, como para las macetas, para facilitar el seguimiento de los algoritmos
durante las pruebas mejor implementamos este método, en el que mostraremos los
detalles de las plantas que más nos interesen; pero, cuidado, no pintaremos la maceta
entera, si existe, sino solo su nombre, que no queremos entrar en un bucle infinito
(recuerda que en el toString de la maceta pintamos la planta).
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
Clases de la estructura de plantas del macetohuerto 227
public abstract class Planta implements IPlanta {
protected Especie especie;
protected Familia familia;
protected Set<Especie> incompatibles;
protected Set<Especie> compatibles;
private String nombre;
private int superficieRequerida;
private int volumenRequerido;
private Date fechaSiembra;
private IMaceta maceta;
protected Planta(String nombre, int distancia, int litros) {
[Link] = nombre;
superficieRequerida = distancia^2;
volumenRequerido = litros * 1000;
incompatibles = new HashSet<>();
compatibles = new HashSet<>();
}
@Override
public boolean esCompatible(IPlanta planta) {
// TODO Auto-generated method stub
}
@Override
public boolean tengoEspacio(IMaceta maceta) {
// TODO Auto-generated method stub
}
@Override
public void plantar(IMaceta maceta) {
fechaSiembra = new Date(); // hoy
[Link] = maceta;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
[Link] = nombre;
}
public int getSuperficieRequerida() {
return superficieRequerida;
}
public void setSuperficieRequerida(int superficieRequerida) {
[Link] = superficieRequerida;
}
public int getVolumenRequerido() {
return volumenRequerido;
}
public void setVolumenRequerido(int volumenRequerido) {
[Link] = volumenRequerido;
}
public Especie getEspecie() {
return especie;
}
228 Proyecto «macetohuerto»
public void setEspecie(Especie especie) {
[Link] = especie;
}
public Familia getFamilia() {
return familia;
}
public void setFamilia(Familia familia) {
[Link] = familia;
}
@Override
public String toString() {
return "Planta " + nombre + " [especie=" + especie + ", familia=" + familia
+ ", superficieRequerida="
+ superficieRequerida + ", volumenRequerido="
+ volumenRequerido + ", incompatibles=" + incompatibles
+ ", fechaSiembra=" + fechaSiembra
+ (maceta != null ? ", maceta=" + [Link]() : "")
+ "]";
}
}
Clase abstracta PlantaAromatica
Las plantas aromáticas no tienen nada de particular, más que el hecho de ser plantas
aromáticas, así que solo implementaremos el constructor (que no se hereda) y que con
los datos recibidos llamará al constructor de Planta, y luego fijará el atributo familia,
tomando el valor adecuado del enumerado Familia.
Haremos abstracta esta clase por el mismo motivo por el que hicimos abstracta la clase
Planta: es imposible crear una PlantaAromatica sin saber qué planta en concreto es.
package [Link];
import [Link];
public abstract class PlantaAromatica extends Planta {
protected PlantaAromatica(String nombre, int distancia, int litros) {
super(nombre, distancia, litros);
familia = [Link];
}
}
Clase abstracta PlantaFruto
Exactamente lo mismo aplica al resto de clases de este nivel.
package [Link];
import [Link];
public abstract class PlantaFruto extends Planta {
protected PlantaFruto(String nombre, int distancia, int litros) {
super(nombre, distancia, litros);
familia = [Link];
}
}
Clases de la estructura de plantas del macetohuerto 229
Clase abstracta PlantaHoja
package [Link];
import [Link];
public abstract class PlantaHoja extends Planta {
protected PlantaHoja(String nombre, int distancia, int litros) {
super(nombre, distancia, litros);
familia = [Link];
}
}
Clase abstracta PlantaRaiz
Pero las plantas de raíz tienen una particularidad: requieren una profundidad mínima
para crecer y por eso:
• necesitarán un atributo adicional profundidad requerida,
• recibirán el parámetro en el constructor,
• sobrescribirán el método tengoEspacio para tener en cuenta este nuevo requisito
(esto lo veremos de aquí a unas páginas).
package [Link];
import [Link];
import [Link];
public abstract class PlantaRaiz extends Planta {
private int profunidadRequerida;
protected PlantaRaiz(String nombre, int distancia, int litros, int profundidad) {
super(nombre, distancia, litros);
profunidadRequerida = profundidad;
familia = [Link];
}
public int getProfunidadRequerida() {
return profunidadRequerida;
}
public void setProfunidadRequerida(int profunidadRequerida) {
[Link] = profunidadRequerida;
}
}
Clase Hinojo
Empezamos con las clases concretas para cada una de las especies de plantas que vamos
a implementar. Estas clases ya no serán abstractas, pues son de las que queremos crear
instancias. Por cada una de las instancias que creemos, solo precisaremos conocer el
nombre que le daremos a la futura plantita, el resto de atributos serán iguales para todas
las semillas de hinojo.
230 Proyecto «macetohuerto»
• Así que lo primero que hay que hacer en el constructor es llamar al constructor del
padre, que recibirá ese nombre, la distancia entre plantas y el volumen en litros de
sustrato que requiere cada plantita para crecer bien. En el caso del Hinojo, según la
tabla 9.1, son 10 cm y 8 litros.
• Fijamos el atributo especie con el valor que toque del enumerado Especie.
• Seguimos completando las listas de compatibilidades e incompatibilidades, que
podemos consultar en la tabla 9.2. El Hinojo es incompatible con el Tomate y la
Zanahoria, pero compatible con la Lechuga.
package [Link];
import [Link];
public class Hinojo extends PlantaAromatica {
public Hinojo(String nombre) {
super(nombre, 10, 8);
especie = [Link];
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
}
}
Clase Lechuga
package [Link];
import [Link];
public class Lechuga extends PlantaHoja {
public Lechuga(String nombre) {
super(nombre, 12, 22);
especie = [Link];
[Link]([Link]);
[Link]([Link]);
}
}
Clase Perejil
package [Link];
import [Link];
public class Perejil extends PlantaAromatica {
public Perejil(String nombre) {
super(nombre, 8, 5);
especie = [Link];
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
}
}
Clases de la estructura de plantas del macetohuerto 231
Clase Tomate
La clase Tomate es un poco distinta. De momento empezamos con una implementación
igual a la del resto de plantas.
package [Link];
import [Link];
public class Tomate extends PlantaFruto {
public Tomate(String nombre) {
super(nombre, 30, 18);
especie = [Link];
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
}
}
Clase TomateCherry
Los TomateCherry son como los Tomate, pero más pequeños. Eso significa que son la
misma especie y tienen las mismas compatibilidades e incompatibilidades. Por eso, su
implementación es tan sencilla. TomateCherry extiende Tomate y su constructor solo
tiene que llamar al de Tomate, pasándole sus propias cifras de los requisitos: 25 cm de
distancia entre plantas y 15 litros de sustrato.
package [Link];
public class TomateCherry extends Tomate {
public TomateCherry(String string) {
super(string, 25, 15);
}
}
Nueva implementación de la clase Tomate
Hemos visto que TomateCherry requiere pasarle al constructor de Tomate la distancia y
el volumen. Lo que haremos es disponer de dos constructores en Tomate:
• uno público, que reciba el nombre de la planta, como en el resto de plantas, que es
el que usaremos para crear nuevos tomates,
• y uno protegido que pueda ser llamado desde la clase hija TomateCherry para hacerle
llegar los valores adecuados para los tomatitos.
Para no duplicar el código, lo que hacemos es que sea el constructor protegido el que
«haga todo el trabajo» y lo llamamos desde el público usando this().
232 Proyecto «macetohuerto»
El código de esta clase quedaría así:
package [Link];
import [Link];
public class Tomate extends PlantaFruto {
public Tomate(String nombre) {
this(nombre, 30, 18);
}
protected Tomate(String nombre, int superficie, int volumen) {
super(nombre, superficie, volumen);
especie = [Link];
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
}
}
Clase Zanahoria
Las Zanahoria son PlantaRaiz, así que cuentan con un requisito más que las plantas del
resto de familias. ¿En qué se nota eso en su implementación? Solamente en que le pasamos
un dato más al constructor de su padre: los 25 cm de profundidad requerida. Pero nada
más, porque será la clase abstracta PlantaRaiz quien se encargue de implementar lo que
sea necesario para considerar ese requisito.
package [Link];
import [Link];
public class Zanahoria extends PlantaRaiz {
public Zanahoria(String nombre) {
super(nombre, 10, 3, 25);
especie = [Link];
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
}
}
El huerto y sus procesos
Ya disponemos de la mayor parte del código del macetohuerto, pero nos faltan algunos
métodos esenciales que «dejamos para más adelante». Pues ya hemos llegado a ese momento.
Clase Huerto
Empezamos la clase Huerto, que no hemos escrito aún. Implementa la interfaz IHuerto
y sus dos métodos, addMaceta y plantar.
El huerto y sus procesos 233
El huerto tiene:
• un nombre.
• un conjunto ([Link]) de macetas.
• un constructor que recibe el nombre y también inicializa el conjunto.
• addMaceta, que añade una Maceta al conjunto de macetas.
• plantar, recorre el conjunto de macetas y por cada una intenta plantar la planta. Si lo
consigue, devuelve la maceta en la que se ha plantado la planta; si no, lo intenta con
la siguiente. Si no lo consigue en ninguna, devuelve null.
• toString, como en el resto de clases en las que lo hemos implementado, sacaremos
la información que nos resulte interesante, imprimiendo en este caso el nombre del
huerto y la lista de macetas.
package huerto;
import [Link];
import [Link];
public class Huerto implements IHuerto {
private String nombre;
private Set<IMaceta> macetas;
public Huerto(String nombre) {
[Link] = nombre;
macetas = new HashSet<>();
}
@Override
public void addMaceta(IMaceta maceta) {
[Link](maceta);
}
@Override
public IMaceta plantar(IPlanta planta) {
for (IMaceta maceta : macetas) {
if ([Link](planta)) {
return maceta;
}
}
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
[Link]("Huerto: " + nombre + "\n");
for (IMaceta maceta : macetas) {
[Link]("\t" + maceta + "\n");
}
return [Link]();
}
}
234 Proyecto «macetohuerto»
Método plantar de la clase Maceta
El huerto intenta plantar una planta en una maceta, pero eso es responsabilidad de la Maceta.
• Lo primero que comprobaremos es si la nueva planta que queremos plantar, el
atributo planta, es compatible con cada una de las plantas, p, que ya hay entre las
plantas de la maceta.
• Solo si es compatible con todas, comprobamos si la planta tiene espacio en esta
maceta, en this. En esta ocasión será responsabilidad de la planta decidir si «le gusta»
la maceta. Si no fuera compatible, no vale la pena hacer más comprobaciones.
• Si tiene espacio, si cabe, añadimos la planta a la colección de plantas de la maceta y
llamamos al método plantar de la planta, pasándole la maceta, this, de nuevo,
delegando responsabilidades.
@Override
public boolean plantar(IPlanta planta) {
[Link]("--- PLANTANDO " + [Link]() + " EN "
+ [Link]());
boolean compatiblesOk = true;
for (IPlanta p : plantas) {
boolean compatibleOk = [Link](p);
if (!compatibleOk) {
[Link]("--- " + [Link]() + " no es compatible con "
+ [Link]());
}
compatiblesOk &= compatibleOk;
}
boolean cabe = false;
if (compatiblesOk) {
cabe = [Link](this);
}
if (cabe) {
[Link](planta);
[Link](this);
}
return cabe;
}
NOTA:
Incluimos trazas en el código, utilizando [Link], para poder seguir, por la consola,
lo que va sucediendo.
El huerto y sus procesos 235
Método esCompatible de la clase Planta
La compatibilidad entre plantas es su responsabilidad. El algoritmo que hemos definido
es el que nos ha pedido el cliente, válido solo para este ejemplo:
• si las plantas son de la misma especie, son compatibles (podemos plantar dos
zanahorias en la misma maceta);
• si no, si la especie de planta no está dentro del conjunto de especies compatibles de
esta planta,
• solo serán compatibles si no son de la misma familia
• y la especie de planta no está en la lista de incompatibles.
Quizá aún te cuesta leer un poco este código, así que voy a explicarlo con detalle. Este
método está dentro de la clase Planta, con lo que se ejecutará sobre una instancia de una
planta (this), comparándolo con la planta recibida como argumento.
Iremos comparando propiedades de ambas plantas. Cuando hacemos especie.
equals([Link](), especie hace referencia al atributo especie de la planta this,
que es un atributo privado. Por tanto, para acceder a la especie de la planta planta,
debemos utilizar el método getEspecie, es imposible acceder directamente al atributo.
Lo mismo nos sucede para comparar las familias.
@Override
public boolean esCompatible(IPlanta planta) {
boolean compatible = true;
if ()) {
if ()) {
compatible &= );
compatible &= );
}
}
return compatible;
}
Método tengoEspacio de la clase Planta
También es responsabilidad de la planta determinar si tiene o no espacio en una maceta.
• Primero comprobamos si la superficie disponible en la maceta (que será calculada,
cómo no, por la maceta) es mayor que la superficie que requiere la planta.
• Por otro lado, miraremos si el volumen disponible (también calculado por la maceta)
es mayor que el requerido por la planta.
• Devolvemos la conjunción de ambos valores.
@Override
public boolean tengoEspacio(IMaceta maceta) {
boolean superficieOk = [Link]() > getSuperficieRequerida();
if (!superficieOk) {
[Link]("--- Superficie ko para " + getNombre() + " en "
+ [Link]());
}
236 Proyecto «macetohuerto»
boolean volumenOk = [Link]() > getVolumenRequerido();
if (!volumenOk) {
[Link]("--- Volumen ko para " + getNombre() + " en "
+ [Link]());
}
return superficieOk && volumenOk;
}
NOTA:
Podríamos optimizar el código comprobando el volumen solamente si la superficie es ok, pero
en ese caso perderíamos la posibilidad de añadir la traza sobre si el volumen falla o no, y nos
costaría más depurar el programa. Según las necesidades (eficiencia vs. claridad), implementaremos
de una forma o de otra.
Método tengoEspacio de la clase PlantaRaiz
Pero tenemos un caso especial de plantas a las que no le vale la implementación genérica
de tengoEspacio. Las plantas de raíz necesitan comprobar también si la maceta tiene
profundidad suficiente para satisfacer los requisitos de la planta.
Hacemos la verificación de la profundidad como la hemos hecho para superficie y
volumen. Y, luego, sacando partido a la herencia, para comprobar superficie y volumen
llamamos al método tengoEspacio del padre (super), aprovechando su implementación,
reutilizando el código. Nada de cortar y pegar.
@Override
public boolean tengoEspacio(IMaceta maceta) {
boolean profundidadOk = [Link]() > profunidadRequerida;
if (!profundidadOk) {
[Link]("--- Profundidad ko para " + getNombre() + " en "
+ [Link]());
}
return [Link](maceta) && profundidadOk;
}
¿Sabrías decirme la diferencia entre
return [Link](maceta) && profundidadOk;
y
return profundidadOk && [Link](maceta);
?
Podrían parecer equivalentes, pero existe una diferencia sutil. En ambas soluciones
hacemos un and entre dos booleanos. Ambos han de ser ciertos para que el resultado sea
cierto, así que, a priori, el orden de los factores no debería importar. Pero Java es un
lenguaje perezoso y eso significa que, si el primer operando de un and es falso, ya no se
esfuerza en evaluar el segundo operando, tenemos el falso garantizado. Si fuera un or,
no evaluaría el segundo operando si el primero ya fuera cierto, pues con un cierto es
suficiente para dar por cierto un or.
El huerto y sus procesos 237
Así pues, la diferencia entre estas dos líneas sería que la primera, la que hemos utilizado
en nuestro código, es menos eficiente, porque siempre va a llamar a tengoEspacio,
mientras que en la segunda solo llamaremos a ese método si la profundidad es ok.
¿Por qué he escogido entonces el código menos eficiente? Por el mismo motivo que
indicaba en el apartado anterior: para disponer de las trazas y saber por qué motivo una
planta ha rechazado una maceta.
Pruebas
Salvo error u omisión, ya está completo el código del huerto, así que llegó el momento
de probarlo.
Clase Sistema
Creamos una clase Sistema, con un método main, en la que crearemos un huerto, tres
macetas y unas cuantas plantas, para luego plantarlas.
Escribimos un método auxiliar para trazar el resultado de cada operación.
package huerto;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class Sistema {
private static final int NUM_ZANAHORIAS = 5;
public static void main(String[] args) {
Huerto miHuerto = new Huerto("Mi primer huerto");
Maceta cuadrada = new MacetaRectangular("Cuadradita", 20, 20, 20);
Maceta botella = new MacetaTubular("Botella", 25, 15);
Maceta maceton = new MacetaTubular("Macetón", 40, 40);
[Link](cuadrada);
[Link](botella);
[Link](maceton);
[Link]("Mi huerto nuevito: " + miHuerto);
IPlanta tomatitos = new TomateCherry("tomatitos");
IPlanta acelga = new Lechuga("hojas verdes");
IPlanta zanahoria = new Zanahoria("larguita");
IPlanta perejil = new Perejil("verdito");
IPlanta hinojo = new Hinojo("hijo sin ojo");
List<Zanahoria> zanahorias = new ArrayList<>();
for (int i = 0; i < NUM_ZANAHORIAS; i++) {
Zanahoria z = new Zanahoria("z" + i);
238 Proyecto «macetohuerto»
[Link](z);
}
IMaceta macetaTomatitos = [Link](tomatitos);
pintaResultadoPlantar(tomatitos, macetaTomatitos);
IMaceta macetaAcelga = [Link](acelga);
pintaResultadoPlantar(acelga, macetaAcelga);
IMaceta macetaZanahoria = [Link](zanahoria);
pintaResultadoPlantar(zanahoria, macetaZanahoria);
IMaceta macetaPerejil = [Link](perejil);
pintaResultadoPlantar(perejil, macetaPerejil);
IMaceta macetaHinojo = [Link](hinojo);
pintaResultadoPlantar(hinojo, macetaHinojo);
for (Zanahoria z : zanahorias) {
IMaceta macetaZ = [Link](z);
pintaResultadoPlantar(z, macetaZ);
}
[Link]("Mi huerto con muchas plantas: " + miHuerto);
}
private static void pintaResultadoPlantar(IPlanta planta,
IMaceta maceta) {
if (maceta != null) {
[Link]("He plantado " + [Link]() + " en "
+ [Link]());
} else {
[Link]("No he podido plantar " + [Link]());
}
}
}
No tiene mucho misterio este método, lo podemos ejecutar y ver cómo resulta:
Mi huerto nuevito: Huerto: Mi primer huerto
Maceta Macetón [TUBULAR] (sup 1256cm2 - vol 50240cm3).
vacía
>> Disponible sup 1256cm2 - vol 50240cm3
Maceta Cuadradita [RECTANGULAR] (sup 400cm2 - vol 8000cm3).
vacía
>> Disponible sup 400cm2 - vol 8000cm3
Maceta Botella [TUBULAR] (sup 153cm2 - vol 3825cm3).
vacía
>> Disponible sup 153cm2 - vol 3825cm3
--- PLANTANDO tomatitos EN Macetón
He plantado tomatitos en Macetón
--- PLANTANDO hojas verdes EN Macetón
He plantado hojas verdes en Macetón
--- PLANTANDO larguita EN Macetón
He plantado larguita en Macetón
--- PLANTANDO verdito EN Macetón
--- hojas verdes no es compatible con verdito
--- larguita no es compatible con verdito
--- PLANTANDO verdito EN Cuadradita
He plantado verdito en Cuadradita
--- PLANTANDO hijo sin ojo EN Macetón
--- tomatitos no es compatible con hijo sin ojo
--- larguita no es compatible con hijo sin ojo
--- PLANTANDO hijo sin ojo EN Cuadradita
Pruebas 239
--- verdito no es compatible con hijo sin ojo
--- PLANTANDO hijo sin ojo EN Botella
--- Volumen ko para hijo sin ojo en Botella
No he podido plantar hijo sin ojo
--- PLANTANDO z0 EN Macetón
He plantado z0 en Macetón
--- PLANTANDO z1 EN Macetón
He plantado z1 en Macetón
--- PLANTANDO z2 EN Macetón
He plantado z2 en Macetón
--- PLANTANDO z3 EN Macetón
--- Volumen ko para z3 en Macetón
--- PLANTANDO z3 EN Cuadradita
--- verdito no es compatible con z3
--- PLANTANDO z3 EN Botella
--- Profundidad ko para z3 en Botella
No he podido plantar z3
--- PLANTANDO z4 EN Macetón
--- Volumen ko para z4 en Macetón
--- PLANTANDO z4 EN Cuadradita
--- verdito no es compatible con z4
--- PLANTANDO z4 EN Botella
--- Profundidad ko para z4 en Botella
No he podido plantar z4
Mi huerto con muchas plantas: Huerto: Mi primer huerto
Maceta Macetón [TUBULAR] (sup 1256cm2 - vol 50240cm3).
Planta z0 [especie=ZANAHORIA, familia=RAIZ, superficieRequerida=8,
volumenRequerido=3000, incompatibles=[PEREJIL, HINOJO], fechaSiembra=Fri Mar 05
19:58:40 CET 2021, maceta=Macetón]
Planta z2 [especie=ZANAHORIA, familia=RAIZ, superficieRequerida=8,
volumenRequerido=3000, incompatibles=[PEREJIL, HINOJO], fechaSiembra=Fri Mar 05
19:58:40 CET 2021, maceta=Macetón]
Planta z1 [especie=ZANAHORIA, familia=RAIZ, superficieRequerida=8,
volumenRequerido=3000, incompatibles=[PEREJIL, HINOJO], fechaSiembra=Fri Mar 05
19:58:40 CET 2021, maceta=Macetón]
Planta hojas verdes [especie=LECHUGA, familia=HOJA, superficieRequerida=14,
volumenRequerido=22000, incompatibles=[], fechaSiembra=Fri Mar 05 19:58:40 CET
2021, maceta=Macetón]
Planta tomatitos [especie=TOMATE, familia=FRUTO, superficieRequerida=27,
volumenRequerido=15000, incompatibles=[HINOJO], fechaSiembra=Fri Mar 05 19:58:40
CET 2021, maceta=Macetón]
Planta larguita [especie=ZANAHORIA, familia=RAIZ, superficieRequerida=8,
volumenRequerido=3000, incompatibles=[PEREJIL, HINOJO], fechaSiembra=Fri Mar 05
19:58:40 CET 2021, maceta=Macetón]
>> Disponible sup 1183cm2 - vol 1240cm3
Maceta Cuadradita [RECTANGULAR] (sup 400cm2 - vol 8000cm3).
Planta verdito [especie=PEREJIL, familia=AROMATICA, superficieRequerida=10,
volumenRequerido=5000, incompatibles=[ZANAHORIA, LECHUGA], fechaSiembra=Fri Mar
05 19:58:40 CET 2021, maceta=Cuadradita]
>> Disponible sup 390cm2 - vol 3000cm3
Maceta Botella [TUBULAR] (sup 153cm2 - vol 3825cm3).
vacía
>> Disponible sup 153cm2 - vol 3825cm3
240 Proyecto «macetohuerto»
Gracias a las trazas puedes hacer un seguimiento de por qué ha distribuido así las plantas
en las macetas. Por ejemplo, el «hijo sin ojo» no pudo ser plantado porque macetón y
cuadradita tenían ya plantas no compatibles con el hinojo y en la botella no cabía. z3 y
z4 tampoco se han podido plantar, pero por problemas de espacio. Pero no deberías
conformarte con que yo te lo diga. Comprueba que realmente mis suposiciones son
correctas, pues quizá hay alguna errata en el código y no la hemos visto.
Si este código fuera a pasar a producción, deberíamos quitar todas esas trazas, pues solo
las queríamos para cerciorarnos de que todo funciona bien.
NOTA:
En el capítulo 11 aprenderemos a depurar y así poder seguir en vivo qué está sucediendo.
En el capítulo 12 descubriremos los test unitarios, una forma más profesional de probar el
código, y en el 13 cómo dejar trazas de ejecución, que a veces son necesarias, pero sin usar
los [Link] de los aprendices.
Pruebas 241
3
Parte
Parte
Buenas
prácticas
10 Manejo de excepciones
En este capítulo aprenderás a:
• Identificar qué son las excepciones.
• Manejar las excepciones.
• Reconocer las malas prácticas en el manejo de excepciones.
Introducción
En este capítulo intentaré ayudarte a entender las excepciones en Java: qué son y también,
muy importante, cómo manejarlas.
Llevo muchos años programando en Java. He participado en muchos proyectos en equipos
más o menos grandes y, por lo general, por una cosa u otra, el código resultante siempre
anda un poco mal de calidad. A menudo es por falta de tiempo para pensar y darle un par
de vueltas a las cosas, pero muy frecuentemente el problema viene de que algunos conceptos
se nos enseñaron rápido y mal, y no somos capaces de aplicarlos correctamente.
También he sido formadora de jóvenes ingenieros, en varias ediciones, y fueron los
propios alumnos quienes me pidieron profundizar más en las excepciones.
Para ser los reyes de las excepciones tendremos que empezar con un poco de teoría. Lo
siento. Intentaré ser clara y breve, pero hay que pasar por ello.
Seguiremos viendo cómo podemos manejarlas en nuestro código, descubriremos sus
palabras clave (throws, throw, try, catch, finally) y lo más importante: cuándo y por qué
usar unas u otras.
Para corroborar que has asimilado todo y has entendido bien las excepciones en Java,
tengo preparados unos cuantos ejercicios en los que tendrás que reflexionar sobre la
corrección del tratamiento que se está haciendo.
Para rematar, haremos un proyecto sencillo pero completo, en el que pondrás en práctica
lo aprendido. Juntos nos aseguraremos de que todo el mundo lance y reciba lo merecido.
Estoy convencida de que, tras terminar este capítulo, nunca más tendrás miedo a tratar
los errores que se puedan presentar en tus programas.
¿Qué son las excepciones?
¿Qué es una excepción?
Las excepciones son el mecanismo que tiene Java (y otros muchos lenguajes de
programación) para manejar situaciones que influyen en el normal funcionamiento de
un programa. Señalan que se ha producido un error durante la ejecución del programa
que impide seguir con su flujo normal.
No hay que confundirlas con un error de compilación, que se produce cuando cometemos
algún error sintáctico en el código (llamamos a un método que no existe, lo llamamos
con menos parámetros de los que espera, intentamos asignar un texto a una variable
numérica, nos olvidamos de cerrar un paréntesis o de poner un punto y coma). Hoy en
día, los entornos de desarrollo nos suelen avisar de los errores de compilación en cuanto
los cometemos, subrayándolos en rojo o marcándolos de alguna otra manera.
246 Manejo de excepciones
Figura 10.1. Ejemplo de error de compilación en Eclipse.
Sin embargo, las excepciones, aunque las trataremos escribiendo código, realmente solo
se producen en tiempo de ejecución.
Antiguamente, en lenguajes de más bajo nivel, se usaba el retornar el valor -1 para indicar
que algo había ido mal, con lo que los programas se llenaban de if (returnValue != -1) { …}.
Hoy en día, las excepciones nos permiten evitar ese farragoso código y centrarnos en el
flujo normal de nuestro programa.
T10.1
Entender y manejar bien las excepciones nos ayudará a sacarles el máximo partido y a
asegurarnos, en la medida de lo posible, que nuestros programas funcionen correctamente,
proporcionando la mejor experiencia a nuestros usuarios, pues el correcto manejo de las T10.2
excepciones nos brinda un modo robusto y orientado a objetos para tratar escenarios de error.
Tipos de excepciones
Ya hemos visto qué son las excepciones o, más bien, su definición. Veamos ahora los tipos
que se encuentran en Java.
Aunque en general hablamos de excepciones, realmente, hablando con propiedad, lo suyo
sería hablar de Throwables (traducido, 'lanzables'), pues esta es la clase madre, de la que
heredan otras dos clases: Error y Exception. Sus nombres ya nos dan buenas pistas, ¿no?
Figura 10.2. Diagrama de clases con algunos errores y excepciones.
¿Qué son las excepciones? 247
Efectivamente, la clase Error se usa para aquellas situaciones en las que el problema está
fuera del ámbito de nuestra aplicación. Se utiliza para esos problemas a los que es
imposible anticiparnos, ni hacer nada para recuperarnos de ellos. Por ejemplo, si se
produce un fallo hardware, si peta la máquina virtual o hay una falta de memoria.
Figura 10.3. Descripción de la clase Error en el API de Java.
Raramente nos encontraremos directamente con un Error (sí, en mayúscula y en inglés).
Normalmente, nos toparemos con alguno de sus hijos: VirtualMachineError, IOError,
AssertionError... Puedes consultar en el API de Java las clases de Error predefinidas.
Figura 10.4. Jerarquía de la clase Error en el API de Java.
Cuando se produce un Error en nuestro programa lo único que podemos hacer, si eso, es
intentar salir con la mayor dignidad posible: es decir, como mucho, preparando un mensaje
de error decente para nuestro usuario, con el grado de seriedad que le corresponda a
nuestra aplicación. Ya sabes, un simple «Se ha producido un error. Por favor, inténtelo de
nuevo más tarde» o un divertido «¡Ups! ¡¡Se ha roto algo, pero yo no he sido!!».
248 Manejo de excepciones
Figura 10.5. Ejemplo de mensaje de error formal, en YouTube.
Figura 10.6. Ejemplo de mensaje de error informal, ilustración de dreamstime.
La clase hermana de Error es Exception y, sí, es la famosa de la familia. ¡Pobres Errors,
nadie se acuerda de ellos, ni para tenerles miedo, y eso que son mucho más fieros!
Figura 10.7. Descripción de la clase Exception en el API de Java.
¿Qué son las excepciones? 249
Exception tiene muchas clases hijas, pero hay una especial entre todas ellas:
RuntimeException, que a su vez, tiene otras muchas hijas.
Figura 10.8. Jerarquía de la clase Exception en el API de Java.
Así pues, las excepciones se clasifican en dos tipos: las runtime y el resto también llamadas
checked. En la figura 10.2 se aprecian con el contorno liso las excepciones checked y con el
T10.3
contorno punteado las excepciones runtime. La diferencia entre unas y otras es la siguiente:
las checked exception son de declaración obligatoria, es decir, no podemos pasar de ellas
en el código. Tendremos que hacer algo. Obligatorio. ¿El qué? Eso ya lo veremos más
T10.4 adelante. Sin embargo, con las runtime no tenemos esa obligación. Podemos tratarlas
exactamente como las checked, pero también hacer como si no estuvieran allí.
Runtime Exceptions
Decíamos que la diferencia entre runtime y checked exceptions es que las checked hay que
tratarlas, mientras que las runtime pueden ser tratadas o no.
250 Manejo de excepciones
Veamos algunos ejemplos de RuntimeException. Puedes encontrar muchos más en el
API de Java, pero con las que te cruzarás más a menudo son:
• ClassCastException: esta excepción se va a producir cada vez que intentemos
convertir un objeto de un tipo en un objeto de otro tipo no compatible. Por ejemplo,
no tendremos problemas en convertir un String en Object, porque todo son objetos
en Java, pero...
¿Eres valiente? ¿Crea un número en una variable Object e intenta hacerle un casting
a un String!
Object x = new Integer(0);
[Link]((String)x);
• IndexOutOfBoundsException: Índice fuera de límites exception… Si nos lo está diciendo:
esta se produce cuando, en una serie de elementos, intentamos acceder a una posición
que no existe. Tiene dos hijas conocidas, sus variedades para String y Array.
¿Quieres conocer a una de ellas? Intenta acceder a la posición 7 de un array de 5
elementos.
int[] a = {1, 2, 3, 4, 5};
int num = a[7];
¿O a la otra? Intenta acceder a la posición 9 del String "Hola!".
String s = "Hola!";
char c = [Link](9);
Y ahora, la reina de las runtime, la más temida de todas:
• NullPointerException: seguro que ya la conoces. De pequeñita me contaron que en
Java no había punteros (qué maravilla, librarse de los malditos punteros de C), pero
resulta que no era así. Todo en Java son punteros, aunque no los manejemos nosotros
directamente. Esta excepción nos lo recuerda a menudo y viene cuando intentas
pedirle algo a un objeto nulo.
Object nada = null;
[Link](); E10.1
ADVERTENCIA:
E10.2
Estos fragmentos de código propuestos no funcionarán. Lanzarán las excepciones mencionadas.
Checked Exceptions
Las checked exception son todas aquellas excepciones que son hijas, nietas, etcétera, de
Exception, pero no lo son de RuntimeException.
Sí, vale, quizá si lo hubiera diseñado yo habría creado una clase CheckedException,
hermana de RuntimeException, para que esta parte fuera más fácil de explicar, pero no,
no tuve ese honor, así que… son checked exception todas las Exception que no son
RuntimeException.
¿Qué son las excepciones? 251
errors
checked exceptions
runtime exceptions
Figura 10.9. Checked exceptions, runtime exceptions y errors de la figura 10.2 identificados.
Vamos a mencionar algunas de las más conocidas. De nuevo te invito a que navegues un poco
por el API de Java para ver qué excepciones existen. Pero, en cualquier caso, esas no son todas,
ya que cualquier desarrollador (el de las librerías más populares o tú mismo) puede crear las
excepciones que necesite.
• IOException: que a su vez tiene unas cuantas hijas: FileNotFoundException, EOFException
(EOF, por End of File, fin de fichero), MalformedURLException… Todas estas nos avisan
de que ha sucedido algún problema de entrada/salida, es decir, al intentar leer un fichero
(no lo encuentra o ya se ha terminado), al acceder a un recurso por internet (la URL no
tiene un buen formato)…
• SQLException: cuando nos topemos con una de estas excepciones sabremos que algo ha
fallado a la hora de acceder a la base de datos (SQL es el lenguaje que se usa para hacer
consultas a una base de datos, de ahí el nombre de SQLException). Podría suceder que
la query esté mal escrita o que haya un problema con la conexión a la base de datos, quién
sabe… El mensaje o la subclase de la excepción nos dará detalles más concretos.
• ParseException: decimos que parseamos un String cuando intentamos entender lo que
pone en él, por ejemplo, convertir un «01/02/2004» en una fecha, 1 de febrero de 2004. O
«-1.23» en un número, menos uno con veintitrés. Si lo que hay en ese String no tiene el
formato correcto («46//987» o «-3.,2»), ¡no debería sorprendernos que nos saltara una
ParseException!
252 Manejo de excepciones
No voy a seguir nombrando excepciones, que nos darían las uvas y no aporta nada. Tienes
la maravillosa documentación oficial de Java con toda la información necesaria, disponible E10.3
en internet. Recuerda: busca «API Java» en tu buscador favorito y allí lo encontrarás.
Naturaleza de las excepciones
Fijándonos en su naturaleza, hay excepciones debidas a errores de programación,
es decir, a la torpeza del programador que escribe el código. Clásicos de este tipo
serían las NullPointerException y las IllegalArgumentException.
Si llamo a un método (escrito por otro programador) que está mal escrito y que está
llamando a un método sobre un objeto que puede ser nulo, como llamante de ese
método poco puedo hacer: protestarle al autor y pedirle que lo arregle. Si es un
compañero de equipo, arreglaremos ese método. Pero, y si se trata de una librería
externa, ¿qué podemos hacer?
Está muy bien echarle las culpas a otro, pero a veces la causa de la excepción está
en errores en el código llamante, es decir, en el que estoy escribiendo yo. Por ejemplo,
intento hacer algo que no está permitido por el API (la interfaz) de ese método, violo
su contrato. Pongamos que quiero llamar a un método que calcula raíces cuadradas.
Recordemos que solo se puede calcular la raíz cuadrada de un número positivo.
Pongamos que le paso un número negativo a ese método. Lo normal es que falle,
¿no? ¿Qué podemos hacer en ese caso? Pues como es mi código el que está mal,
tendré que intentar tratar el problema, por ejemplo, comprobar que siempre pasaré
un número positivo a ese método.
Jamás debería entrar en producción un código que no haya sido suficientemente
probado y verificado como para asegurarnos de que no se va a producir ninguna
de ellas en un entorno real de producción.
Hasta aquí estas son las excepciones con las que debemos encontrarnos cuando
estamos desarrollando, escribiendo nuestro código y probándolo constantemente.
La tercera naturaleza de excepciones con las que nos podemos encontrar son aquellas
que se producen debido a fallos en los recursos. Tengo un código perfectamente
escrito que usa unas librerías magníficas a prueba de bombas. Estoy llamando a un
servicio web que me da unos datos que después de tratarlos meteré en base de datos.
Y siempre funciona bien, salvo cuando se cae la red, o el servidor web no responde,
o alguien ha pisado el cable de alimentación del ordenador en el que está la base de
datos y este ¡se ha apagado! En estos casos, ¿qué podemos hacer? Bueno, echarnos
a llorar, pero yo sugeriría proponerle al usuario que lo reintente más tarde o, a lo
mejor, si tenemos un sistema de backup secundario, podemos tirar contra otro
servidor web u otra base de datos. Normalmente dejaremos un mensaje en los T10.5
registros de logs y terminaremos el programa, con la esperanza de que alguien
reconecte el sistema que cayó.
¿Qué son las excepciones? 253
API de las excepciones
Las excepciones son clases Java como cualquier otra (de acuerdo, con alguna peculiaridad),
pero lo que quiero decir es que tienen (o pueden tener) constructores, métodos y atributos
como cualquier otra clase. Como he mencionado antes, la edición estándar de Java incluye
un buen puñado de excepciones, pero cualquier desarrollador puede crear las que requiera
y añadirles métodos, constructores, atributos y lo que considere necesario. Sin embargo,
como todas son hijas de Exception y nietas de Throwable, tendrán algunas cosas en común.
Constructores
Figura 10.10. Los constructores de Throwable en el API de Java.
Por lo general, las excepciones presentan varios constructores:
• el de por defecto (sin argumentos), en el que no proporcionaremos ningún detalle sobre
la excepción,
• uno que recibe un String, generalmente un mensaje de error, con el detalle de lo sucedido,
• uno que recibe otro Throwable, la causa original de la excepción,
• hay otro constructor que combina mensaje y causa
• y podría haber algunos más.
Más adelante iremos viendo cuál nos conviene más usar en qué ocasiones.
254 Manejo de excepciones
Métodos
Los métodos de las excepciones están definidos en Throwable. Veamos los más usados:
• getMessage(): devuelve el mensaje de la excepción, ese que se le pasó al constructor
cuando la creamos. Es útil a la hora de completar la información en nuestros logs o en los
mensajes de error que mostraremos a los usuarios.
• getLocalizedMessage(): suele devolver lo mismo que getMessage, a no ser que una clase
hija haya sobrescrito este método para producir un mensaje específico a un Locale en
concreto (Locale nos indica la configuración de idioma y país de la máquina en la que
estamos ejecutando el código).
• getCause(): devuelve la excepción (bueno, el Throwable) que causó esta excepción. No
recuerdo haberlo usado demasiadas veces, pero podría resultar útil para conocer
programáticamente la causa del problema si tenemos opciones de solucionarlo.
• toString(): devuelve el nombre de la excepción y el mensaje. En ocasiones es mucho más
útil que el getMessage, pues hay excepciones cuyo mensaje aporta muy poca información,
pero su nombre mucha. Por ejemplo, NullPointerException, con mensaje vacío, o
IndexOutOfBoundsException, con su mensaje «3». En ocasiones, hay que conocer nombre
y mensaje de la excepción para comprender lo sucedido.
• printStackTrace(): apostaría que este es el que mejor conoces, pues es el que más se usa
en los malos ejemplos de código que se centran en cosas «más importantes» que las
excepciones. Es un método muy útil cuando estás programando, pues te imprime por la
salida de error estándar (normalmente la consola de tu entorno de desarrollo o un fichero
de logs en producción) el stack trace (la pila de trazas) de la excepción.
Stack trace es una lista de todos los métodos a los que se ha llamado para llegar a tu excepción:
el primero de la lista es aquel en el que se ha producido el error y el último, el inicio del código.
Cuando estamos programando, es imprescindible para saber qué ha sucedido, sobre todo en
las excepciones que son «culpa del programador». Sin embargo, como veremos más adelante,
jamás deberíamos considerarlo una solución a la hora de tratar una excepción, que es lo que
se suele ver en los ejemplos de código que se encuentran en algunos recursos.
Figura 10.11. API (incompleto) del método printStackTrace.
¿Qué son las excepciones? 255
La figura 10.11 no incluye la documentación completa de este método. Consúltala en el
T10.6 API de Java, porque está muy bien explicada.
Bueno, lo prometido es deuda, y con esto termina la teoría sobre las excepciones.
T10.7
Ejemplo práctico
Hagamos un ejemplo de código. Podemos hacerlo juntos o inténtalo primero por tu
cuenta. Si te atreves con ello, a continuación tienes las especificaciones.
Enunciado
Vamos a crear dos clases de excepción propias, que luego usaremos en otros ejemplos.
• TechnicalException
• Tipo: Runtime Exception
• Constructores: 2
• recibe mensaje
• recibe mensaje y causa
• BusinessException
• Tipo: Checked Exception
• Constructores: 2
• recibe código de error y mensaje
• recibe código de error, mensaje y causa
• Métodos: 1
• generateMessage (privado), que recibe código de error y mensaje y los
devuelve concatenados
• Enumerados:
• ErrorCode:
• Valores:
• NEGATIVE: El número recibido es negativo
• EVEN: El número recibido es par
• Constructores: 1
• recibe un mensaje
• Métodos: 1
• toString para que devuelva "ERROR: " + mensaje + ". Detalle: "
Te recomiendo que lo intentes. Si te bloqueas, siempre puedes empezar a leer la solución
propuesta, ver los primeros pasos y volver a intentarlo. ¡Es la mejor forma de aprender!
256 Manejo de excepciones
Solución propuesta
Hemos pasado suficiente rato tratando cómo son las excepciones que existen en el API
de Java. Veamos ahora cómo crear nuestras propias excepciones, para adaptarlas a las
circunstancias de nuestras aplicaciones.
Abre tu entorno de desarrollo y empecemos.
Crearemos dos excepciones, una BusinessException, que será checked, para tratar todos
esos errores que tengan que ver con nuestra lógica de negocio. Y una TechnicalException,
que la haremos runtime, para los errores técnicos.
TechnicalException
Empecemos por la TechnicalException:
• Creamos una nueva clase llamada TechnicalException, que extienda de Runtime
Exception.
• Creamos un constructor que reciba un mensaje (String).
• Y otro que reciba un mensaje y una causa (Throwable).
En ambos constructores llamaremos al constructor equivalente del padre usando super.
public class TechnicalException extends RuntimeException {
public TechnicalException(String message) {
super(message);
}
public TechnicalException(String message, Throwable cause) {
super(message, cause);
}
}
¡Y ya está! ¡Ya la tenemos lista!
Para la TechnicalException hemos optado por hacerla runtime porque con ella controlaremos
aquellos problemas técnicos que se produzcan por cualquier lugar del código y no
queremos que nos afecten demasiado, no queremos tener por todos lados capturas y
lanzamientos de excepciones técnicas, pues poco podemos resolver.
BusinessException
Ahora vamos a crear la BusinessException. En este caso, heredaremos directamente de
Exception y, además, vamos a crearle unos códigos de error para que cuando se produzca
una de estas orientemos al usuario y/o resolvamos el problema en función de nuestra
lógica de negocio:
• Creamos una nueva clase llamada BusinessException, que extienda de Exception.
• Creamos un enumerado (enum) dentro de la misma clase, llamado ErrorCode.
• Que tendrá un atributo message (String) y un constructor que reciba ese String.
¿Qué son las excepciones? 257
• Creamos dos valores dentro del enumerado:
NEGATIVE, con el mensaje "El número recibido es negativo" y
EVEN, con "El número recibido es par".
• Para terminar con el enumerado, sobrescribimos el método toString para que
devuelva "ERROR: " más el mensaje ". Detalle:";
Ahora que ya tenemos los códigos de error listos, vamos a por los constructores. Haremos
dos, como antes, pero ahora ambos recibirán el código de error además del mensaje y,
en su caso, la causa:
• Creamos un primer constructor que reciba el código de error (ErrorCode) y el detalle
del mensaje (String).
• Y un segundo constructor que reciba el código, el detalle y la causa (Throwable).
Para no duplicar trabajo, el primer constructor (el de dos argumentos) llamará al
constructor de tres argumentos, usando this(), pasándole null como causa.
En el cuerpo del constructor de tres argumentos vamos a, primero, llamar al constructor del
padre con un mensaje preparado y la causa. Para ello, prepararemos el mensaje con un método
privado y estático que devuelve un String, llamado generateMessage y que recibe un código
de error y un mensaje detallado. Y devuelve la concatenación del código de error y del mensaje.
(Recuerda que en el enumerado hemos sobrescrito el método toString para que nos formatee
ese código de error). Tras haber llamado al constructor padre con nuestro mensaje preparado
y la causa, ya podemos guardar el código de error, por si lo necesitamos.
Crearemos un atributo privado con el código de error y un método get, siguiendo las
mejores prácticas de encapsulamiento java.
public class BusinessException extends Exception {
private ErrorCode errorCode;
public BusinessException(ErrorCode errorCode, String detailMessage) {
this(errorCode, detailMessage, null);
}
public BusinessException(ErrorCode errorCode, String detailMessage,
Throwable cause) {
super(generateMessage(errorCode, detailMessage), cause);
[Link] = errorCode;
}
private static String generateMessage(ErrorCode errorCode,
String detailMessage) {
return errorCode + detailMessage;
}
public ErrorCode getErrorCode() {
return errorCode;
}
enum ErrorCode {
NEGATIVE("El número recibido es negativo"),
EVEN("El número recibido es par");
258 Manejo de excepciones
private String message;
ErrorCode(String message) {
[Link] = message;
}
@Override
public String toString() {
return "ERROR: " + message + ". Detalle: ";
}
}
}
De momento, dejamos nuestras dos excepciones aquí aparcadas. Dentro de unas páginas,
cuando ya sepamos manejar las excepciones, seguiremos con este ejemplo.
¿Cómo manejar las excepciones?
En esta sección aprenderemos cómo manejar las excepciones en Java. Analizaremos una a
una cada una de las palabras clave con las que cuenta Java para el manejo de excepciones,
veremos cuándo y cómo hay que usarlas, y terminaremos ampliando un poco más el ejemplo
que vimos al final de la sección anterior, dándoles uso a nuestras propias excepciones.
¿De dónde vienen las excepciones? throw
Podríamos decir que algunas vienen de París (de código que usamos nosotros, pero ha
sido escrito por otros, como librerías externas), pero no todas «vienen».
Normalmente, los métodos de otras API que llamemos, nos lanzarán excepciones
cuando lo necesiten, pero también nosotros podemos crear y lanzar nuevas
excepciones cuando queramos.
El origen de una excepción es algo tan sencillo como una línea de código que la lanza.
Si se produce alguna situación que no me gusta, lanzo (throw, sin s) nueva (new) MyException,
el tipo de excepción que me interese, propia o del API de Java, y con el mensaje de error que
proporcione la mejor información para saber qué ha sucedido. También podríamos usar
cualquiera de los otros constructores disponibles para nuestra excepción.
if (algoNoMeGusta(a, b)) {
throw new MyException("Algo no me gustó de " +
a + " ni " + b);
}
En este caso, algo no me gustó de «lo que me hayan pasado como a» ni «lo que me hayan
pasado como b».
¿Cómo manejar las excepciones? 259
¿Son contagiosas? throws
Así de entrada, y visto el miedo que suelen tenerles los programadores, apostaría que
sí, que son contagiosas, pero en realidad dependerá de la situación, y aprenderemos
juntos a que al menos no sean mortales.
Si un método no es capaz de resolver una checked exception que él genera o que ocasiona
alguno de los métodos a los que llama, entonces tiene la obligación de pasársela al método
que le está llamando. Esto lo hacemos añadiendo la cláusula throws (con s final), tras la
declaración de argumentos de nuestro método, junto con el nombre de la clase de
excepciones que estemos lanzado. Si hubiera varias, las podemos separar por comas.
private void yoNoSe() throws MyException {
yoLanzoUnaExcepcion();
}
private void yoLanzoUnaExcepcion() throws MyException {
throw new MyException("No funciono");
}
Esto que acabamos de decir es obligatorio en el caso de las checked exception, pero, si
tenemos una runtime entre manos, no hay por qué declararla, pero podemos hacerlo si
tenemos especial interés en que se sepa que ese método puede lanzar esa excepción,
dándole así la oportunidad al programador que va a usar nuestro método de estar
preparado para lo peor.
Cuando declaremos el lanzamiento de excepciones, es muy recomendable que lo
incluyamos en el javadoc de nuestro método y expliquemos en qué casos se va a lanzar.
Un buen ejemplo de ello lo tienes, una vez más, en la documentación oficial de Java:
/**
* Returns the {@code char} value at the
* specified index. An index ranges from {@code 0} to
* {@code length() - 1}. The first {@code char} value of the sequence
* is at index {@code 0}, the next at index {@code 1},
* and so on, as for array indexing.
*
* <p>If the {@code char} value specified by the index is a
* <a href="[Link]#unicode">surrogate</a>, the surrogate
* value is returned.
*
* @param index the index of the {@code char} value.
* @return the {@code char} value at the specified index of this string.
* The first {@code char} value is at index {@code 0}.
* @exception IndexOutOfBoundsException if the {@code index}
* argument is negative or not less than the length of this
* string.
*/
public char charAt(int index) {
if ((index < 0) || (index >= [Link])) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
260 Manejo de excepciones
Figura 10.12. API del método chatAt de la clase String.
T10.8
Como vemos al final de la figura 10.12 o del fragmento de texto, nos dice que lanzará
una IndexOutOfBoundsException, si el índice que le estamos pasando como argumento
es negativo o mayor que la longitud del String. Como es lógico y natural, nos protestará T10.9
si le pedimos el carácter de la octava posición de "Hola", que solo tiene cuatro letras.
¿Pero no se pueden tratar? try / catch
Acabamos de ver cómo declarar que tenemos problemas, pero la gracia de las excepciones
es que podamos resolverlos (o al menos intentarlo). Veamos cómo.
private void noQuieroPares(int a) throws ParException {
if (esPar(a)) {
throw new ParException(a + " es par!");
}
}
Tenemos un método auxiliar, noQuieroPares, que recibe un entero y puede lanzar (throws,
con s), una ParException. Si vemos el cuerpo de este método, tenemos un if que comprueba
si el número recibido es par y, en ese caso, lanza (throw) una nueva (new) ParException
diciendo que el número recibido es par.
private void yoFunciono(int a) {
noQuieroPares(a);
}
¿Cómo manejar las excepciones? 261
Tenemos otro método, yoFunciono se llama (humilde él), que no declara la posibilidad de
lanzar ninguna excepción (es decir, seguro que no lanza una checked exception, quien sabe si
en alguna situación muy chunga puede lanzar una runtime exception o un error).
No intentes buscarles un sentido realista a estas funciones, por favor, porque no lo tienen.
Estábamos con yoFunciono, que recibe un entero y queremos llamar a noQuieroPares,
pasándole ese mismo entero. Pero resulta que noQuieroPares declara que (a veces) puede
lanzar una ParException. Y como ParException es una checked exception, tengo la obligación
de hacer algo con ella. Pero mi método se llama yoFunciono y no declara el lanzamiento de
ninguna excepción, así que tendré que resolver el problema.
Tras mucho pensarlo a nivel funcional, veo que la mejor solución, si el número que me han
pasado no le gusta a noQuieroPares, es reintentarlo con el número siguiente. Es decir, si 6 no
le gustó porque es par, pues voy a pasarle un 7.
Traducido a código, esto implicaría meter la llamada a noQuieroPares(a) dentro de un bloque
try (intentar), con sus llaves de apertura y cierre, seguido de un bloque catch (capturar) en el
que queremos capturar una excepción del tipo ParException que llamaremos pe.
private void yoFunciono(int a) {
try {
noQuieroPares(a);
} catch (ParException pe) {
noQuieroPares(a+1);
}
}
Dentro del bloque try tenemos el código que nos puede dar problemas. Dentro del bloque
catch, ponemos el código que soluciona esos posibles problemas. En este caso, la llamada a
noQuieroPares con a + 1.
En el catch podríamos capturar directamente la excepción lanzada o cualquiera de sus «padres»
en la jerarquía. Sin embargo, te recomiendo que, por favor, seas lo más concreto posible a la
hora de escoger qué excepción capturar, en este caso, ParException, para así asegurar la mayor
precisión posible a la hora de resolver el problema.
Este código aún no compila, pues la segunda llamada a noQuieroPares también tiene una
posible excepción que tratar, pero, de momento, no quiero complicarlo más. Solo quería
enseñarte una forma de tratar una excepción. Hay un problema y sé cómo solucionarlo, ¡pues
lo trato! Y nadie se entera de que aquí ha pasado algo (porque funcionalmente me interesa
que así sea, no porque quiera esconder nada a nadie).
¿Pero no se pueden tratar? (Más complicado)
Ya has notado que hemos empezado a entrar en materia… A partir de ahora es muy
importante que entiendas bien lo explicado hasta el momento antes de continuar leyendo,
pues poco a poco vamos a ir complicándolo todo. Pero no te asustes, que iremos despacio.
262 Manejo de excepciones
Si lo necesitas, en cualquier momento puedes volver a releer la parte que no hayas
entendido del todo. No hay problema. Tómate tu tiempo.
Pongamos, siguiendo con el ejemplo de antes, que ahora tengo un método aún más
exigente que se llama noQuieroParesNiNegativos y que puede lanzarme una ParException,
como antes, y una NegativoException.
private void noQuieroParesNiNegativos(int a)
throws ParException, NegativoException {
if (esPar(a)) {
throw new ParException(a + "es par!");
}
if (a < 0) {
throw new NegativoException(a + "es negativo :(");
}
}
Este nuevo método, si recibe un par, lanzará una ParException, como antes; y si no,
comprobará si el número es negativo, en cuyo caso lanzaría una NegativoException.
Ponemos, por tanto, el throws seguido de ParException, coma, NegativoException.
Fíjate en que he dicho «y si no», pero no tengo un else en el código. Esto es porque, en
cuanto hacemos un throw en el código, se corta el flujo ejecución normal y se lanza la
excepción, con lo que en caso de que el número recibido sea par, nunca llegaremos a
ejecutar el segundo if. Así que, a efectos prácticos, es como si fuera un else.
Vamos a probarlo, busca el fichero [Link] entre las fuentes proporcionadas. Tiene
un método main que llama a noQuieroParesNiNegativos con el primer argumento recibido
convertido a entero.
public final static void main(String[] args) throws Exception {
noQuieroParesNiNegativos([Link](args[0]));
}
ADVERTENCIA:
Si no le pasamos nada o no es un entero, fallará. No quiero complicar el código con ese
tratamiento ahora.
Primero lo compilamos.
> javac [Link]
Si llamo a noQuieroParesNiNegativos con un 1…
> java NoQuiero 1
no pasa nada.
Si la llamo con un 2…
> java NoQuiero 2
Exception in thread "main" ParException: 2 es par!
at [Link]([Link])
at [Link]([Link])
me lanza una ParException diciendo que "2 es par!".
¿Cómo manejar las excepciones? 263
Si la llamo con un -3…
> java NoQuiero -3
Exception in thread "main" NegativoException: -3 es negativo :(
at [Link]([Link])
at [Link]([Link])
me lanza una NegativoException diciendo que "-3 es negativo" y una carita triste.
¿Qué pasa si la llamo con -4? Dilo en voz alta, nadie te oye, y si hay alguien cerca… pues
nada, ¡sabrá que estás estudiando mucho! Ahora pruébalo. Llama a noQuiero
ParesNiNegativos con un -4 y mira qué pasa.
> java NoQuiero -4
Exception in thread "main" ParException: -4 es par!
at [Link]([Link])
at [Link]([Link])
¿Qué pasó? ¿Acertaste? Espero que sí, pero, si no, no pasa nada… Veamos qué sucede
y por qué.
Si apostaste por que lanzaba las dos excepciones… Lo siento, pero eso nunca sucederá,
porque como he comentado hace un momentito, en cuanto se lanza una excepción se
corta el flujo normal de ejecución. Por tanto, nunca podremos «acumular» varias
excepciones y es imposible llegar al código que genera la segunda. Por consiguiente, la
respuesta correcta, lo que realmente sucede al ejecutar este código, es que lanza la
ParException con el mensaje "-4 es par!", pues esta es la primera comprobación que
hemos hecho y, como ya ha fallado, no hemos ni mirado si era negativo o no.
Ahora que tenemos claro cómo funciona noQuieroParesNiNegativos, escribamos una
función que llame a este método. Haremos algo parecido al yoFunciono, pero ahora será
yoFuncionoAVeces.
NOTA:
Uso método y función de forma indistinta, me estoy refiriendo a lo mismo.
private void yoFuncionoAVeces(int a) {
noQuieroParesNiNegativos(a);
}
Nuestro método privado, que no devuelve nada (void), se llama yoFuncionoAVeces y
recibe un entero a que usaremos como parámetro de noQuieroParesNiNegativos que,
como antes, puede lanzarnos una ParException, pero también una NegativoException.
La ParException ya sabemos cómo tratarla, ¿no? Haremos como antes. Envolvemos la
llamada con un try y un catch que capturará ParException pe, e intentaremos llamar a
noQuieroParesNiNegativos con a + 1 para asegurarnos de que ya no se podrá quejar por
recibir un número par.
private void yoFuncionoAVeces(int a) {
try {
noQuieroParesNiNegativos(a);
} catch (ParException pe) {
noQuieroParesNiNegativos(a+1);
}
}
264 Manejo de excepciones
Sin embargo, con NegativoException no sabemos muy bien qué hay que hacer. No,
no me digas que podríamos capturarla y volver a llamarla con -a porque
funcionalmente en nuestro caso esa no es una solución viable (ya que, si lo fuera,
¡no me permitiría mostrarte lo que quiero mostrar en este ejemplo!). yoFuncionoAVeces
es incapaz de tratar una NegativoException y por tanto se ve obligada a declarar
un throws (con s) NegativoException. Y ya que estamos, añádelo también al javadoc:
@throws NegativoException si el número recibido es negativo.
private void yoFuncionoAVeces(int a) throws NegativoException {
try {
noQuieroParesNiNegativos(a);
} catch (ParException pe) {
noQuieroParesNiNegativos(a+1);
}
}
Como en el caso anterior, este código sigue sin compilar porque no estamos tratando la
T10.10
ParException que puede lanzar la segunda llamada a noQuieroParesNiNegativos. Que
tú y yo, que somos personas muy listas, sabemos que nunca lo va a lanzar (si era par
antes, ya no lo será cuando le sumemos 1), pero eso el compilador de Java no lo sabe y, T10.11
por ello, protesta. No te preocupes por ahora. Luego lo resolvemos.
¿Siempre hay que tratarlas?
Ya hemos visto muchas formas de tratar las excepciones. Vamos a resumir un poco qué
hacer en cada caso.
Si detectamos un problema nuevo, ¡a por el pulsador de alarma: throw! Es decir, en el
momento en el que nos demos cuenta de que en nuestro código hay un problema (el
número recibido es par), añadimos una sentencia throw (sin s) de una nueva excepción
para alertar a quien haga falta.
Cuando recibimos una excepción, si la sabemos resolver (¡qué suerte!), lo arreglamos
nosotros mismos sin molestar a nadie, mediante los bloques try/catch.
Si no podemos resolver, lo que haremos será pasarle el problema a quien nos llame.
Usaremos la cláusula throws en la declaración de nuestro método.
Nos queda una cuarta opción… ¿y si no hay que hacer nada? Esto puede suceder, por
ejemplo, si estamos seguros de que ese caso no va a pasar nunca. O bien porque, si pasa,
el problema no es grave y podemos seguir con el flujo normal de nuestro programa sin
efectos colaterales.
En estos casos, lo que es posible hacer es dejar un bloque catch vacío, pero, muy
importante, nunca lo dejaremos vacío del todo: añadiremos un comentario explicando
claramente por qué no hay nada que hacer en ese caso concreto.
¿Cómo manejar las excepciones? 265
Lo que nos quedaba pendiente
¿Recuerdas que teníamos pendiente un error de compilación en yoFunciono? Pues ahora
ya sabemos cómo resolverlo: envolveremos la segunda llamada a noQuieroPares, la de
a + 1, con un bloque try, con un catch que capturará la ParException. Y ¿qué haremos
dentro de ese catch? Nada, porque sabemos (si era par antes, no lo será ahora) que nunca
va a suceder. Pero para asegurarnos de que quien lea el código lo entienda a la primera,
pondremos un comentario explicándolo: // si a era par, a+1 no puede serlo.
private void yoFunciono(int a) {
try {
noQuieroPares(a);
} catch (ParException pe) {
try {
noQuieroPares(a+1);
} catch (ParException parE) {
// si a era par, a+1 no puede serlo
}
}
}
Y aún mejor…
La solución propuesta no es del todo mala, pero es mejorable. ¿Te has planteado qué
podría pasar si un día los matemáticos se reúnen, deciden cambiar la definición de par
y, de repente, si a es par, a+1 sigue siéndolo?
Aparentemente nuestro programa seguiría funcionando, pero en algunos casos lo haría mal.
Si nos queremos asegurar de que llegado ese momento alguien se enterará de que hay
un problema, podemos lanzar una nueva IllegalStateException, que construiremos con
un mensaje de error (esta excepción debería ser imposible con el valor, y lo que valga
a + 1) y con la excepción original, en este caso, llamada parE (por el ámbito de las variables,
no podemos volver a llamar pe a la segunda ParException anidada).
Luego volveremos a hablar de ello, pero pasándole la excepción original a la nueva
IllegalStateException proporcionaremos a quien vaya a tratarla la información sobre el
problema real, facilitándole mucho la vida.
private void yoFunciono(int a) {
try {
noQuieroPares(a);
} catch (ParException pe) {
try {
noQuieroPares(a+1);
} catch (ParException parE) {
// si a era par, a+1 no puede serlo
throw new IllegalStateException(
"Esta excepción debería ser imposible con el valor "
+ (a + 1), parE);
}
}
}
Como IllegalStateException es hija de RuntimeException, no es obligatorio declararla,
con lo que yoFunciono no la declarará con ninguna cláusula throws.
266 Manejo de excepciones
¡Finalmente! finally
Ya conocemos throws (con s) para declarar que un método puede lanzar una excepción,
throw (sin s) para lanzar nuevas excepciones, try para envolver el código que puede
darnos problemas y catch para meter el código que trata la excepción. Pero nos falta
conocer a la quinta palabra reservada de esta familia: finally (finalmente).
Hemos dicho hace un par de secciones que, una vez se lanza una excepción, se corta el
flujo de ejecución del programa y ya no se ejecutan las siguientes líneas del código, sino
que saltamos al bloque catch o al método llamante (si no hubiera catch).
Pero hay ocasiones en las que necesitamos hacer algo, haya ido todo bien o haya fallado
algo. Un ejemplo típico es la liberación de recursos, como el cierre de conexiones, el
guardado de ficheros o de datos… dependerá de la situación.
private void loDejoLimpio(int a) {
Recurso rec = obtenerRecursos();
algoQuePuedePetar(rec);
seguimos(rec);
liberarRecursos(rec);
sigoConMiVida();
}
Veamos el método loDejoLimpio. Está compuesto de cinco operaciones:
• obtenerRecursos: que realizará las conexiones o aperturas que sean necesarias, y
nos devolverá una referencia a los mismos, rec.
• algoQuePuedePetar: que recibe rec, y que por su nombre parece indicar que no es
muy fiable, que además declara una MyException.
• seguimos: también recibe el parámetro rec, pero parece que ya no hace cosas
peligrosas.
• liberarRecursos: cerrará las conexiones o lo que fuera que se abrió al obtenerRecursos.
• sigoConMiVida: que se seguirá trabajando con normalidad, sin necesitar esos
preciosos recursos que habíamos usado antes.
Hemos dicho que algoQuePuedePetar puede lanzar una MyException, así que tendremos
que envolverlo con su try. Pero no es necesario dejar solita a esa operación dentro del
try, podemos meterle también el resto de las operaciones. Sin embargo, sigoConMiVida
lo dejamos fuera del try, porque funcionalmente nos interesa que sea así.
private void loDejoLimpio(int a) {
try {
Recurso rec = obtenerRecursos();
algoQuePuedePetar(rec);
seguimos(rec);
liberarRecursos(rec);
} catch (MyException me) {
...
}
sigoConMiVida();
}
¿Cómo manejar las excepciones? 267
Pero si algoQuePuedePetar peta, falla, ni seguimos, ni vamos a liberarRecursos,
porque, como hemos dicho ya unas cuantas veces, el flujo se corta al lanzarse la
excepción.
Que se ejecute o no sigoConMiVida dependerá de lo que hagamos en el catch. Si
resolvemos el problema, sí podremos seguir. Si no lo resolvemos y acabamos lanzando
una excepción, saliendo del método o terminando el programa, sigoConMiVida no
se ejecutará.
Lo más importante en este ejemplo es el problema de la liberación de recursos: si
no se liberan, el sistema puede saturarse y tendríamos problemas muy graves, así
que es vital que vayan las cosas bien o mal, liberemos los recursos que ya no
necesitamos.
Aquí es donde usaremos el bloque finally: el código que escribamos dentro de este
bloque se ejecutará siempre, tras terminar de ejecutar el código del try, si todo ha
ido bien, o al terminar el catch, si algo fue mal.
private void loDejoLimpio(int a) {
Recurso rec;
try {
rec = obtenerRecursos();
algoQuePuedePetar(rec);
seguimos(rec);
} catch (MyException me) {
...
} finally {
liberarRecursos(rec);
}
sigoConMiVida();
}
Con este código, si todo va bien, se ejecutarían las cinco operaciones que comentamos
al principio. Si fallara algo en algoQuePuedePetar, las operaciones ejecutadas serían
obtenerRecursos, algoQuePuedePetar (ejecutado a medias, hasta que petó), lo que
tengamos en el catch, liberarRecursos (en el finally) y, dependiendo de si el catch
nos saca del método o no, sigoConMiVida.
Aunque no tengamos excepciones previstas de por medio, es posible seguir usando
E10.4 el bloque finally para cerrar lo que necesitemos: por ejemplo, si en medio de nuestro
código tenemos varias vías de salida.
Con recursos (try)
Lo que hemos visto hasta ahora es válido para cualquier versión de Java. Sin embargo,
en Java 7 se añadieron formas nuevas de hacer algunas cosas.
Aunque te parezca increíble, en grandes proyectos reales aún en desarrollo se siguen
usando versiones antiguas, pues no es lo mismo mantenerse al día con tus proyectitos
268 Manejo de excepciones
de prácticas (un yate), que con grandes proyectos empresariales con decenas de
miles de días de trabajo a las espaldas y responsabilidades serias en producción (un
transatlántico). Por eso, es conveniente conocer las diferencias entre una versión y
otra... ¡Nunca sabes en qué versión te tocará trabajar!
Try-with-resources es una sentencia try que declara uno o varios recursos, siendo un
«recurso» un objeto que debe ser cerrado cuando terminamos de usarlo (y que tiene
que implementar AutoCloseable). Con esta construcción nos evitamos la rama finally
para cerrar recursos, aunque es posible seguir usándola para otros menesteres.
El propio Java se encargará de cerrar todos los recursos que declaramos en los
paréntesis del try.
private void loDejoLimpio(int a) {
try (Recurso rec = obtenerRecursos()){
algoQuePuedePetar(rec);
seguimos(rec);
} catch (MyException me) {
...
}
sigoConMiVida();
}
La principal diferencia con el clásico try/catch/finally es que antes, si se producía una
excepción en el finally, había que controlarla específicamente, mientras que, con el
try-con-recursos, la excepción que se pueda producir durante el cierre de los recursos
será tratada por el bloque catch del try.
Antes teníamos:
Recurso rec;
try {
rec = ...;
...
} catch (MyException me) {
... // trato las excepciones del try
} finally {
if (rec != null) {
try {
[Link]();
} catch (Exception e) {
... // trato la excepción del close
}
}
}
Ahora tendremos:
try (Recurso rec = obtenerRecursos()){
...
} catch (MyException me) {
... // trato las excepciones del try
} catch (Exception e) {
... // trato la excepción del close
}
¿Cómo manejar las excepciones? 269
Multicaptura (catch)
También desde Java 7 se añadió la posibilidad de capturar varios tipos de excepciones
en el mismo catch.
Este mecanismo lo utilizaremos en aquellos casos en los que antes pondríamos varias
ramas catch, todas con tratamiento idéntico y que ahora podemos sintetizar:
private void tratamientos() {
try {
algoConMuchosFallos();
} catch (AException ae) {
tratamientoUno(ae);
} catch (BException be) {
tratamientoUno(be);
} catch (CException ce) {
tratamientoDos(ce);
}
}
Vemos que las excepciones A y B tienen el mismo tratamiento, así pues, podríamos
juntarlas en la misma rama. Sin embargo, la excepción C conservará su propia rama,
pues el tratamiento que le hacemos es distinto.
private void tratamientos() {
try {
algoConMuchosFallos();
} catch (AException | BException e) {
tratamientoUno(e);
} catch (CException ce) {
tratamientoDos(ce);
}
}
¡Ahora sí que no tenemos excusa para capturar excepciones demasiado generales!
Y con esto llegamos al final del manejo de las excepciones. Vamos a continuar con el
ejemplo que empezamos en la sección anterior y luego veremos qué no hay que hacer
con las excepciones. ¡No te saltes la próxima sección porque contiene información
fundamental para convertirte en todo un maestro en la gestión de excepciones!
Ejemplo práctico
Sigamos con el ejemplo de código. De nuevo, podemos hacerlo juntos o inténtalo primero
por tu cuenta. Si te atreves con ello, a continuación tienes las especificaciones de lo que
vamos a hacer.
Enunciado
Escribiremos un programa (main) que coja el primer argumento que le pasen, lo convierta
a un número entero y haga ciertos cálculos (calcular su cuadrado) si no es par ni negativo.
Sacaremos el resultado por la salida estándar, mostrando el mensaje "El resultado es " +
el valor calculado.
270 Manejo de excepciones
Debemos controlar cualquier tipo de excepción que se pueda producir y garantizar la
mejor experiencia de usuario.
Lo voy a hacer en cuatro pasos. Ve uno a uno: lees uno, lo intentas, ves el resultado
propuesto y, luego, a por el siguiente.
1. Tendremos un método main que llama al método trataNumero que recibe String[]
args y devuelve un entero (el cuadrado del primer argumento transformado a int).
Si no tuviéramos ningún argumento, lanza una TechnicalException.
TEST: Si le pasamos un 2, nos devuelve un 4.
2. Añadimos a trataNumero las llamadas a dos nuevos métodos: noQuieroPares y
noQuieroNegativos. Ambos reciben un entero, no devuelven nada y lanzan una
BusinessException si el número recibido no es de su agrado (recuerda que cuando
la creamos, también creamos un enumerado con códigos de error). De momento, en
el main lanzamos las excepciones que sean necesarias para que compile todo.
TEST: Ahora, al pasarle un 2, revienta.
3. No queremos que nuestro programa reviente (muestre al usuario las trazas de los
errores que se hayan producido), con lo que en el main controlaremos la
BusinessException, sacando por [Link] un aviso de que se ha producido
un error funcional y los datos de la excepción (sin trazas, por favor).
Como tenemos los códigos de error, sacaremos un mensaje con pistas para resolver
el problema, distintos en función de cada código de error.
TEST: Ahora, al pasarle de nuevo el 2, nos saca unos mensajes comprensibles para
el usuario.
4. Vamos a probar nuestro código con varios valores, para asegurarnos de tenerlo todo
controlado.
TEST: Le pasamos un 3 y devuelve 9, funciona.
TEST: Le pasamos un 2, error funcional, pide un impar.
TEST: Le pasamos un -3, error funcional, pide positivo.
TEST: Lo llamamos sin argumentos, revienta.
Nos falta controlar las TechnicalException, sacaremos por [Link] un aviso
parecido al de la BusinessException, pero como error técnico y sin códigos de error.
TEST: Lo llamamos sin argumentos, error técnico, informando del problema.
TEST: Lo llamamos con una w. Revienta.
Hay que controlar este problema, mediante una TechnicalException.
TEST: Lo llamamos con una w, error técnico.
Para terminar, añadimos un mensaje de agradecimiento al usuario, vayan bien o mal
las cosas.
Pruébalo todo bien y ¡compara tu solución con la propuesta!
¿Cómo manejar las excepciones? 271
Solución propuesta
Usando TechnicalException
Al final de la sección anterior creamos nuestras excepciones propias, BusinessException,
que es una checked exception, de esas que hay que tratar sí o sí, y TechnicalException, que
es una runtime, y, por tanto, podemos dejar «sin tratar» hasta el último momento.
En las últimas lecciones, hemos aprendido las palabras clave para el manejo de las
excepciones, sean nuestras o de otros.
Ahora continuemos con nuestra aplicación de ejemplo. En nuestro programa, queremos
coger el primer argumento que se nos pase, convertirlo a un número entero y, si no es
par ni negativo, hacerle algo (multiplicarlo por sí mismo). Imprimiremos por la salida
estándar el resultado de dicha operación.
Empecemos a picar código:
public class TestExcepcional {
public static final String TECH_SIN_ARGUMENTOS =
"Se esperaba al menos un argumento";
public static void main(String[] args) {
int res = trataNumero(args);
[Link]("El resultado es " + res);
}
private static int trataNumero(String[] args) {
if ([Link] == 0) {
throw new TechnicalException(TECH_SIN_ARGUMENTOS);
}
int num = [Link](args[0]);
return num * num;
}
}
• Creamos una clase llamada TestExcepcional.
• Y en ella creamos un método public static void main que recibe un array de Strings
args como argumentos. Este método será el que ejecutemos para probar nuestro
código. En el mundo real, tendríamos una aplicación web o móvil o de escritorio
mucho más bonitas que un simple main, pero demasiado complejas para este ejemplo.
• Desde el main, llamaremos a un método trataNumero que recibe el array de Strings
y devuelve un entero, res.
• E imprimiremos ese resultado por pantalla, con el mensaje "El resultado es " y lo que
valga res.
• Para que esto compile, tenemos que implementar ese método:
• private static int trataNumero que recibe un String llamado args.
• Lo primero que tendremos que hacer es comprobar que hemos recibido al menos
un argumento y, si no, protestar por ello:
272 Manejo de excepciones
• Si la longitud de args es igual a cero lanzamos nueva TechnicalException con
un mensaje de error, "Se esperaba al menos 1 argumento", que podemos
extraer a la constante TECH_SIN_ARGUMENTOS.
• Como es un runtime, no necesitamos declararla.
• Otra alternativa al if para controlar si tenemos al menos un argumento habría
sido acceder directamente a la posición 0 del array, y capturar una
ArrayIndexOutOfBoundsException. Intentarlo por ti mismo es una buena
práctica que te propongo.
• Ahora que ya nos hemos asegurado de haber recibido un dato, convirtámoslo a
número, porque lo hemos recibido como texto. Para ello, declaramos un entero
num y lo inicializamos con el resultado de hacer un [Link] y le pasamos
args de 0.
• Ya tenemos un número, hagamos nuestros cálculos: devolvemos el cuadrado.
Vamos a parar aquí a probar nuestro código. Pasémosle un 2 y esperemos que
nos devuelva un 4.
> javac TestExcepcional
> java TestExcepcional 2
El resultado es 4
¡Bien!
Usando BusinessException
Parece que de momento va bien. Pero habíamos dicho que no queríamos aceptar ni pares
ni negativos… Sigamos.
En el método trataNumero, entre la conversión a número y nuestras cuentas, vamos a
llamar a un par de métodos que hagan nuestros controles funcionales. Técnicamente
somos capaces de calcular el cuadrado de números pares y negativos, pero, por algún
motivo, nuestro cliente ¡no quiere hacerlo! Pues, donde hay patrón, no manda marinero,
controlemos esos casos.
private static void noQuieroPares(int n) throws BusinessException {
if (n % 2 == 0) {
throw new BusinessException(
[Link],
"Valor " + n);
}
}
• Creamos un método private static void noQuieroPares que recibe un entero n.
• Comprobamos si n módulo (usamos el signo %, en Java) 2 es cero, para saber si es par.
• ¿Qué hacemos aquí? Podríamos devolver un código de error o un booleano negativo,
pero, ya conocemos las excepciones y sabemos que están hechas para esto, ¿no? ¡Pues
a por ello!
• Si el número es par, lanzamos una BusinessException, que nos pide un código de
error y un mensaje; y, si la tenemos, una causa. Aquí no tenemos causa, así que código
¿Cómo manejar las excepciones? 273
de error es EVEN y mensaje "Valor" + el valor de n. Ten cuidado y pon un espacio al
final del texto para que no se peguen las letras y el número.
• ¡Uy! ¡Ahora no compila! ¿Recuerdas cómo tenemos que resolverlo? En efecto, hay
que declarar que lanzamos la BusinessException porque esta no es runtime, así que
es de declaración (o tratamiento) obligatoria.
private static void noQuieroNegativos(int n) throws BusinessException {
if (n < 0) {
throw new BusinessException(
[Link],
"Valor " + n);
}
}
• Creamos otro método private static void noQuieroNegativos que también recibe un
entero n.
• Comprobamos si n es menor que cero.
• Lanzamos una BusinessException con el código de error NEGATIVE y de nuevo
"Valor" + el valor de n.
private static int trataNumero(String[] args) throws BusinessException {
if ([Link] == 0) {
throw new TechnicalException(TECH_SIN_ARGUMENTOS);
}
int num = [Link](args[0]);
noQuieroPares(num);
noQuieroNegativos(num);
return num * num;
}
• Volvemos a trataNumero, como decíamos, tras recuperar el número y antes de hacer
cuentas, metemos las llamadas a nuestros dos metodillos:
• noQuieroPares de num.
• noQuieroNegativos de num.
• Y como el compilador amablemente nos indica, tenemos que declarar también aquí
que vamos a lanzar una BusinessException.
• Estamos propagando nuestra excepción, así que en el main también tendremos que
hacer algo con ella. De momento, la lanzaremos. A ver qué pasa.
NOTA:
En el repositorio de ficheros del libro encontrarás las versiones, paso a paso, de la clase
TestExcepcional.
> javac [Link]
> java TestExcepcional 2
Exception in thread "main" BusinessException: ERROR: El número recibido es par.
Detalle: Valor 2
at [Link]([Link])
at [Link]([Link])
at [Link]([Link])
274 Manejo de excepciones
¡Aaah! ¡Qué cosa más horrible! ¡No podemos permitir que nuestro usuario vea semejantes
cosas en su aplicación! Se va a pensar que estamos en Matrix. Nunca deberíamos dejar
ver a nuestros usuarios las trazas de nuestras excepciones. Deberíamos tratarlas antes,
aunque solo sea para enseñarle algo más bonito y comprensible.
Cuidando mis excepciones
¡Solucionemos eso de enseñarle cosas feas al usuario!
¿Qué sucedió? Pues que en uno de los últimos pasos tomamos la decisión equivocada y
optamos por lanzar la BusinessException en el main. Y el main, en esta aplicación, es
nuestro último punto de control. Así que es allí donde debemos controlar lo que le
mostramos a nuestro usuario.
Por tanto, borramos lo del throws BusinessException del main y volvemos al problema
de compilación. ¿Qué otra alternativa hay? Nos dice que tenemos una excepción sin
manejar, así que vamos a meterle un bloque try / catch.
public static void main(String[] args) {
try {
int res = trataNumero(args);
[Link]("El resultado es " + res);
} catch (BusinessException be) {
}
}
• Ponemos un try llave antes de la llamada a trataNumero
• y dejaremos que el bloque envuelva nuestras dos líneas de código, incluyendo la que
imprime el resultado
• catch BusinessException be.
Y ahora toca pensar. ¿Qué queremos hacer aquí? ¿Es posible resolver el problema sin
molestar al usuario? ¿Necesitamos su colaboración? ¿O no hay nada que hacer?
En una aplicación real, podríamos pedirle al usuario que nos proporcionara datos que
cumplan las condiciones, pero como no queremos complicar más este ejemplo, simplemente
brindaremos unos mensajes por la salida estándar explicando que ha habido un problema
y sugiriendo cómo evitarlo en futuras ejecuciones.
public static void main(String[] args) {
try {
int res = trataNumero(args);
[Link]("El resultado es " + res);
} catch (BusinessException be) {
[Link]("===== ERROR FUNCIONAL =====");
[Link] ("Se ha producido un error funcional: " + be);
switch ([Link]()) {
case EVEN:
[Link]("Prefiero números impares (1, 3, 5, ...)");
break;
case NEGATIVE:
[Link]("Quiero números positivos");
break;
}
}
}
¿Cómo manejar las excepciones? 275
• Hacemos un [Link] con un título para el error funcional.
• Escribimos una segunda línea con el texto "Se ha producido un error funcional: "
y la excepción (esto llamará a su toString que, recordemos, nos saca el nombre
de la excepción y el mensaje con el que se creó).
Y ya que estamos, como tenemos los códigos de error, vamos a hacer un switch
sobre el código de error y a sacarle mensajes más detallados de cómo resolver
el problema en cada caso: uno para el caso impar y otro para el negativo. ¡No
olvides los breaks!
Tengo que reconocer que no es la panacea de trato al usuario, pero al menos ¡ya
no le mostraremos las trazas! A ver qué pasa al ejecutar.
> javac [Link]
> java TestExcepcional 2
===== ERROR FUNCIONAL =====
Se ha producido un error funcional: BusinessException: ERROR: El número recibido
es par. Detalle: Valor 2
Prefiero números impares (1, 3, 5, ...)
> java TestExcepcional -1
===== ERROR FUNCIONAL =====
Se ha producido un error funcional: BusinessException: ERROR: El número recibido
es negativo. Detalle: Valor -1
Quiero números positivos
¡Mucho mejor! Ahora el usuario ya siente que le hablamos a él, no a un robot loco.
Probando mis excepciones
Bueno, nuestro código ha mejorado mucho. Comprobemos que todo esté bien. Hagamos
pruebas. ¡Siempre hay que probar nuestro código! Y no solo con los valores que sabemos
que funcionan.
> java TestExcepcional 3
El resultado es 9
Si le pasamos un 3, todo va bien. El resultado es 9.
Y si le pasamos un 2…
> java TestExcepcional 2
===== ERROR FUNCIONAL =====
Se ha producido un error funcional: BusinessException: ERROR: El número recibido
es par. Detalle: Valor 2
Prefiero números impares (1, 3, 5, ...)
Como habíamos previsto, protesta y nos dice que pongamos un número impar. Bien.
Vamos a probar con -3, a ver qué dice:
> java TestExcepcional -3
===== ERROR FUNCIONAL =====
Se ha producido un error funcional: BusinessException: ERROR: El número recibido
es negativo. Detalle: Valor -3
Quiero números positivos
¡Otro error funcional! ¡Somos unos artistas! Ahora se queja de que no quiere un
negativo. Genial.
276 Manejo de excepciones
Vamos ahora a ser más malos… ¿Qué pasa si no le metemos nada?
> java TestExcepcional
Exception in thread "main" TechnicalException: Se esperaba al menos un argumento
at [Link]([Link])
at [Link]([Link])
¡Ups! ¡Una traza! ¡De nuestra TechnicalException! Pero habíamos dicho que ¡nada
de enseñar trazas de ejecución al usuario!
Que la TechnicalException sea una runtime nos ha permitido olvidarnos de ella hasta
ahora, pero no podemos permitir que el usuario vea trazas, así que ¡habrá que hacer
algo en el main!
public static void main(String[] args) {
try {
int res = trataNumero(args);
[Link]("El resultado es " + res);
} catch (BusinessException be) {
[Link]("===== ERROR FUNCIONAL =====");
[Link]("Se ha producido un error funcional: " + be);
switch ([Link]()) {
case EVEN:
[Link]("Prefiero números impares (1, 3, 5, ...)");
break;
case NEGATIVE:
[Link]("Quiero números positivos");
break;
}
} catch (TechnicalException te) {
[Link]("***** ERROR TECNICO *****");
[Link]("Se ha producido un error técnico: " + te);
}
}
Aprovechando el mismo try que ya teníamos para las BusinessExceptions, capturemos
también las TechnicalException y hagamos un tratamiento parecido. Si te parece,
ahora sacaremos los mensajes por la salida de error (que así saldrán en rojo).
• Hacemos un [Link] con un título para el error técnico.
• Escribimos una segunda línea con el texto "Se ha producido un error técnico: "
y la excepción.
• En este caso no tenemos códigos de error, así que no podemos ofrecer más detalle
a nuestro usuario.
Ejecutamos:
> javac [Link]
> java TestExcepcional
***** ERROR TECNICO *****
Se ha producido un error técnico: TechnicalException: Se esperaba al menos 1
argumento
¡Ahora sale en rojo (si usas Eclipse) un mensaje de error legible y comprensible para el
usuario y con información suficiente para que llame al servicio técnico a pedir ayuda si
no sabe cómo resolverlo!
¿Cómo manejar las excepciones? 277
Pero aún nos queda otra prueba. ¿Qué pasará si le metemos una letra, w por ejemplo?
> java TestExcepcional w
Exception in thread "main" [Link]: For input string: "w"
at [Link](NumberFormatException.
java:65)
at [Link]([Link])
at [Link]([Link])
at [Link]([Link])
at [Link]([Link])
¡Grrrr! ¡Otra traza! ¡Una NumberFormatException! ¿Pero de dónde viene? Si nos fijamos,
la propia traza nos lo dice, de la línea 32 de TestExcepcional, y ¿qué tenemos ahí? El
parseo del String a número. Lógico que nos falle con una w. Pero ¿por qué no nos ha
obligado a tratarla? Pues porque la NumberFormatException es una runtime, por tanto,
no es obligatorio tratarla. Pero si no la tratamos… ¡nos pasan estas cosas!
Bueno, no es grave. Habíamos creado una TechnicalException para estos casos, ¿no?
private static int trataNumero(String[] args) throws BusinessException {
if ([Link] == 0) {
throw new TechnicalException(TECH_SIN_ARGUMENTOS);
}
try {
int num = [Link](args[0]);
noQuieroPares(num);
noQuieroNegativos(num);
return num * num;
} catch (NumberFormatException nfe) {
throw new TechnicalException(TECH_MAL_FORMATO, nfe);
}
}
• Envolvamos el código de trataNumero con try catch
• que capture la NumberFormatException
• y lance una TechnicalException con el mensaje "El primer argumento debe ser un
número entero", que meteremos en la constante TECH_MAL_FORMATO
• y a la que también le pasaremos la causa original del problema.
¡Volvemos a probar!
> javac [Link]
> java TestExcepcional w
***** ERROR TECNICO *****
Se ha producido un error técnico: TechnicalException: El primer argumento debe
ser un número entero
¡Ahora ya nos sale bien controlada! Prueba si quieres otros casos. Si nos queda alguno
por controlar, arréglalo y me avisas, para que lo actualice en la próxima edición. ¡Gracias!
En este ejemplo hemos usado throw, throws, try y catch. ¿Recuerdas qué palabra clave de las
excepciones nos falta? ¡Sí! Finally, pues venga, terminemos esto bien. Añadamos un mensaje
de agradecimiento al usuario por confiar en nosotros, hayan ido bien o mal las cosas:
public static void main(String[] args) {
try {
int res = trataNumero(args);
[Link]("El resultado es " + res);
} catch (BusinessException be) {
278 Manejo de excepciones
[Link]("===== ERROR FUNCIONAL =====");
[Link]("Se ha producido un error funcional: " + be);
switch ([Link]()) {
case EVEN:
[Link]("Prefiero números impares (1, 3, 5, ...)");
break;
case NEGATIVE:
[Link]("Quiero números positivos");
break;
}
} catch (TechnicalException te) {
[Link]("***** ERROR TECNICO *****");
[Link]("Se ha producido un error técnico: " + te);
} finally {
[Link]("¡Gracias por usar este programa!");
}
}
• Tras el último catch ponemos un bloque finally
• y dentro un mensaje "¡Gracias por usar este programa!".
¡Enhorabuena! ¿Has visto qué lejos has llegado ya con las excepciones? Parece que ya
estás preparado para ver las malas prácticas más habituales y saber ¡por qué tú no
debes hacerlas!
Malas prácticas
Comérsela con patatas
¿Comérsela con patatas? Vaya título raro para una sección sobre excepciones en Java.
Hasta ahora hemos visto la teoría (sí, fue duro, lo sé), cómo manejar las excepciones y
sus palabras clave (throws, throw, try, catch, finally). Ahora abordemos cómo no manejarlas.
Veamos fragmentos de código que espero que nunca generes por ti mismo (y no solo
porque yo te lo pida aquí, sino porque entiendes las causas por las que no hay que hacer
código como este y qué consecuencias tiene).
[Link]();
[Link]();
Veamos este fragmento de código. Tenemos una variable llamada nulo, que no sabemos
de qué tipo es, ni qué valor tiene, y sobre la que llamamos al método loQueSea. En la
segunda línea, sobre esa misma variable, llamamos a su método otraCosa.
Estamos muy orgullosos de nuestro código, queda muy bonito, porque los paréntesis
quedan alineados al ser los nombres de los métodos igual de largos, así que lo subimos
a producción sin probarlo ni nada, porque somos los mejores y nada puede fallar. Pero
resulta que una vez en producción, los usuarios empiezan a usar la aplicación y, a la
primera de cambio, el código ¡falla! ¡Da una NullPointerException! El cliente llama a
nuestro jefazo, el jefazo al jefecillo y al final nos llega el problemón a nosotros. ¡Hay que
arreglar esa NullPointerException ahora mismo!
Malas prácticas 279
Eso es fácil, no sé cómo se nos pasó.
try {
[Link]();
} catch (NullPointerException npe) {
}
[Link]();
Simplemente, le ponemos un try y un catch a esa línea, capturamos la NullPointerException
y ¡ya está! ¡Listo! ¡Subimos a producción en tiempo récord!
¿Nos podemos ir ya de fin de semana tranquilos? ¿Qué te parece? Te dejo pensarlo un
momento…
Pues me temo que no, que en breve volverá la tormenta y ¡ya será un huracán de
categoría 5!
Si te fijas bien, al llamar a loQueSea tuvimos un NullPointerException porque nulo era
nula (¡quién lo iba a imaginar!). ¿Qué valor tendrá nulo cuando llamemos a otraCosa?
Pues seguirá siendo nula y, por tanto, tendremos otra NullPointerException.
¿No crees que sería mejor investigar por qué nulo es nulo, y arreglar eso?
Veamos otro caso…
String s = "123..45";
int num = [Link](s);
[Link](num);
Recibimos un String de cualquier sitio, imaginemos que lo ha tecleado un usuario.
Parseamos (convertimos a número) ese String y, luego, establecemos como salario de un
empleado esa cifra. Fácil, ¿no? Después se guardarán esos datos y, hacia finales de cada
mes, la aplicación de nóminas los leerá y generará la transferencia para que ese empleado
cobre su nómina.
El día que te contrataron a ti en esa empresa, la persona que te dio de alta en el sistema
tenía un mal día y en vez de asignarte un sueldo de 12 345 euros mensuales (¡guao!,
pedazo de sueldo!, ¡enhorabuena!), se le colaron un par de puntitos ahí en medio y tecleó
123..45.
Entonces, parseInt no fue capaz de entender ese número y lanzó una NumberFormatException,
que como NullPointerException es una RuntimeException y, por tanto, el programador
no tuvo obligación de tratarla al escribir este código.
El programador suplente se comió el marrón de que había habido un fallo, y nada, subió una
corrección rápidamente a producción, para que ya no fallara más. Rodeó con un try/catch el
parseInt y ¡ya! ¡Que es viernes y se quiere ir de finde!
String s = "123..45";
int num = 0;
try {
num = [Link](s);
} catch (NumberFormatException nfe) {
}
[Link](num);
280 Manejo de excepciones
De todo esto, tú no te enteraste de nada (cómo te lo iban a contar si eras el nuevo), pasaron
los días, estabas muy feliz en tu nuevo trabajo, aprendiendo un montón y enfrentándote
a interesantes retos, cuando llegó final de mes. Tus compañeros fueron diciendo «yo ya
he cobrado!», «¡yo aún no!, a mi banco siempre llega por la tarde», «yo cobré ayer». Y tú
venga a mirar en la web de tu banco y nada; en la app del banco, nada. A ver mañana…
Nada… ¿Habré dado bien mi número de cuenta? ¿Se lo habrán ingresado a otro? Pasan
los días… y nada. Hablas con Recursos Humanos y no tienen ni idea de qué ha pasado.
¿Lo sabes ahora?
Leamos despacito el código, para resolver el misterio. Tenemos un String, una variable
numérica inicializada a cero, un intento de convertir ese String en un número, tenemos
controlada la posible NumberFormatException y luego asignamos al empleado ese
número como salario. ¿Qué puede haber fallado?
Veamos qué sucede si el String no tenía un buen formato y saltó la NumberFormatException,
¿cómo resolvimos el problema? (¿Con un catch? ¡ah!). ¿Y eso qué significa?: num vale 0,
intentamos parsear el número y falla, así que num sigue valiendo 0, al tratar la excepción,
no hacemos nada, y luego asignamos al salario el valor de num, que sigue valiendo 0.
Pues nada, ¡sigue esperando a que te llegue la nómina!
¿Qué tienen en común estos dos ejemplos? En ambos hemos hecho lo mismo para tratar
las excepciones: nada. O peor aún, sí hemos hecho algo. Hemos capturado la excepción
y ¡nos la hemos comido con patatas! La hemos hecho desaparecer. La hemos capturado
y hemos seguido con el flujo normal del programa, como si no hubiera pasado nada.
Pero sí han pasado cosas, y muy graves.
En este punto del curso, necesito que entiendas, aceptes y asimiles que estos dos
ejemplos de tratamiento de excepciones, a los que me gusta llamar del tipo comérsela
con patatas son totalmente inadmisibles en cualquier programa, me da igual si es
profesional o por afición.
Vimos cómo manejar las excepciones, que podemos hacer cosas en el catch para resolverlas
o que podemos lanzarlas a nuestro llamante para que las resuelva él si nosotros no
sabemos. No tiene perdón que las tratemos así, que las ignoremos y las destruyamos.
A partir de este instante, en tu vida como programador, queda totalmente prohibida la
captura de excepciones sin tratamiento. No más catch vacíos en nuestra vida. ¿Estamos?
Comérsela con lechuga
Acabamos de ver unos ejemplos muy simples con consecuencias horribles (marrones de
última hora, gente que no cobra sus nóminas…) debidos al mal manejo de las excepciones,
debidos al código de pésima calidad generado por un programador que nunca se
preocupó de entender y aprender a manejar las excepciones. Por suerte, tú no caerás en
el mismo error.
Malas prácticas 281
A decir verdad, por lo general, hasta el propio entorno de desarrollo nos puede avisar
si dejamos un bloque catch vacío, así que el problema de excepciones comidas con patatas
no es tan común como podríamos pensar. A menudo, es sustituido por un problema aún
peor… ¡las excepciones comidas con lechuga!
Todos sabemos que las patatas fritas son poco saludables y la lechuga es la panacea de
las buenas dietas, ¿no? Bueno, podemos discutirlo con un nutricionista, o no ser tan
extremos, pero parece que la lechuga es más sana que las patatas…
Pues en este caso es muchísimo peor. Veámoslo en el código:
try {
[Link]();
} catch (NullPointerException npe) {
// Ups!! nulo es nulo, qué mal rollo
[Link]("Ups!! nulo es nulo," +
"qué mal rollo");
[Link]([Link](), npe);
[Link]();
}
[Link]();
Parece que alguien salió escarmentado de un caso de comérsela con patatas y al viento puso por
testigo que jamás volvería a dejar un catch vacío. Y se lo tomó a pecho…
En este ejemplo tenemos:
• un comentario: ¡Ups! nulo es nulo, qué mal rollo, para que lo vea el próximo desarrollador
que pase por aquí.
• un [Link]: con el mismo mensaje, para que quede en el fichero de logs o en la consola
del entorno de desarrollo o donde sea que aparezcan los mensajes de log de debug, por
si nadie lee el código.
• un [Link]: con el mensaje de la excepción y la propia excepción, para que quede bien
fijado en todos los sistemas de log que ha sucedido algo muy grave.
• y, por si fuera poco, añadimos un printStrackTrace (hablamos de este método hace unas
cuantas páginas) para que saque por la salida estándar toda la traza de la excepción que
se ha producido.
Está claro que esta vez ya nadie dirá que nos hemos comido la excepción. Hemos dejado
rastro de ella por todos lados. Porque, como todos sabemos, en los sistemas en producción
hay siempre alguien leyendo la consola, la famosa «salida estándar» ¿no? (por si lo dudas,
no, no suele haber nadie leyendo).
Bueno, si nulo da una NullPointerException cuando llamamos a loQueSea, el problema
quedaría bien logueado, pero ¿qué pasaría con la llamada a otraCosa? Con tanta traza, ¿nulo
habría cambiado de valor? Pues no, nulo seguiría siendo nula, así que, en esta ocasión,
volveríamos a tener una NullPointerException en producción. Eso si no revienta antes el
disco duro por quedarse sin espacio, debido a la cantidad de porquería acumulada en los
sistemas de logs.
282 Manejo de excepciones
¿Ves por qué es peor comérsela con lechuga que con patatas? En apariencia, parece que se está
haciendo algo: si pasas rápidamente por el código, no verás un alarmante catch vacío. Y el
entorno de desarrollo tampoco lo detectará, pero tú y yo sabemos que el comportamiento será
el mismo que antes. Mismo comportamiento, más escondido. ¡Problemón a la vista!
Si al final de la sección anterior dijimos que estaban rotundamente prohibidos los catchs
vacíos, ahora ampliamos la prohibición a los catchs en los que se genera mucha porquería
de logs, pero se sigue no haciendo nada útil, sin corregir el problema, ni trasladarlo a quien
pueda resolverlo.
Queda exenta de esta prohibición la solución que le dimos a la segunda llamada de
noQuieroPares que vimos hace unas secciones, en la que estábamos superconvencidos de que
jamás podría suceder. Recuerda que propusimos dos soluciones: dejando un comentario
explicativo o, aún mejor, lanzando una IllegalStateException.
Perder la memoria histórica
Hemos visto el caso de comerse las excepciones, en su variante con patatas o con lechuga.
Ahora toca el turno a otra forma de hacerlo mal con las excepciones. A esta la llamo
perder la memoria histórica.
Es muy común en muchos sistemas que cuando se produzca cualquier excepción en el
código, el tratamiento sea lanzar una excepción de otro tipo, dependerá de cómo sea la
arquitectura del proyecto, por ejemplo, una TechnicalException o una BusinessException
o como quieran bautizarlas en cada proyecto. De esta forma, se uniformiza el tratamiento
de errores en las capas más altas del proyecto.
Volvemos al ejemplo de nuestra variable nulo. Ya hemos aprendido que no podíamos
dejar el catch vacío, así que en la línea 4, dentro del catch, lanzamos una nueva Exception
y así «alguien por las alturas» se enterará del problema y podrá resolverlo.
1 try {
2 [Link]();
3 } catch (NullPointerException npe) {
4 throw new Exception();
5 }
6 [Link]();
[Link]
at [Link]([Link])
Ese pobre «alguien por las alturas» se encontrará con una traza de una Exception (con
un tipo que aporta entre poca y ninguna información) generada en la línea 4 del código.
Irá a la línea 4 del código para descubrir la causa y verá que es una nueva excepción, y
no podrá saber por qué.
¿Has visto la película Buscando a Nemo? ¿Te acuerdas de Dory, el pececillo azul? ¿Recuerdas
por qué era famosa Dory? Era la amiga de Nemo y ¡tenía memoria de pez! ¡Nunca se
acordaba de nada!
Malas prácticas 283
Veamos este otro fragmento de código… Hay que buscar la diferencia con el anterior. A
ver si la encuentras, es muy sutil.
1 try {
2 [Link]();
3 } catch (NullPointerException npe) {
4 throw new Exception(npe);
5 }
6 [Link]();
[Link]
at [Link]([Link])
Caused by: [Link]
at [Link]([Link])
Son tres letras de diferencia en la línea 4… Ahora, al constructor de Exception le pasamos
la excepción original, que tenemos en la variable npe. Esas míseras tres letras tienen unas
consecuencias muy importantes. En la traza de las excepciones verá que ha habido una
Exception en la línea 4, generada por (y esa es la novedad) una NullPointerException en
la línea 2. ¡Ya sabe dónde tiene el problema que resolver: nulo es nulo en la línea 2!
Evidentemente, en un código de 6 líneas, la diferencia no suele ser muy grande, pero, en la
vida real, te puedes encontrar (demasiado a menudo) con métodos monstruosos de cientos
de líneas y ahí no es tan sencillo encontrar la causa original, si fue Dory quien programó el
código con sus aletas y se olvidó de la causa original al lanzar la nueva excepción.
Moraleja de esta sección: si por circunstancias diversas necesitamos lanzar una excepción
de un tipo distinto a la que hemos capturado, no olvidemos pasarle a esta nueva excepción
la excepción capturada, para no perder nunca la causa original del problema.
Nos queda aún otra mala práctica en la gestión de excepciones. Tómate un respiro si lo
necesitas o… ¡sigue a tope con la siguiente sección para descubrir cuál es!
Generalizar
Ya hemos aprendido a no comernos las excepciones ni a perder la memoria olvidándonos
de los orígenes. Vamos a por la siguiente consigna: ¡No generalices!
Quizá seas demasiado joven, quizá en tu país no se emitió, pero ¿recuerdas el anuncio
de Catalana Occidente de mediados de los 90 en el que una niña decía que su padre lo
arreglaba «todo, todo y todo»? Aún estará por YouTube, si te apetece verlo. Bueno, pues
hay gente que con las excepciones hace como el papá de esta niña. Lo solucionan todo,
todo y todo ¡en una sola línea!
String s = "123..45";
int num = 0;
try {
num = [Link]([Link]());
[Link](num);
} catch (Exception e) {
// y aquí lo arreglo todo, todo y todo
...
}
284 Manejo de excepciones
Volvemos al fragmento de código que te dejó sin nómina. Analiza qué problemas pueden
suceder, aparte de la NumberFormatException que ya vimos.
• [Link], que sirve para quitar los espacios por delante y por detrás de un String, podría
fallar si el String es nulo: tenemos una posible NullPointerException.
• empleado, también podría ser nulo y, por tanto, llamar a setSalario también podría
darnos otra NullPointerException.
• supongamos que setSalario guarda ese salario en algún sitio: podría fallar por un
problema de acceso a base de datos, con una SQLException, por ejemplo.
• también podría darnos una, no sé, una BusinessException, si comprueba que el salario
recibido está o no dentro de un rango razonable. No sé si se te ocurre algún posible
problema más…
El caso es que, con tantas excepciones distintas, el programador tenía el día vago y decidió
capturar una Exception y listo. Así pilla de una sentada todos los problemas. Y los resuelve
todos, como el papá de la niña del anuncio.
¿Pero se solucionan de la misma manera todos los problemas? Si el usuario no nos ha
pasado un buen valor, podemos pedirle que nos lo vuelva a dar; si el empleado no existe,
quizá tengamos que crearlo; si la base de datos no está disponible, puede ser una buena
opción sacar un mensaje de error para que el usuario llame al Departamento de
Informática o quizá, aún mejor, podemos mandar un mail automáticamente al equipo
de soporte.
String s = "123..45";
int num = 0;
try {
num = [Link]([Link]());
[Link](num);
} catch (NumberFormatException nfe) {
// s no lleva un número
...
} catch (NullPointerException npe) {
// s o empleado son nulos
...
} catch (BusinessException be) {
// setSalario falló
...
}
Para hacer todo eso, necesitamos capturar cada excepción por separado y actuar en
consecuencia en cada caso. Ten en cuenta que, al capturar Exception, no solo capturamos
las checked exceptions que puedan declarar los métodos a los que llamemos. Recuerda que
RuntimeException es hija de Exception, por tanto, en ese catch también capturaríamos
cualquier runtime exception no esperada.
Última regla a aplicar: nunca captures una Exception (ni un Throwable), a no ser que ya
estés en el último punto de control de tu programa y tengas que evitar que las excepciones
(y sus trazas) le lleguen al usuario.
Ahora ya sabes cómo se manejan las excepciones y qué no hacer con ellas.
Malas prácticas 285
Conclusión
En este capítulo hemos aprendido las palabras clave para la gestión de excepciones: throws,
throw, try, catch y finally, pero lo más importante, cuándo y por qué usar unas u otras.
Con lo que quiero que te quedes, sobre todo, es con la idea de que las excepciones no
hay que esconderlas, sino decidir si sé resolver el problema o tengo que delegarlo.
Test y ejercicios
Test 10.1. Las excepciones no son un mecanismo exclusivo de Java.
a) Verdadero
b) Falso
Test 10.2. En caso de error de ejecución en mi programa, la mejor solución es
devolver un -1.
a) Verdadero
b) Falso
Test 10.3. ¿Cuál es la relación jerárquica entre Exception, Error y Throwable?
a) Exception es madre de Error y Throwable.
b) Throwable es madre de Exception y Error.
c) Error es hija de Exception y Throwable.
d) Las tres son hijas de Failure.
Test 10.4. ¿Cuántas clases hijas tiene Error?
a) ¡Imposible de determinar, yo me puedo crear cuantas quiera!
b) 12 conocidas: AnnotationFormatError, AssertionError, AWTError, CoderMalfunction Error,
FactoryConfigurationError, FactoryConfigurationError, IOError, LinkageError, ServiceConfigurationError,
ThreadDeath, TransformerFactory ConfigurationError, VirtualMachineError.
c) 2: OutOfMemory, IOError.
Test 10.5. ¿Cómo podemos evitar las excepciones debidas a errores humanos?
a) Probando bien el código antes de entregarlo.
b) Prestando atención al escribir nuestro código.
c) Leyendo la documentación de los métodos que usamos.
d) Aplicando todas las anteriores.
Test 10.6. ¿Qué método del API de las excepciones devuelve el nombre y el
mensaje de la excepción?
a) getMessage
b) getLocalizedMessage
c) toString
d) printStackTrace
Test 10.7. ¿Qué devuelve el método printStackTrace()?
a) La traza de la excepción.
b) La excepción y su traza.
c) Nada.
286 Manejo de excepciones
Test 10.8. ¿Cuál de estas versiones de código es correcta?
a) void metodoA throw Exception {
throw new Exception();
}
b) void metodoB throw Exception() {
throws new Exception;
}
c) void metodoC throws Exception {
throw new Exception();
}
d) void metodoD throw new Exception() {
throws Exception;
}
Test 10.9. Dado este método:
public boolean metodo2(int a, int b) throws NegativosException {
if (a < 0 && b < 0) {
throw new NegativosException(a, b);
}
return a*a > b;
}
¿Cuál de estas versiones de javadoc es correcta?
a) /**
* Comprueba si el cuadrado de a es mayor que b.
*
* @param a primer valor a tener en cuenta, se calculará su
* cuadrado
* @param b segundo valor a tener en cuenta
*
* @return cierto si el cuadrado de a es mayor que b.
* @exception si a y b son negativos
*/
b) /**
* Comprueba si el cuadrado de a es mayor que b.
*
* @param a primer valor a tener en cuenta, se calculará su
* cuadrado
* @param b segundo valor a tener en cuenta
*
* @return cierto si el cuadrado de a es mayor que b.
* @exception NegativosException si a y b son negativos
*/
c) /**
* Comprueba si el cuadrado de a es mayor que b.
*
* @param a primer valor a tener en cuenta, se calculará su
* cuadrado
* @param b segundo valor a tener en cuenta
*
* @return cierto si el cuadrado de a es mayor que b.
* @throws Exception si a y b son negativos
*/
Test y ejercicios 287
d) /**
* Comprueba si el cuadrado de a es mayor que b.
*
* @param a primer valor a tener en cuenta, se calculará su
* cuadrado
* @param b segundo valor a tener en cuenta
*
* @return cierto si el cuadrado de a es mayor que b.
* @negativoException si a y b son negativos
*/
Test 10.10. ¿Cuál de estas versiones de código es correcta?
Considera las siguientes declaraciones:
void lanzoMyExcetion() throws MyException {...};
void hagoAlgo() {...}
a) void metodoA throws MyException {
try {
lanzoMyExcetion();
} catch (MyException) {
hagoAlgo();
}
}
b) void metodoB throws MyException {
try {
lanzoMyExcetion();
} catch (MyException me) {
hagoAlgo();
}
}
c) void metodoC {
try {
lanzoMyExcetion();
} catch (MyException) {
hagoAlgo();
}
}
d) void metodoD {
try {
lanzoMyExcetion();
} catch (MyException me) {
hagoAlgo();
}
}
Test 10.11. ¿Cuál será la salida por la consola de este código?
public void metodo() throws Exception {
[Link]("A");
try {
[Link]("B");
throw new Exception();
[Link]("C");
} catch (RuntimeException re) {
288 Manejo de excepciones
[Link]("D");
throw re;
[Link]("E");
} catch (Exception e) {
[Link]("F");
throw e;
[Link]("G");
}
[Link]("H");
}
a) ABCDEFGH
b) ABF
c) ABD
d) ABFGCH
Ejercicio 10.1. Provoca una NullPointerException
Hazlo como quieras, pero consigue que el método lloron() lance una NullPointerException.
public class LanzadorNullPointer {
public static void lloron() {
// Inténtalo aquí: escribe cualquier código
// que lance una NullPointerException
}
}
Ejercicio 10.2. Provoca muchas excepciones
Ahora que ya has conseguido lanzar una NullPointerException, intenta lanzar alguna
más. En cada método propuesto, lanza la excepción pedida en el comentario.
public class LanzadorMultiple {
public static void queMeSalgo() {
// Inténtalo aquí: escribe cualquier código
// que lance una IndexOutOfBoundsException
}
public static void malditaCasta() {
// Inténtalo aquí: escribe cualquier código
// que lance una ClassCastException
}
public static void siemprePositivoNuncaNegativo() {
// Inténtalo aquí: escribe cualquier código
// que lance una NegativeArraySizeException
}
}
Ejercicio 10.3. Controlando mi primera excepción
Te presento un trozo de código que no compila... Necesito que logres que compile, pero
sin ocultar a nadie los problemas que puedan surgir.
public class TodoControlado {
public static int cuentaBytes() {
String s = "Me ilusiona tener las excepciones controladas";
byte[] misBytes = [Link]("encodingQueNoExiste");
return [Link];
}
}
Test y ejercicios 289
Ejercicio 10.4. Orden de ejecución
A partir del código dado, implementa los métodos metodoUno y metodoDos para
conseguir, sin tocar el código del método ordename, que suceda lo siguiente:
Si llamo a ordename con ... debe devolver:
• 0 > "ABCGHI"
• 1 > "ABDGHI"
• 2 > "ABEGH"
• 3 > "ABCG"
• 4 > "ABDG"
public class OrdenEjecucion {
protected static String res;
public static void ordename(int caso) throws BException {
res = "A";
try {
res += "B";
metodoUno(caso);
res += "C";
} catch (AException ae) {
res += "D";
} catch (BException be) {
res += "E";
throw be;
// ni compila, si la pongo res += "F";
} finally {
res += "G";
metodoDos(caso);
res += "H";
}
res += "I";
}
private static void metodoUno(int caso)
throws AException, BException {
// tu código aquí
}
private static void metodoDos(int caso) {
// tu código aquí
}
}
class AException extends Exception {
}
class BException extends Exception {
}
TRUCO:
En el repositorio de código encontrarás test unitarios para poder validar tu solución a estos
ejercicios, puede que sea buena aunque sea distinta a la mostrada como solución en el libro.
290 Manejo de excepciones
Soluciones
Test 10.1. Las excepciones no son un mecanismo exclusivo de Java.
a) Verdadero: Muchos otros lenguajes utilizan excepciones para controlar los flujos de error.
Eso sí, no en todos los lenguajes se hace de la misma forma.
Test 10.2. En caso de error de ejecución en mi programa, la mejor solución es
devolver un -1.
b) Falso: ¡Correcto! Justo estamos aquí para aprender la mejor solución: manejando bien las
excepciones. Los return -1 deberían ser cosa del pasado.
Test 10.3. ¿Cuál es la relación jerárquica entre Exception, Error y Throwable?
b) Throwable es madre de Exception y Error.
Test 10.4. ¿Cuántas clases hijas tiene Error?
a) ¡Imposible de determinar, yo me puedo crear cuantas quiera!
Test 10.5. ¿Cómo podemos evitar las excepciones debidas a errores humanos?
d) Aplicando todas las anteriores: ¡Genial! ¡Cuento contigo para que las apliques siempre!
Test 10.6. ¿Qué método del API de las excepciones devuelve el nombre y el
mensaje de la excepción?
c) toString: ¡¡Correcto!!
Test 10.7. ¿Qué devuelve el método printStackTrace()?
c) Nada: ¡¡Correcto!! En todo caso, saca información por la consola, pero devolver, devolver…
¡no devuelve nada!
Test 10.8. ¿Cuál de estas versiones de código es correcta?
c) void metodoC throws Exception {
throw new Exception();
}
Efectivamente, throw new más constructor en el cuerpo, throws y el nombre de la
excepción, en la declaración.
Test 10.9. Dado este método:
public boolean metodo2(int a, int b) throws NegativosException {
if (a < 0 && b < 0) {
throw new NegativosException(a, b);
}
return a*a > b;
}
¿Cuál de estas versiones de javadoc es correcta?
b) /**
* Comprueba si el cuadrado de a es mayor que b.
*
* @param a primer valor a tener en cuenta, se calculará su cuadrado
* @param b segundo valor a tener en cuenta
*
Soluciones 291
* @return cierto si el cuadrado de a es mayor que b.
* @exception NegativosException si a y b son negativos
*/
La excepción lanzada es NegativosException y eso se documenta con las etiquetas
@exception o @throws.
Test 10.10. ¿Cuál de estas versiones de código es correcta?
Considera las siguientes declaraciones:
void lanzoMyExcetion() throws MyException {...};
void hagoAlgo() {...}
d) void metodoD {
try {
lanzoMyExcetion();
} catch (MyException me) {
hagoAlgo();
}
}
¡Bien hecho! Como capturamos la excepción del lanzoMyExcetion y hagoAlgo no
lanza ninguna, no hay ninguna excepción que declarar.
Test 10.11. ¿Cuál será la salida por la consola de este código?
public void metodo() throws Exception {
[Link]("A");
try {
[Link]("B");
throw new Exception();
[Link]("C");
} catch (RuntimeException re) {
[Link]("D");
throw re;
[Link]("E");
} catch (Exception e) {
[Link]("F");
throw e;
[Link]("G");
}
[Link]("H");
}
b) ABF
Tras el lanzamiento de la excepción (después de B), saltamos al catch de Exception
(F) y, tras el relanzamiento de la excepción, ya salimos del método.
Ejercicio 10.1. Provoca una NullPointerException
public class LanzadorNullPointer {
public static void lloron() {
// Inténtalo aquí: escribe cualquier código
// que lance una NullPointerException
String saludo = null;
[Link]();
[Link](saludo);
}
}
292 Manejo de excepciones
Ejercicio 10.2. Provoca muchas excepciones
public class LanzadorMultiple {
public static void queMeSalgo() {
// Inténtalo aquí: escribe cualquier código
// que lance una IndexOutOfBoundsException
String[] lista = {"uno", "dos"};
String tercero = lista[2];
[Link](tercero);
}
public static void malditaCasta() {
// Inténtalo aquí: escribe cualquier código
// que lance una ClassCastException
Number numero = new Integer(3);
Double decimal = (Double) numero;
[Link](decimal);
}
public static void siemprePositivoNuncaNegativo() {
// Inténtalo aquí: escribe cualquier código
// que lance una NegativeArraySizeException
String[] lista = new String[-2];
}
}
Ejercicio 10.3. Controlando mi primera excepción
import [Link];
// No olvides importar la clase de la excepción!
public class TodoControlado {
// Como getBytes puede lanzar una
// UnsupportedEncodingException
// necesito declararlo con "throws"
public static int cuentaBytes()
throws UnsupportedEncodingException {
String s = "Me ilusiona tener las excepciones controladas";
byte[] misBytes = [Link]("encodingQueNoExiste");
return [Link];
}
}
Ejercicio 10.4. Orden de ejecución
private static void metodoUno(int caso) throws AException, BException {
switch (caso) {
case 1:
case 4:
throw new AException();
case 2:
throw new BException();
default:
// todo bien
}
}
private static void metodoDos(int caso) {
switch (caso) {
case 3:
case 4:
throw new RuntimeException();
default:
// todo bien
}
}
Soluciones 293
11 Depuración
En este capítulo aprenderás a:
• Utilizar el depurador de Eclipse.
• Depurar un programa.
• Distinguir para qué sirve depurar.
Introducción
En los ejemplos del sOOPer o del MacetoHuerto vimos que se puede curiosear qué
pasa en el código imprimiendo trazas, pero hay veces que eso no es suficiente
(también es un trabajo muy arduo y no muy limpio, no apto para proyectos
profesionales). La otra alternativa que tenemos es la depuración, debug en inglés,
derivada de ‘quitar bichos’ (bugs). En cada lenguaje de programación y en cada
entorno se hará con herramientas distintas, pero es importante entender el concepto
y saber manejarse con ello. Cuando se presente alguna anomalía en el código o
cuando este se complica, también nos será de gran ayuda poder tirar de depurador
para ver qué pasa, cuándo, qué valores tenemos…
Porque depurar no es otra cosa que ejecutar el código paso a paso. Evidentemente,
como los programas son muy largos, podemos poner «puntos de ruptura» (o breakpoints)
en líneas concretas para que se ejecute del tirón hasta ese punto. Y cuando vamos
avanzando, decidimos si ejecutamos la línea como una unidad indivisible, si nos
metemos dentro del método que vamos a ejecutar o si salimos del método en el que
estamos y vamos al método desde el que nos han llamado. Mientras estamos parados
en un punto, es posible visualizar los objetos con los que contamos, qué valores tienen,
e incluso manipularlos. Pero, cuidado, manipular un dato no resuelve el problema,
porque a la próxima ejecución volveremos a tener el valor «malo», pero nos puede
ayudar a probar rápidamente una posible solución. Pero eso es depuración avanzada.
Cómo depurar en Eclipse
Cada herramienta dispone de sus opciones e, incluso, de su vocabulario, pero ahora
aprenderemos a depurar en el entorno de desarrollo Eclipse. Lo importante es quedarse
con los conceptos: si los entiendes bien, te será fácil adaptarte a otros entornos.
Detener ejecución Meterse en método (F5)
Continuar ejecución hasta el próximo punto de ruptura (F8) Salir del método (F7)
Activar/desactivar puntos de ruptura
Pausar ejecución Ejecutar en modo normal
Ejecutar línea (F6) Ejecutar en modo depuración
Figura 11.1. Barra de depuración en Eclipse.
Para ejecutar en modo depuración le tenemos que dar al icono del bichito que hay
al lado de la flecha de ejecución (lo puedes ver en la figura 11.1, señalado como
Ejecutar en modo depuración). Su funcionamiento en cuanto a paso de argumentos,
selección de clases… es exactamente el mismo que la ejecución normal, sobre todo
296 Depuración
si aún no hemos puesto un punto de ruptura, pero aun así Java nos ofrecerá pasar
a la vista de depuración, en la que la distribución de ventanas es más adecuada para
nuestro nuevo objetivo.
TRUCO:
Si la vista de depuración no salta automáticamente podemos abrirla a mano: Ventana>Perspectiva>
Abrir Perspectiva>Otras>Depurar.
Si queremos volver a la vista normal de Java, solo clicamos en el icono
correspondiente en la esquina superior derecha (ver figura 11.2). Según Figura 11.2.
vayamos usando más herramientas de Eclipse, aumentará la lista de Perspectivas
iconitos disponible. en Eclipse.
Para poner un punto de ruptura, una forma de conseguirlo es hacer doble clic en el
número de línea. Se pondrá un punto azul bien grande al lado. Otro doble clic hará
desaparecer el punto de ruptura. Si queremos mantenerlo, pero inhabilitarlo, tenemos
una opción para ello en el menú contextual que aparece al clicar con el botón
secundario del ratón. Podemos ver la lista de todos los puntos de ruptura que hay
(en la pestaña de los puntos de ruptura, al lado de la de las variables).
Cuando queramos ejecutar sin parar en los puntos de ruptura, pero sin quitarlos
todos porque los necesitaremos más adelante, utilizaremos el icono señalado como
Activar/desactivar puntos de ruptura en la figura 11.1.
Depurando Supermercado
Tomemos como ejemplo la clase Supermercado del sOOPer, donde ponemos un
punto de ruptura en la línea 19 y, si le damos a Depurar, veremos algo parecido a la
figura 11.3.
Figura 11.3. Depurando en Eclipse, antes de ejecutar la línea 19.
Cómo depurar en Eclipse 297
La línea 19, en la que habíamos puesto el breakpoint, se ha sombreado en verde. Ahora
el circulito azul está medio tapado por una flecha azul que junto al sombreado verde
nos marcan por qué línea vamos.
En la pestaña Variables, a la derecha de la pantalla, vemos los args, que no estamos
usando en este caso, y miPedido, recién creado con los contenedores vacíos.
En la pestaña Depurar, a la izquierda de la pantalla, se halla la pila de ejecución, es
decir, todos los niveles de llamadas acumuladas que tenemos, que de momento solo
es una, pues seguimos en el método main de Supermercado, que ha sido nuestro
punto de entrada.
T11.1
En este momento, la línea marcada aún no se ha ejecutado, sino que es la próxima
línea a ejecutar. Para ello, fíjate bien en los iconos. Si queremos ejecutar una línea,
T11.2 le damos a la flecha que se está saltando una línea, Ejecutar línea (F6) en la figura
11.1 o a la tecla F6. Le podemos dar dos veces, por ejemplo.
Figura 11.4. Depurando en Eclipse, tras ejecutar las líneas 19 y 20.
Ahora, en la figura 11.4, la flechita azul apunta a la línea 21, que es la que está
sombreada en verde: será la próxima en ser ejecutada. Ahora podemos ver más
claramente el punto de ruptura en la línea 19.
Si te fijas en la pestaña Variables, han aparecido dos nuevas, bolsa1 y caja1, que son
justo las que creamos en las líneas recién ejecutadas.
Seguimos depurando, pero en vez de ejecutar la línea 21, vamos a meternos dentro,
en este caso, del constructor de Caja. Para ello, le damos al icono de la flecha que
T11.3 se mete entre dos líneas, Meterse en método (F5) en la figura 11.1 o a la tecla F5, así
observaremos qué pasa dentro del método addContenedor.
298 Depuración
Figura 11.5. Depurando en Eclipse, dentro de addContenedor.
En la figura 11.5 vemos unas cuantas novedades:
• En la pestaña central, se ha abierto la clase Pedido, y ahora tenemos destacada
la línea 41, que se corresponde al método addContenedor.
• En la pestaña de la izquierda, la pila de llamadas tiene un nivel más, por encima
de [Link] aparece [Link]. Vigilando esta pestaña
podemos orientarnos y no perdernos en depuraciones más complejas que la actual.
• En la pestaña de la izquierda, hemos dejado de ver las variables que teníamos
en el main de Supermercado y ahora vemos this, que es el pedido en el que
estamos, y contenedor, que es el parámetro que ha recibido el método que
estamos depurando.
Como este método tiene una sola línea, utilizar el icono de Ejecutar línea (F6) tendría T11.4
el mismo efecto que su vecino de la derecha, el etiquetado como Salir del método
(F7) en la figura 11.1. Ejecutar una línea terminará la ejecución del método, porque T11.5
es la última, pero si hubiera más, pasaría a la siguiente. Salir del método, por el
contrario, ejecutará todas las líneas restantes de ese método y regresaría al siguiente
nivel de la pila de ejecución, es decir, regresaría al método main de Supermercado, T11.6
a la línea siguiente, la 22, como se aprecia en la figura 11.6.
Figura 11.6. Depurando en Eclipse, volviendo al main.
Cómo depurar en Eclipse 299
Pero quiero enseñarte más detalles de la figura 11.6.
• En la pestaña Depurar, ya no aparece en la pila de ejecución la clase Pedido, pues
hemos regresado a Supermercado.
• En la pestaña Variables, he desplegado miPedido, sus contenedores, su primer
elemento (en posición 0), y ahí constatamos que dentro de miPedido, de referencia
pedido001, ya está añadido el contenedor de referencia B111, es decir, el que acabamos
de añadir.
Para qué sirve depurar
Como ya debes estar imaginando, se pueden dedicar horas y horas a depurar un
fragmento de código, observando detenidamente cada paso y todas sus consecuencias.
Puede ser un buen ejercicio hacerlo ahora que estás aprendiendo, para familiarizarte con
la herramienta.
En un proyecto real, profesional, cuando ya te hayas familiarizado con el depurador, no
necesitarás depurar línea a línea todo el programa, pero sí lo utilizarás para analizar
fragmentos problemáticos que no se comporten como tú esperas.
La depuración te permite comprobar el valor que van tomando las variables, el flujo que
sigue el programa, por qué líneas pasa o no pasa… De esta forma, es posible detectar
errores en el código, localizando las erratas. ¡Cuántas veces no me he confundido con
un + o un -, u olvidando un ! para negar un booleano! ¡Pierdes la concentración un
segundo, y ya no funciona nada! Puedes releer el código para encontrar el problema,
pero si depuras verás valores y flujos reales, no imaginarios; y, por tanto, será más fácil,
rápido y seguro de localizar.
Otra utilidad curiosa del depurador es que, si cuentas en tu entorno con el código fuente
de las librerías que utilizas, puedes ver el código de dichas librerías y cómo funciona.
Ahora que estás aprendiendo, te resultará muy interesante trastear por las clases de Java,
por ejemplo, de String, de las listas, de la que te apetezca… Aunque si te metes muy
dentro del código, puedes llegar a fragmentos incomprensibles, que no vale la pena
intentar entender. Está en tus manos decidir hasta qué punto quieres llegar en la
investigación de código propio o ajeno.
También es posible que te veas en la tesitura de tener que modificar código existente de
T11.7 tu proyecto, pero que no acabas de identificar cómo está hecho. La depuración te ayudará
a entender código existente, al verlo ejecutar en directo.
300 Depuración
Test
Figura 11.7. Código de ejemplo para los tests.
Test 11.1. Partiendo del código de la figura 11.7, si pulso F6, ¿en qué línea se
parará la ejecución?
a) 5
b) 9
c) 12
d) 7
Test 11.2. Tras haber pulsado F6 para el test 11.1, pulsamos F6, ¿en qué línea
se parará la ejecución?
a) 5
b) 9
c) 10
d) Termina la ejecución
Test 11.3. Tras haber pulsado F6 para el test 11.2, pulsamos F5, ¿en qué línea
se parará la ejecución?
a) 11
b) En alguna del código del API de Java
c) 5
d) Termina la ejecución
Test 11.4. Tras haber pulsado F5 para el test 11.3, ¿cómo salimos de ese código
infernal y volvemos al nuestro?
a) Pulsando el icono Meterse en método (F5) de la figura 11.1.
b) Pulsando el icono Ejecutar línea (F6) de la figura 11.1.
c) Pulsando el icono Salir del método (F7) de la figura 11.1.
d) Pulsando el icono Detener ejecución de la figura 11.1.
Test 301
Test 11.5. Tras haber salido del «código infernal» del test 11.4, ¿en qué línea se
parará la ejecución?
a) 5
b) 9
c) 11
d) 12
Test 11.6. Estando en la línea en la que nos haya dejado el test 11.5, ¿qué se
habrá imprimido ya por la consola?
a) a1 b1 c1
b) a1 a2
c) a1 b1
d) a1 b1 c1 a2
Test 11.7. Estando en la línea en la que nos haya dejado el test 11.6, pulsamos
F8, ¿en qué línea se parará la ejecución?
a) 10
b) En alguna del código del API de Java
c) 6
d) Termina la ejecución
Soluciones
Test 11.1. Partiendo del código de la figura 11.7, si pulso F6, ¿en qué línea se
parará la ejecución?
b) 9: Al ejecutar metodo, el depurador se parará en el punto de ruptura de la línea 9, que
viene antes que la línea 5.
Test 11.2. Tras haber pulsado F6 para el test 11.1, pulsamos de nuevo F6, ¿en
qué línea se parará la ejecución?
c) 10: F6 ejecutará la línea 9 y como en [Link] no tenemos puntos de ruptura,
llegará a la línea siguiente, la 10.
Test 11.3. Tras haber pulsado F6 para el test 11.2, pulsamos F5, ¿en qué línea
se parará la ejecución?
b) En alguna del código del API de Java: Posiblemente en el constructor [Link],
pues nos hemos metido en el detalle de cómo se ejecuta el [Link], al que le
pasamos el resultado de concatenar los Strings.
302 Depuración
Test 11.4. Tras haber pulsado F5 para el test 11.3, ¿cómo salimos de ese código
infernal y volvemos al nuestro?
c) Pulsando el icono Salir del método (F7) de la figura 11.1
Test 11.5. Tras haber salido del «código infernal» del test 11.4, ¿en qué línea se
parará la ejecución?
b) 9: en la segunda ejecución de metodo
Test 11.6. Estando en la línea en la que nos haya dejado el test 11.5, ¿qué se
habrá imprimido ya por la consola?
a) a1 b1 c1: metodo se ha ejecutado una vez completo, con parámetro 1, y en la segunda
ejecución, hemos parado justo antes de ejecutar la primera línea, así que a2 aún no se ha
pintado.
Test 11.7. Estando en la línea en la que nos haya dejado el test 11.6, pulsamos
F8, ¿en qué línea se parará la ejecución?
d) Termina la ejecución: Hemos reanudado la ejecución, ya no pasamos por ningún punto
de ruptura más, así que ya terminamos.
Soluciones 303
12 Test unitarios
En este capítulo aprenderás a:
• Testar unitariamente tu aplicación.
• Decidir qué test son los más adecuados.
• Simular partes del código en los test.
• Trabajar con nociones básicas de TDD, JUnit y Mockito.
Introducción
Los test unitarios nos permiten comprobar el correcto funcionamiento de una unidad
de código, que puede ser un método, una clase. Los test unitarios no son aquellos que
prueban toda la aplicación, ni la integración de varios módulos, no, no, ¡esos no! Solo
prueban los métodos uno a uno. ¿Y eso para qué sirve? Si nos aseguramos de que cada
método hace lo que tiene que hacer, pero sobre todo que sigue haciéndolo cambio tras
cambio, contaremos con la certeza de que nuestro código es robusto y no ha sufrido
regresiones con las evoluciones.
Con esto quiero decir que, si disponemos de una buena cobertura de test unitarios, es
decir, si muchas líneas de nuestro código son probadas con alguno de los test unitarios
que hemos escrito, cuando alguna evolución, alguna corrección o algún otro cambio en
nuestro código cambie el comportamiento de algún otro fragmento de ese código, nos
daremos cuenta enseguida y lo corregiremos con el menor coste posible.
Un test unitario puede ser ejecutar a mano el main con ciertos parámetros y comprobar
que nos da el resultado que esperamos. Pero hacerlo de forma manual no es eficaz ni
eficiente. Perderíamos mucho tiempo en ello y, por tanto, no lo vamos a hacer de forma
exhaustiva.
Si usamos alguna herramienta para automatizar esos test, dándole a un botón o ejecutando
un comando, comprobaremos que todo sigue bien o, aún mejor, detectaremos qué ha
empezado a ir mal. Así que ipso facto analizaremos el problema y encontraremos la
solución, que puede ser corregir el código o el test, porque también puede que el test
estuviera mal o que los requisitos del proyecto hayan cambiado, que la corrección del
código sea correcta y que simplemente nos toque adaptar el test.
En el desarrollo de código profesional, no diremos que una tarea está terminada hasta
que hayamos escrito y ejecutado sus correspondientes test unitarios.
Tener test unitarios no es suficiente para asegurar la calidad de nuestra aplicación. Hay
que hacer más tipos de test, pero, desde luego, los test unitarios son una muy buena
práctica y son importantes.
Test Driven Development
Normalmente, primero codificamos y luego probamos. Pero existe una práctica de
ingeniería del software que nos invita a primero escribir el test unitario y después
implementar nuestro código. Test Driven Development (TDD) se llama, que traduciríamos
como ‘desarrollo guiado por pruebas’. Puede parecer una locura, una pérdida de tiempo…
pero la verdad es que, si nos forzamos a escribir el test antes de escribir el código, nos
forzamos a pensar cómo queremos que se comporte ese código, qué casos especiales
debemos tener en cuenta… De forma que, probablemente, si lo hacemos así, nos será
306 Test unitarios
más fácil crear bien a la primera nuestro código. Además, en caso de que no lo logremos,
en cuanto ejecutemos el test unitario nos daremos cuenta de ello.
El ciclo de desarrollo sería el siguiente:
• Elegir una funcionalidad: de la lista de requisitos del proyecto o backlog, en
metodologías ágiles.
• Escribir un test: nos fuerza a entender los requisitos, las especificaciones de la
funcionalidad que hemos de implementar.
• Comprobar que el test falla: pues aún no contamos con la nueva funcionalidad
implementada. Si no fallara, es o porque hemos escrito mal la prueba o porque la
funcionalidad ya existía en el proyecto.
• Implementar la funcionalidad: escribiendo el código más sencillo posible para que
la prueba funcione, siguiendo el principio KISS (Keep It Simple, Stupid!).
• Ejecutar los test: ahora sí deben funcionar, tanto el nuevo como los preexistentes.
• Refactorizar: para evitar sobre todo duplicaciones del código, pero también para
hacer otras mejoras. Una a la vez: cambiamos una cosa, probamos todos los test.
Siguiente cambio, pruebas completas otra vez.
• Actualizar la lista de requisitos: marcando como hecha esta funcionalidad y añadiendo
los nuevos requisitos que se hayan descubierto.
Nunca he trabajado en un proyecto con esta técnica, pero es un enfoque que parece T12.1
bastante interesante.
JUnit
Los test unitarios se pueden ejecutar a mano, pero, como hemos dicho, lo suyo es
programarlos. Y programando en Java, la biblioteca más utilizada se llama JUnit, que es
un conjunto de clases o framework, que nos facilitará la tarea de implementar test unitarios
o incluso otros tipos de test.
Eclipse suele llevar integrado un plugin de JUnit, que nos lo hará aún más fácil. El uso
habitual que hacemos de esta herramienta es crear una clase de test por cada clase que
queramos probar, y dentro de esa clase, creamos uno o varios métodos de test para testar
cada uno de los métodos de la clase a comprobar.
En los métodos de test, a los que a partir de ahora nos referiremos simplemente como
test, prepararemos el contexto para luego llamar al método a testar, para finalmente ver
si el resultado obtenido coincide con el esperado. Para esta comprobación se utilizan
aserciones, que no es más que llamar a ciertos métodos pasándoles ambos valores: el
obtenido y el esperado.
Mejor lo ilustro con ejemplos, porque puede sonar muy complicado, pero no lo es. Ya verás.
JUnit 307
Esqueleto de un test unitario
Partimos del código de una calculadora muy simple, a la que debemos someter a
nuestros mejores test. Esta calculadora solo sabe sumar y restar dos números, ya
dije que era simple:
package cap12;
public class Calculadora {
public static int suma(int a, int b) {
return a + b;
}
public static int resta(int a, int b) {
return a - b;
}
}
El código fuente suele ir metido en una carpeta src y, de ahí, en las carpetas correspondientes
a los paquetes de nuestra paquetización. Las clases de test suelen ir la misma paquetización
que las testadas. Sin embargo, para evitar remezclar el código y porque muchas veces el
código de test no va a ir incluido en las entregas, lo que hacemos
es tener una carpeta test, equivalente a la src, donde meteremos
el código de test, recuerda, respetando la paquetización, claro.
Para permitir el acceso a los métodos a testar, que si no se usan
fuera de la clase deberían ser privados (private), hay que
cambiarles la visibilidad a protegida (protected). Con los
métodos públicos no habrá problemas de acceso. Figura 12.1. Estructura del
proyecto, con código fuente
Para crear la clase de test con Eclipse (aunque puedes hacerlo y test separados.
a mano), sobre la clase Calculadora, haremos clic derecho e iremos a Nueva>Caso de
prueba JUnit. Escogeremos la versión de JUnit que queramos. En este caso, yo cogeré la
más nueva, JUnit Juniper, pero si en tu proyecto ya hay test, mejor seguir con la misma
versión. Corregimos la carpeta fuente a test. El paquete y el nombre de la clase nos los
sugiere automáticamente, cap12 y CalculadoraTest. Comprobamos que la clase sometida
a prueba sea la que queremos: Calculadora. Le damos a Siguiente.
Ahora nos ofrece la lista de métodos que podemos testar. Escogemos los dos de
Calculadora, en los de Objeto no nos meteremos.
De las casillas de verificación de abajo, la primera Crear apéndices de método finales
sirve para que los métodos sean cualificados como final, de forma que no se puedan
sobrescribir en clases hija. Y la segunda Crear tareas para los métodos de prueba
generados hará simplemente que, en el contenido autogenerado de las pruebas que
por lo general es inútil, se cree marcado con un comentario TODO para que no se
nos olvide corregirlo. No sé hasta qué punto es útil, pues como el código generado
hará fallar el test, tarde o temprano veremos que hay que corregirlo. ¡A tu gusto! Le
damos a Finalizar.
308 Test unitarios
Figura 12.2. Creación de un caso de prueba JUnit.
Figura 12.3. Creación de un caso de prueba JUnit, segundo paso.
Nos ha generado una clase con dos test, que son los métodos con la anotación @Test,
que se suelen llamar testLoQueSea por convención, aunque desde hace unas cuantas
versiones de JUnit ya no es necesario. En las antiguas, sin anotaciones, esa era la forma
de marcar qué métodos eran los test.
JUnit 309
package cap12;
import static [Link].*;
import [Link];
class CalculadoraTest {
@Test
void testSuma() {
fail("No implementado aun");
}
@Test
void testResta() {
fail("No implementado aun");
}
}
Aunque estos test van a fallar, veamos cómo se ejecutarían. Si hacemos clic derecho sobre la
clase CalculadoraTest y vamos a Ejecutar como>Prueba JUnit nos saltará rápidamente una
pestaña nueva, llamada JUnit, con no muy buenos resultados. Nos saca la lista de clases de
test que hemos ejecutado y la lista de métodos de cada
una de ellas. Por encima se hallan unos contadores que
nos dicen que se han ejecutado 2 de 2 test, que tenemos 0
errores y 2 anomalías. El contador de errores, el de la cruz
roja, nos indicará cuántos test han dado un error, es decir,
están mal escritos, no hemos logrado un resultado. El
contador de anomalías, el de la cruz azul oscuro, nos
marca cuántos test han fallado, es decir, han contrastado
que el resultado obtenido no es el esperado. La diferencia
parece ser un leve matiz, pero es importante. No es lo
mismo un test que no funciona (da error) que uno que Figura 12.4. Resultado de ejecutar los
funciona, pero no da el resultado esperado (anomalía). test autogenerados.
En este ejemplo, han sido dos anomalías: los test están bien escritos, pero el resultado da un
error. En la zona de abajo de la pestaña, se observan las causas de los errores o anomalías. En
este caso, nos dice «No implementado aun», que es justo el mensaje que hemos puesto, bueno,
que ha puesto Eclipse, en el cuerpo de cada test, forzando un fallo con el método fail.
T12.2
Aún no hemos probado nada, pero ya está preparado el esqueleto de nuestros test.
Primeros test unitarios
Partiendo del esqueleto de test de la Calculadora, implementemos esos test para que nos
resulten útiles.
Ahora mismo, los métodos autogenerados por Eclipse llaman al método fail con un mensaje:
«No implementado aun». Ese método, si te fijas (poniendo el ratón encima, por ejemplo), es
de la clase Assertions. JUnit nos ofrece una serie de método de aserciones que serán con los
que iremos comprobando que los resultados obtenidos son iguales a los esperados, o son o
310 Test unitarios
no nulos, ciertos o falsos… El método fail en concreto hará que nuestro test siempre dé un
fallo. Aunque parezca mentira, en algunos casos puede interesarnos, en serio.
Bueno, veamos testSuma, ahí queremos probar que la suma funciona bien. Es un método
muy simple, con poco misterio, pero aun así intentaremos someterlo a un par de pruebas
crueles. Primero la fácil: 2 + 3. ¿Cuánto debe dar? 5. Pues a por ello.
void testSuma() {
int res = [Link](2, 3);
assertEquals(5, res);
}
• Primero, haremos una llamada normal a nuestro método, pasándole como argumentos
un dos y un tres. En la variable res tendremos el resultado de la operación. Y lo que
comprobaremos es que ese resultado es igual al que esperamos: cinco.
• Segunda línea: assertEquals, valor esperado (expected, en inglés) 5, valor obtenido
(actual, en inglés) res. Aunque los test serán correctos o darán fallo igualmente los
pongamos del derecho o del revés, es importante respetar el orden de primero, el
esperado, y luego el obtenido, porque en caso de fallo esos valores se mostrarán en
el mensaje. Y no es lo mismo que te diga «esperado 3 obtenido 5» que «esperado 5
obtenido 3». Probemos eso también, con una segunda línea en la que el valor esperado
sea 6.
void testSuma() {
int res = [Link](2, 3);
assertEquals(5, res);
assertEquals(6, res);
}
Guardamos y aprovechamos para constatar cómo ejecutar un solo test, ya que el de la
resta aún no lo hemos resuelto: es muy fácil. Sobre el nombre del método, en vez de sobre
el nombre de la clase, hacemos clic derecho para ir a Ejecutar como>Prueba JUnit. Solo
se ejecutará ese test y… nos sigue dando fallo.
Reejecutar última prueba
Figura 12.5. Resultado de ejecutar el test de la suma, con test fallido.
JUnit 311
En las trazas de abajo, se aprecia que hay un fallo de aserción, porque esperábamos
un 6 y hemos obtenido un 5. Eso nos ha pasado por esa segunda línea de prueba
que hemos puesto para juguetear con el esperado/obtenido. Borramos esa línea y
volvemos a probar. Podemos darle al icono verde con flecha blanca, de ejecutar, con
flecha amarilla encima de Volver a. El tooltip de ese botón es Reejecutar última prueba
(ver figura 12.5).
Figura 12.6. Resultado de ejecutar el test de la suma, todo bien.
¡Ahora ya nos sale en verde! Bien, dos más tres son cinco y nuestra calculadora sabe
obtener el resultado.
Te animo a intentarlo para la resta. Aprovecha si quieres para juguetear un poco, nunca
está de más.
void testResta() {
int res = [Link](8, 3);
assertEquals(5, res);
}
T12.3
Ejecutamos ahora toda la clase, para probar cómo funcionan todos los test. Ya sabes:
E12.1
botón derecho sobre el nombre de la clase e ir a Ejecutar como>Prueba JUnit.
¡Todo bien! Ya sabemos escribir y ejecutar test simples.
Detección de regresiones
Hemos escrito los test para la suma y la resta, y todo funcionó bien. Ahora, cada vez
que ejecutemos los test unitarios, comprobaremos que todo sigue bien. En proyectos
con sistemas de integración continua, se suelen ejecutar los test de forma periódica,
de manera que, en cuanto algún cambio en el código hace que deje de funcionar
alguno de ellos, se detecta con celeridad. Estos sistemas son hasta capaces de mandar
un correo electrónico al autor del cambio para avisarle.
En efecto, nuestro código tan simple no parece que tenga muchos números de sufrir
cambios, pero nunca se sabe… Imaginemos que alguien pasa por el código y, algo
despistado, cambia el signo menos del método [Link] por un signo más.
Guardamos. Y reejecutamos los test.
312 Test unitarios
Esta vez, aunque el test de suma va bien, el de resta
ha fallado. Esperábamos un 5, pero obtuvimos un 11.
Si damos doble clic sobre la línea del testResta en las
trazas de ejecución del test, nos llevará directamente
a esa línea del código de test, para que así podamos ir
investigando qué ha pasado. Nos lleva a la línea del
assertEquals. Podemos comprobar que el test está bien
escrito, porque queremos restar 8 menos 3 y esperamos
que nos dé 5. Usa los dedos si hace falta, pero eso está
bien. Observemos el código de resta, con Ctrl-clic sobre
Figura 12.7. Resultado de ejecutar los
el nombre del método: return a + b. ¡Ahí está! ¡Si test de Calculadora, con una regresión.
queríamos restar! Fácil de corregir, pero ¡espera! no
lo toques todavía.
Aprovechemos la ocasión para destacar la importancia de escoger bien las pruebas que
hacemos. Imaginemos que la obsesión de nuestro jefe fuera simplemente que la tasa de
cobertura del código fuera suficiente como para hacer feliz a nuestro cliente. Ese es su
único objetivo para hacer los test unitarios, ¡qué pena!
La tasa de cobertura es el porcentaje de líneas del código que se ejecutan al ejecutar los
test, es decir, el porcentaje de líneas testadas. Se ve si le damos al botón Ejecutar que
debajo tiene una barrita verde/roja como la del JUnit (ver figura 12.8) y nos abre una
pestaña en la zona de abajo con la cobertura de cada clase. Si vamos desplegando, veremos
el porcentaje de cobertura de cada paquete, clase, método… Suma y resta están al 100 %,
así que tendríamos al jefe contento. Calculadora «solo» tiene un 72,7 %, probablemente
porque no estamos probando el constructor ni el equals ni otras cosas heredadas.
Ejecutar test unitarios con cobertura
Figura 12.8. Resultado de una ejecución de test unitarios con cobertura.
Detección de regresiones 313
Regresemos a lo de aprovechar la ocasión para ver la importancia de escoger bien las
pruebas. Volvemos a la clase de test. Y ahora imaginamos que lo de pensar una prueba
del estilo de 8 menos 5 era muy complicado, y no teníamos tiempo para echar cuentas,
y decidimos que era más práctico, más fácil, más rápido, poner simplemente 0 menos 0
es 0, total, la obsesión del jefe es la tasa de cobertura. Voy a ponerlo.
void testResta() {
int res = [Link](0, 0);
assertEquals(0, res);
}
Si volvemos a pasar la cobertura, seguimos al 100 % ¡y encima el test ya funciona!
¿Problema resuelto?
¡Va a ser que no! ¡Mira el código de resta! Al final no lo corregimos.
Corrígelo todo y luego ya te digo la moraleja. En el test volvemos al 8 menos 3 es 5. En
la resta, corregimos el código. Guardamos, volvemos a lanzar… Y ahora sigue estando
todo bien.
La moraleja: el objetivo de nuestros test no debe ser sacar una buena nota en la cobertura,
sino que nos permitan detectar errores en nuestro código, para corregirlo lo antes posible
y asegurar la máxima robustez posible para nuestra aplicación. Tómate tu tiempo para
pensar los test, prueba varios casos, pero buscando casos extremos, esos que puedan ser
más problemáticos. Lo veremos con más detalle.
Elección de los test unitarios adecuados
Estamos jugando con un ejemplo muy muy muy simple, una calculadora que suma y
que resta. No tiene mucho misterio ni mucho riesgo, pero hagamos una evolución que
nos motive un poco más.
Por eso de ser completos, añadimos la multiplicación y la división a la Calculadora.
public static int multiplica(int a, int b) {
return a * b;
}
public static int divide(int a, int b) {
return a / b;
}
Y ampliamos nuestros test. Por un lado, añadiremos test para multiplicación y división;
por otro, realizaremos más test de cada operación. Voy a la clase CalculadoraTest y, en
un ataque de pereza, copio y pego los test de suma y resta y creo los de multiplicar y
dividir, 2 por 3 tienen que ser 6, 8 entre 2 tienen que ser 4. Y listo:
@Test
void testMultiplica() {
int res = [Link](2, 3);
assertEquals(6, res);
}
314 Test unitarios
@Test
void testDivide() {
int res = [Link](8, 2);
assertEquals(4, res);
}
Compruebo que todo funciona, ya dispongo de cobertura para tener contento al jefe,
pero… Ahora vamos a pensar un poco mejor los test.
Empezamos por la suma… ¿Qué puntos problemáticos tendremos? Deberíamos
pensar en el elemento neutro de la suma: el cero. Bien, pues probemos cero más algo
es igual a algo, algo más cero igual a algo, cero más cero igual a cero. Luego veremos
qué pasa con los números negativos: algo más menos algo es igual a cero, menos
algo más algo igual a cero, a más b da lo mismo que b más a. Creo que para la suma
es suficiente.
Ahora lo implementamos. Hay dos opciones: un test por cada prueba, de forma que
se ejecuten todos aunque uno falle, o varias pruebas por test, para que haya que
crear y preparar menos test. En este caso tan simple está claro que es mucho mejor
un test por prueba, pero a veces, tras ejecutar un cálculo, nos puede interesar
comprobar varias cosas distintas a la vez. Bueno, pues nada, a duplicar el método.
Creo seis copias más de test suma, las numero e implemento lo dicho.
@Test
void testSuma1() {
int res = [Link](2, 3);
assertEquals(5, res);
}
@Test
void testSuma2() {
int algo = 8;
int res = [Link](0, algo);
assertEquals(algo, res);
}
@Test
void testSuma3() {
int algo = 7;
int res = [Link](algo, 0);
assertEquals(algo, res);
}
@Test
void testSuma4() {
int res = [Link](0, 0);
assertEquals(0, res);
}
@Test
void testSuma5() {
int algo = 3;
int res = [Link](algo, -algo);
assertEquals(0, res);
}
Elección de los test unitarios adecuados 315
@Test
void testSuma6() {
int algo = 5;
int res = [Link](-algo, algo);
assertEquals(0, res);
}
@Test
void testSuma7() {
int a = 2;
int b = 3;
assertEquals([Link](a, b), [Link](b, a));
}
Nos enfrentamos a la resta... Aquí el elemento neutro sigue siendo el cero, pero el orden
de los factores sí altera el resultado… Así pues, cero menos algo es igual a menos algo,
algo menos cero igual a algo, cero menos cero igual a cero, algo menos algo igual a cero.
Luego veremos qué pasa con los números negativos: algo menos algo igual a cero, algo
menos menos algo igual a dos veces algo, a menos b no da lo mismo que b menos a (siendo
a y b distintos, ¡claro!).
@Test
void testResta1() {
int res = [Link](8, 3);
assertEquals(5, res);
}
@Test
void testResta2() {
int algo = 3;
int res = [Link](0, algo);
assertEquals(-algo, res);
}
@Test
void testResta3() {
int algo = 8;
int res = [Link](algo, 0);
assertEquals(algo, res);
}
@Test
void testResta4() {
int res = [Link](0, 0);
assertEquals(0, res);
}
@Test
void testResta5() {
int algo = 7;
int res = [Link](algo, algo);
assertEquals(0, res);
}
@Test
void testResta6() {
int algo = 7;
int res = [Link](algo, -algo);
assertEquals(2 * algo, res);
}
316 Test unitarios
@Test
void testResta7() {
int a = 3;
int b = 2;
assertNotEquals([Link](a, b), [Link](b, a));
}
Para la multiplicación, tenemos elemento neutro el uno y además cuenta como elemento
absorbente el cero, así que probemos algo por uno, algo; uno por algo, algo; uno por uno,
uno; algo por cero, cero; cero por algo, cero; y cero por cero, cero. También podemos
hacer positivo por positivo, positivo; negativo por negativo, positivo; y positivo por
negativo, negativo. Cerramos con un a por b igual a b por a.
@Test
void testMultiplica1() {
int res = [Link](2, 3);
assertEquals(6, res);
}
@Test
void testMultiplica2() {
int algo = 9;
int res = [Link](algo, 1);
assertEquals(algo, res);
}
@Test
void testMultiplica3() {
int algo = 9;
int res = [Link](1, algo);
assertEquals(algo, res);
}
@Test
void testMultiplica4() {
int res = [Link](1, 1);
assertEquals(1, res);
}
@Test
void testMultiplica5() {
int algo = 4;
int res = [Link](algo, 0);
assertEquals(0, res);
}
@Test
void testMultiplica6() {
int algo = 4;
int res = [Link](0, algo);
assertEquals(0, res);
}
@Test
void testMultiplica7() {
int res = [Link](0, 0);
assertEquals(0, res);
}
Elección de los test unitarios adecuados 317
@Test
void testMultiplica8() {
assertTrue([Link](1, 1) > 0);
assertTrue([Link](-1, -1) > 0);
assertTrue([Link](1, -1) < 0);
assertTrue([Link](-1, 1) < 0);
}
Hasta ahora todo ha ido bien… nos queda la división, ya casi lo tenemos. Elemento
T12.4 neutro, de nuevo el uno, pero como en la resta, el orden de los factores altera el
resultado. Además, estamos trabajando con enteros que pueden dar resultados
decimales y… ¿qué pasa con las divisiones entre cero! Uy, quizá no lo tenemos tan
E12.2 cerca… De momento, ejecutamos todos los test con que contamos y comprobamos
que todo ha ido bien.
Testando cosas que van mal
Hemos testado la suma, la resta y la multiplicación. Hemos creado varios test para
cada uno de estos métodos, probando casos extremos, es decir, usando elementos
neutros, positivos, negativos. Nos queda pendiente la división. Pero probar la
división entera no es tan fácil… Sin embargo, empecemos por lo fácil. Ya teníamos
ocho entre dos, cuatro. Podemos hacer algo entre uno, algo. O a por b entre b, a. O
como estamos con la división entera, a por b más uno entre b, sigue siendo a (tiene
resto, pero lo perderíamos). Así que uno entre algo, sería 0 (salvo si ese algo fuera
uno, pues también lo probamos).
@Test
void testDivide1() {
int res = [Link](8, 2);
assertEquals(4, res);
}
@Test
void testDivide2() {
int algo = 7;
int res = [Link](algo, 1);
assertEquals(algo, res);
}
@Test
void testDivide3() {
int a = 3;
int b = 5;
int res = [Link]([Link](a, b), b);
assertEquals(a, res);
}
@Test
void testDivide4() {
int a = 3;
int b = 5;
int res = [Link]([Link](a, b) + 1, b);
assertEquals(a, res);
}
318 Test unitarios
@Test
void testDivide5() {
int algo = 7;
int res = [Link](1, algo);
assertEquals(0, res);
}
@Test
void testDivide6() {
int res = [Link](1, 1);
assertEquals(1, res);
}
@Test
void testDivide7() {
int algo = 7;
int res = [Link](algo, 0);
assertEquals(4, res);
}
Y ahora nuestro test estrella: algo entre cero. ¿Cuánto da? ¿Lo sabes? La verdad es que
no se puede dividir algo entre cero, pero ¿qué resultado dará? ¡Ni idea! ¿Cómo probarlo?
Lo dejamos así, vemos qué pasa y luego adaptamos. Guardamos y ejecutamos.
Figura 12.9. Resultado de una ejecución de los test unitarios con fallo en testDivide7.
Han ido bien todos los test salvo uno, como era de esperar, el testDivide7 ha dado un
error, es decir, no se ha podido ejecutar bien. No es que haya dado un resultado no
esperado, no, es que directamente no ha funcionado. Y abajo de todo nos lo indica muy
bien. Ha dado una ArithmeticException por intentar dividir entre cero.
Ahora ya sabemos cuál es el comportamiento de nuestro método divide cuando
intentamos dividir entre cero. ¿Pero es ese comportamiento adecuado? ¿Es el que
esperamos? Bueno, eso dependerá de las circunstancias, de las especificaciones… Por
un momento, supongamos que sí, que nos parece bien que lance una RuntimeException
ArithmeticException (sé que es runtime porque si no nos habría obligado a tratarla, tú
también lo sabes ya, ¿no?). Bien, en ese caso, ¿cómo comprobamos que cuando se le pasa
un cero lanza lo esperado?
Testando cosas que van mal 319
Existen varias opciones. Si recurrimos a JUnit 5, como es este caso, podemos usar
assertThrows al que le pasaremos la clase de la excepción que esperamos que lance, y
una expresión lambda (las aprenderemos en el capítulo 17) con la expresión a ejecutar,
es decir, la llamada a divide con un cero como segundo parámetro.
@Test
void testDivide7() {
int algo = 7;
assertThrows([Link], () -> {
[Link](algo, 0);
});
}
Otra forma de testar podría ser meter el código en un try catch y poner asserts o fails en
los puntos en los que esperamos que pase o no. En otras palabras: si tras ejecutar divide
con un 0 de divisor llegamos a la siguiente línea, mal. Si capturamos una ArithmeticException,
bien. Si capturamos cualquier otra cosa, mal de nuevo.
Guardamos y probamos, y todos los test, salvo error u omisión, deberían pasar bien.
@Test
void testDivide7A() {
int algo = 7;
try {
int res = [Link](algo, 0);
fail("Aquí no debería llegar");
} catch (ArithmeticException ae) {
assertTrue(true);
} catch (Throwable t) {
fail("No debería lanzar ningún throwable");
}
}
Pero todo esto era en el supuesto de que nos pareciera bien que el método divide reviente,
si recibe un cero, lanzando una excepción incontrolada… Enfoquémoslo de otra forma.
En la clase Calculadora implementemos una segunda versión de divide (para no perder
el test que ya tenemos), que actúe de otra forma.
public static int divide(int a, int b) {
return a / b;
}
public static int divideB(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("No podemos dividir entre cero.");
}
return a / b;
}
public static int divideC(int a, int b) throws ExcepcionPropia {
if (b == 0) {
throw new ExcepcionPropia("No podemos dividir entre cero.");
}
return a / b;
}
Por ejemplo, podríamos comprobar que b no sea cero y, si lo fuera, lanzar una
IllegalArgumentException, que precisamente sirve para esto, como en divideB o bien una
excepción propia nuestra, como en divideC.
320 Test unitarios
package cap12;
public class ExcepcionPropia extends Exception {
public ExcepcionPropia() {
super();
}
public ExcepcionPropia(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ExcepcionPropia(String message, Throwable cause) {
super(message, cause);
}
public ExcepcionPropia(String message) {
super(message);
}
public ExcepcionPropia(Throwable cause) {
super(cause);
}
}
En ambos casos las pruebas serían parecidas a lo visto en el primer caso, pero, claro, hay
que adaptar los nombres de las excepciones en las pruebas. Incluso podemos comprobar
que el mensaje recibido es el esperado.
@Test
void testDivide7B() {
int algo = 7;
assertThrows([Link], () -> {
[Link](algo, 0);
});
try {
int res = [Link](algo, 0);
fail("Aquí no debería llegar");
} catch (IllegalArgumentException ae) {
assertTrue(true);
} catch (Throwable t) {
fail("No debería lanzar ningún throwable");
}
}
@Test
void testDivide7C() {
int algo = 7;
assertThrows([Link], () -> {
[Link](algo, 0);
});
try {
int res = [Link](algo, 0);
fail("Aquí no debería llegar");
} catch (ExcepcionPropia ep) {
assertTrue(true);
assertEquals("No podemos dividir entre cero.", [Link]());
} catch (Throwable t) {
fail("No debería lanzar ningún throwable");
}
}
Guardamos y probamos, y todos los test, salvo error u omisión, deberían pasar bien.
Testando cosas que van mal 321
Espero que ya vayas haciéndote una idea de la importancia y utilidad de los test
unitarios y que hayas perdido un poco el pánico ante la posible visita de un jefe (o
profesor) diciéndote: «Ahora haz los test unitarios».
Juega un poco con lo que ya sabes, créate una clase propia, métele unos cuantos
métodos que hagan algunas chorradas, no sé, jugar con Strings, por ejemplo, créate
test para probarlos, rompe los test y los métodos… ¡Todo lo que rompas jugando no
lo romperás trabajando!
Objetos maquetados (mocks)
A veces, probar de forma unitaria nuestro código es complicado, por no decir imposible. Quizá
el ejemplo más claro, pero no el único, es cuando el método que deseamos probar accede a la
base de datos o usa otro módulo de nuestra aplicación o llama a un servidor de vaya usted a
saber dónde. También cuando necesitaríamos acceder a una funcionalidad que aún no está
implementada o cuando no controlamos el comportamiento de ese componente y necesitamos
provocar una respuesta determinada para hacer el test (que dé error, que devuelva un valor
determinado…).
En caso como estos, usaremos objetos maquetados o más conocidos como mocks (o mock objects).
Son objetos simulados que imitan el comportamiento de objetos reales de una forma controlada.
Serían como los muñecos esos, los dummies, que ponen en los coches para ver el comportamiento
del vehículo en caso de impacto y su efecto en el cuerpo humano.
Para hacer estos objetos mocks existen varias librerías, recurriremos a un ejemplo con Mockito,
cuyo nombre creo que viene del cóctel mojito y no de los fluidos que emanan de las naricitas
de los más pequeños.
Pongamos que disponemos de una SuperCalculadora que tendrá un método raiz que calcule
la raíz cuadrada de un número. Utilizaremos esta calculadora para, siguiendo las enseñanzas
de Pitagoras, obtener la hipotenusa de triángulo al que le pasaremos los dos catetos.
Clase Pitagoras
package [Link];
public class Pitagoras {
private SuperCalculadora sc;
public Pitagoras(SuperCalculadora sc) {
[Link] = sc;
}
public double hipotenusa(int cateto1, int cateto2) {
return [Link](cateto1 * cateto1 + cateto2 * cateto2);
}
}
322
Clase SuperCalculadora
package [Link];
public class SuperCalculadora {
public double raiz(int num) {
// TODO Apéndice de método generado automáticamente
return 0;
}
}
Hemos de preparar ya el test unitario, tenemos un test por implementar en PitagorasTest.
¡Pero SuperCalculadora aún no está implementado!
Clase de test PitagorasTest
package [Link];
import static [Link].*;
import [Link];
class PitagorasTest {
@Test
void testHipotenusaRedonda() {
fail("No implementado aun");
}
}
No te preocupes, Mockito va a ayudarnos.
Partiendo de ese proyecto, nuestra implementación «natural» del test sería algo así:
@Test
void testHipotenusaRedonda() {
SuperCalculadora sc = new SuperCalculadoraImpl();
Pitagoras pita = new Pitagoras(sc);
double hipo = [Link](3, 4);
assertEquals(5, hipo);
}
Es decir, si le pedimos a Pitágoras la hipotenusa de un triángulo de lados 3 y 4,
debería devolvernos un cinco.
Si ejecutamos… ¡Falla! Nos dice que espera un 5, pero tiene un 0. Claro. Porque la
hipotenusa no está aún implementada, ni sabremos cuándo lo estará, pero necesitamos
seguir adelante con los test. Lo simulamos entonces.
Configuración para utilizar Mockito
Lo primero que haremos para usar Mockito o cualquier otra librería, es añadirla en el fichero
[Link] (estamos usando un proyecto maven para facilitar esta tarea). Así pues, abrimos
[Link], quitamos la dependencia JUnit 3 que lleva y ponemos JUnit 5 y Mockito 5:
<!-- [Link] -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>junit5-engine</artifactId>
<version>5.0.0-ALPHA</version>
</dependency>
Objetos maquetados (mocks) 323
<!-- [Link] -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.3.0</version>
<scope>test</scope>
</dependency>
Puedes encontrarlas en [Link] o copiarlas a mano. Y, una vez con Mockito
instalado, modificamos el test.
Clase de test PitagorasTest utilizando Mockito
package [Link];
import static [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@ExtendWith([Link])
class PitagorasTest {
private Pitagoras pita;
private SuperCalculadora sc;
@BeforeEach
public void setup() {
sc = [Link]([Link]);
pita = new Pitagoras(sc);
[Link](this);
}
@Test
void testHipotenusaRedonda() {
[Link]([Link](25)).thenReturn(5.0);
double hipo = [Link](3, 4);
assertEquals(5, hipo);
}
}
• Primero, avisamos a JUnit que extenderemos nuestro sistema de test usando Mockito.
• Ya dentro de la clase, declararemos dos atributos privados: Pitagoras y SuperCalculadora.
• Creamos un método de setup, etiquetada con la anotación @BeforeEach, en el que creamos
los objetos. Para crear pita, sin problema, un new del constructor. Sin embargo, para sc,
no queremos utilizar la SuperCalculadora de verdad, porque no está implementada.
Necesitamos mockearla, utilizando [Link].
• Y en el método de test, quitamos la declaración de la calculadora y de pita (porque ya las
hemos puesto fuera) y llamamos a Mockito, diciéndole que cuando alguien llame a la
SuperCalculadora con un 25, ha de devolver un cinco.
Guardamos, probamos y, si lo hemos hecho todo bien, ¡funciona! ¡Y eso que seguimos sin
tener la raíz implementada!
324
Vale, tienes razón, este ejemplo es muy muy muy básico, pero es que solo pretende enseñarte
que existen los mocks y para qué sirven. Otro ejemplo cercano a este sería un método que
resuelva ecuaciones de segundo grado, esas del b más menos raíz cuadrada de… Podrías
querer probar el método de resolución de ecuaciones sin importarte si el método raíz está T12.5
implementado o no, funciona o no. Los mocks tienen potencial y cierta complejidad, así que,
si te interesan, te invito a investigar más sobre ellos.
Test
Test: 12.1. ¿Qué diferencia hay entre el ciclo de desarrollo clásico y el TDD?
a) En TDD primero escribimos los test y luego implementamos las funcionalidades.
b) En TDD solo escribimos test, no hace falta implementar funcionalidades.
c) En el ciclo clásico primero escribimos los test y luego implementamos las funcionalidades.
d) En el ciclo clásico no se escriben test, en TDD sí.
Test: 12.2. ¿Qué elemento de este fragmento de código indica a JUnit que se
trata de un test?
class DocumentoTest {
@Test
void testCrear() {
fail("No implementado aun");
}
}
a) La palabra Test en DocumentoTest.
b) El @Test de antes del método.
c) La palabra test en testCrear.
d) La llamada a fail dentro del método.
Test: 12.3. ¿Qué sentencia de aserción sería la más adecuada para completar los
puntos suspensivos?
void testContarElementos() {
int res = [Link]("documentoDe5Elementos");
...
}
a) assertNotEquals(8, res);
b) assertNotEquals(res, 8);
c) assertEquals(res, 5);
d) assertEquals(5, res);
Test: 12.4. ¿Cuáles de estos casos no aporta nada a la hora de testar el método
que calcula la longitud de un String?
a) [Link](null);
b) [Link]("");
c) [Link]("hola");
d) [Link]("adios");
Test: 12.5. Si necesitáramos mockear [Link](), completa estas sentencias:
[Link]([Link]("")).thenReturn(___);
[Link]([Link]("hola")).thenReturn(___);
[Link]([Link](" ")).thenReturn(___);
a) 0, 4, 0
b) 0, 1, 4
c) 0, 4, 1
d) 1, 5, 2
Test 325
Ejercicio 12.1. Testando el método cuadrado
Escribe test que comprueben que el método static int cuadrado(int n) de la clase Calculadora
funciona bien, para números negativos y positivos.
Ejercicio 12.2. Testando el Ejemplo02_07
Tomando el Ejemplo02_07 (del capítulo 2), escribe los test para los métodos siempreCierto
y siempreFalso.
Soluciones
Test 12.1. ¿Qué diferencia hay entre el ciclo de desarrollo clásico y el TDD?
a) En TDD primero escribimos los test y luego implementamos las funcionalidades.
Test 12.2. ¿Qué elemento de este fragmento de código indica a JUnit que se
trata de un test?
class DocumentoTest {
@Test
void testCrear() {
fail("No implementado aun");
}
}
b) El @Test de antes del método: la palabra test en el nombre del método era necesario
antes, pero ahora solo se usa por costumbre.
Test 12.3. ¿Qué sentencia de aserción sería la más adecuada para completar los
puntos suspensivos?
void testContarElementos() {
int res = [Link](
"documentoDe5Elementos");
...
}
d) assertEquals(5, res);: Efectivamente, mejor comprobar que el resultado es
el que esperamos y no uno aleatorio e inesperado, y siempre, primero el esperado,
luego el obtenido.
Test 12.4. ¿Cuáles de estos casos no aporta nada a la hora de testar el método
que calcula la longitud de un String?
d) [Link]("adios");: Como ya hemos testado con una palabra de cuatro
caracteres ("hola") en el caso c, testar con una de 5 ("adios") no aporta nada nuevo.
Test 12.5. Si necesitáramos mockear [Link](), completa estas sentencias:
[Link]([Link]("")).thenReturn(___);
[Link]([Link]("hola")).thenReturn(___);
[Link]([Link](" ")).thenReturn(___);
c) 0, 4, 1: ¡Cuidado! En el tercer caso hay un espacio entre las comillas.
326 Test unitarios
Ejercicio 12.1. Testando el método cuadrado
Los test más básicos sería con un test probar el cuadrado de un número positivo y luego
con otro probarlo con un número negativo.
@Test
void testCuadradoPositivo() {
int res = [Link](2);
assertEquals(4, res);
}
@Test
void testCuadradoNegativo() {
int res = [Link](-2);
assertEquals(4, res);
}
También podemos comprobar si el resultado de un número y su negativo coinciden:
@Test
void testCuadrados() {
int num = 2;
int cuadradoPositivo = [Link](num);
int cuadradoNegativo = [Link](-num);
assertEquals(cuadradoPositivo, cuadradoNegativo);
}
Ejercicio 12.2. Testando el Ejemplo02_07
Lo primero que tendremos que hacer es cambiar la visibilidad de los métodos, para que
sean protegidos y podemos llamarlos desde otra clase del mismo paquete.
private protected static boolean siempreCierto() {
[Link]("siempreCierto");
return true;
}
private protected static boolean siempreFalso() {
[Link]("siempreFalso");
return false;
}
Y ahora, ya las pruebas:
class Ejemplo02_07Test {
@Test
void testSiempreCierto() {
assertTrue(Ejemplo02_07.siempreCierto());
}
@Test
void testSiempreFalso() {
assertFalse(Ejemplo02_07.siempreFalso());
}
}
Como estos métodos no tienen parámetros, solo tenemos un caso por probar.
NOTA:
Las aserciones assertTrue y assertFalse comprueban si el valor recibido es cierto o falso,
respectivamente.
Soluciones 327
13 Trazas de ejecución
En este capítulo aprenderás a:
• Programar de una forma más profesional.
• Utilizar la librería log4j.
• Decidir el nivel de trazas adecuado.
• Aplicar buenas prácticas al escribir trazas.
Introducción
Hasta ahora, a lo largo de este libro, hemos estado escribiendo [Link]()
o [Link]() para pintar trazas por la consola y saber por dónde iba la
ejecución del código que estábamos probando. Estamos aprendiendo, y es la forma
más fácil de hacerlo, sin necesitar usar librerías ni aprender demasiadas cosas nuevas.
Está bien. Pero si queremos escribir código profesional, se acabó el usar el [Link]
o el [Link].
A partir de ahora utilizaremos un sistema de trazas (logs) para este fin. Aparte de
profesionalizar nuestro código, estos sistemas nos aportarán una flexibilidad que el
[Link] o el [Link] no nos pueden dar (aparte de pintar en negro o en rojo). Estos
sistemas suelen ser muy configurables.
Niveles de prioridad
Cuando escribimos una traza debemos indicar en qué nivel hay que hacerlo. Según el
nivel de trazas que configuremos, solo se escribirán las trazas de ese nivel o superior.
Según la librería que utilices, los nombres y el número de niveles de prioridad variarán
un poco, pero veamos los de log4j a modo de ejemplo, de más grave a más leve.
Tabla 13.1. Niveles de prioridad en log4j.
Nivel Descripción Método Recomendado en
OFF Deshabilita las trazas. No se mostrará ninguna. Es el nivel * Nunca
de menor detalle.
FATAL Para errores severos que causan una terminación fatal() Producción
prematura de la ejecución del programa. Ha pasado algo
muy muy grave. Probablemente no podemos seguir.
ERROR Para otros errores en tiempo de ejecución o por error() Producción
condiciones inesperadas. Ha sucedido algo, no es bueno,
pero, aunque algún proceso no haya podido seguir, otros
quizá sí.
WARN Nivel de aviso. Puede que haya algún problema, aunque warn() Producción
de momento todo funciona bien. Lo podemos utilizar
para indicar cosas que no son como esperábamos que
fueran, pero que no necesariamente tienen que estar mal.
INFO Eventos de ejecución interesantes (arranque y parada), info() Test
a veces también entrada y salida de un proceso o un
método, o para seguir los pasos del usuario.
DEBUG Cualquier cosa que pase en el programa. debug() Desarrollo
330 Trazas de ejecución
Nivel Descripción Método Recomendado en
TRACE Detalle extremo a utilizar solo durante el desarrollo, no trace() Desarrollo
deberíamos dejar estas trazas (ni siquiera desactivadas)
en el código entregable.
T13.1
ALL Máximo detalle, habilita las trazas de todos los niveles. Es * Desarrollo
equivalente al nivel más bajo, en este caso, TRACE.
T13.2
* No podemos escribir trazas en este nivel, es un nivel de configuración, pero no de traceo.
Configuración
Usemos la librería de trazas que usemos, estas suelen ser muy configurables. A modo
de ejemplo, veremos cómo funciona la configuración de log4j, pero lo importante es
entender los conceptos configurables y, luego, dada una librería concreta, aprender a
configurarla bien, ya sea utilizando un fichero .properties, JSON, XML o incluso
directamente en el código.
En log4j hay tres componentes principales: loggers, appenders y layouts. Siento tanto
anglicismo, pero es el vocabulario que se utiliza. Estos tres componentes trabajan juntos
para permitirnos un control casi total sobre el sistema de trazas.
Los loggers son entidades con nombre, que en versiones antiguas de log4j se llamaban
categorías. Los nombres están jerarquizados de forma parecida a los paquetes. Los
atributos que configuremos para un logger padre serán heredados por sus hijos. Tener
varios loggers nos permite configurar de forma distinta el sistema de trazas de paquetes
o clases diferentes. De esta forma, puedes establecer un nivel de trazas (o el resto de
elementos configurables) adaptado a las necesidades de cada paquete o clase.
El segundo concepto a tratar son los appenders, que no son más que las distintas vías
de salida en las que imprimir las trazas: consola, fichero, servidores remotos… A un
mismo logger le podemos asignar varios appenders, por ejemplo, para imprimir las trazas,
a la vez, en consola y en un fichero.
Si escogemos la salida por consola, poco podremos configurar, pero si utilizamos un
fichero, se abre un mundo de posibilidades, como indicar el nombre del fichero, el tamaño
máximo que alcanza, si queremos un fichero nuevo cada día o cada mes, el número de
ficheros de backup que conservamos, cuál es el criterio de rotación de los ficheros… No
me quiero meter en más detalle, porque las librerías suelen ir bien documentadas, y allí
encontrarás toda la información actualizada.
Los layouts nos permitirán definir el formato de cada línea de las trazas, utilizando un
patrón en el que indicaremos los datos a mostrar y el formato de los mismos. Por ejemplo,
el patrón "%r [%t] %-5p %c - %m%n" dará una salida parecida a esta:
176 [main] INFO [Link] - Tomatitos no es compatible con hijo sin ojo
Configuración 331
Que nos dice, en este orden: el tiempo transcurrido desde el arranque, la hebra en ejecución,
el nivel de la traza (ocupando 5 caracteres para que quede bien alineado), el nombre del logger
que la genera, un guion y el mensaje propiamente dicho.
Podríamos utilizar formatos distintos para la salida por consola o la escritura en fichero, según
nos convenga.
Leer trazas en ficheros o en consola puede ser una locura, es una ciencia en sí misma, pero
también hay herramientas capaces de leer esos ficheros y mostrarlos por pantalla de una forma
gráfica más amigable.
Uso
Ya sabemos configurar las trazas y los niveles de traza que podemos utilizar, veamos ahora
cómo generarlas. El primer paso, en cada clase, es inicializar el logger. Dependerá de las librerías,
las versiones, la forma de utilizarlo, pero podría ser una línea como esta:
Logger logger = [Link]("[Link]");
Más adelante, en el código de los métodos de esa clase, podemos escribir las trazas que
E13.1 consideremos:
[Link]([Link]() + " no es compatible con " + [Link]());
Buenas prácticas
Como ves, escribir trazas de forma profesional es fácil, así que no hay razón para seguir
utilizando [Link]. Sin embargo, hay unas cuantas buenas prácticas que me gustaría que
tuvieras en cuenta:
1. Utiliza una librería existente, no implementes por tu cuenta un sistema de trazas. No
hay necesidad de reinventar la rueda.
2. Escoge el nivel de traza adecuado, según lo explicado en la tabla 13.1, teniendo en
cuenta que:
• TRACE: no deberías usarlo en producción, solo cuando estás trasteando con tu código,
no lo incluyas en el código entregable, suprime estas trazas cuando ya hayan hecho
su función: ayudarte a que el código funcione bien.
• DEBUG: escribe en este nivel esas trazas que te gustaría activar si hay problemas
serios en producción. Normalmente no estarán activas, pero, en caso de problemas,
serían útiles.
3. Escribe mensajes significativos. Piensa que van a ser leídos en situaciones complicadas,
cada línea estará camuflada entre cientos, miles de otras líneas, fuera de contexto.
4. Escribe los mensajes en inglés, especialmente si trabajas en proyectos internacionales.
Lo mismo aplica para los nombres de variables, métodos, documentación… Te aseguras
332 Trazas de ejecución
de que no necesitarás caracteres especiales (como acentos, eñes…) y que serán entendidos
por desarrolladores de cualquier rincón del mundo. Pero en este libro no lo haré.
5. Añade contexto a los mensajes: no es lo mismo decir «una planta no es compatible con
otra» que «tomatitos no es compatible con hijo sin ojo», siendo tomatitos e hijo sin ojo
identificadores de plantas. Otro ejemplo: compara «transacción fallida» con «transacción
1234 ha fallado por error-321: fondos insuficientes».
6. No traces demasiado ni demasiado poco. Si hay demasiadas trazas, encontrar algo será
como buscar una aguja en un pajar, y pronto se llenarán los ficheros o incluso el disco. Si
las trazas son demasiado escasas, cuando haya un problema a resolver, faltará información.
La práctica te ayudará a encontrar el equilibrio.
7. No traces información delicada: ni se te ocurra escribir en una traza una contraseña, un
número de cuenta, un DNI, pero tampoco cualquier dato personal (podrías meterte en
un problemón, a ti, a tu empresa y a tu cliente, por incumplir la ley de protección de datos).
Si necesitas incluir identificadores internos para darles contexto a los mensajes, asegúrate
de que no pueden ser utilizados con fines malignos.
8. Evita el coste de generar el mensaje si no se va a mostrar. Hemos visto que, en función
del nivel que indiquemos al generar la traza y el que configuremos, se mostrará o no ese
mensaje, pero en todos los casos se generará. Por tanto, si vamos a generar un mensaje
con un coste significativo (concatenación de muchos Strings, cálculo de algún dato, consulta
a una base de datos…). Para lograrlo, simplemente debes rodear la línea para generar la
traza con un if que compruebe el nivel de log, por ejemplo:
if ([Link]()) {
[Link](mensajeComplejo)
}
9. Las trazas no son solo para resolver problemas, también se utilizan para auditorías,
estudios de rendimiento y estadísticas. Usar distintos loggers y appenders ayudará en este
punto.
T13.3
10. Lee las trazas que generas, es la mejor forma de comprobar que cumplen las buenas
prácticas anteriores.
Ejemplo práctico
La teoría está muy bien, pero seguro que viéndolo en un ejemplo práctico queda todo mucho
más claro. Haremos un programa que simplemente saque trazas de distintos niveles.
Dependencias
Lo primero que requerimos es crear un proyecto maven al que añadir la dependencia de
log4j2. También se podría hacer un proyecto normal e incluirle las librerías. Para la opción
maven, en Eclipse, nos dirigimos a Nuevo>Proyecto…>Maven>Maven Project. Nos vale
con el arquetipo maven-archetype-quickstart.
Ejemplo práctico 333
El fichero [Link] contiene la configuración de los proyectos maven. Añadimos, dentro
de la etiqueta de dependencias, la de log4j:
<project xmlns="[Link]
xmlns:xsi="[Link]
xsi:schemaLocation="[Link] [Link]
xsd/[Link]">
<modelVersion>4.0.0</modelVersion>
<groupId>cap13</groupId>
<artifactId>trazas</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>trazas</name>
<url>[Link]
<properties>
<[Link]>UTF-8</[Link]>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- [Link] -->
<dependency>
<groupId>[Link].log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.0</version>
</dependency>
</dependencies>
</project>
Configuración
Para la configuración de log4j, podemos utilizar un fichero [Link] que
ubicaremos bajo src/main/resources:
status = warn
# Log to console and rolling file
[Link] = [Link]
[Link] = info
[Link] = false
[Link] = LogToRollingFile
[Link] = LogToConsole
[Link] = Console
[Link] = LogToConsole
[Link] = PatternLayout
[Link] = %d{yyyy-MM-dd HH:mm:[Link]} [%t] %c{1} [%-5level]
- %msg%n
# Rotate log file
[Link] = RollingFile
[Link] = LogToRollingFile
[Link] = logs/[Link]
[Link] = logs/$${date:yyyy-MM}/trazas-%d{yyyyMMdd}-%[Link]
334 Trazas de ejecución
[Link] = PatternLayout
[Link] = %p %C{1.} > %m%n
[Link] = Policies
[Link] = TimeBasedTriggeringPolicy
[Link] = SizeBasedTriggeringPolicy
[Link]=1KB
[Link] = DefaultRolloverStrategy
[Link] = 10
[Link] = warn
[Link] = LogToConsole
En esta configuración de ejemplo, estamos utilizando dos salidas, consola y un fichero
[Link]. Cada una tiene un patrón de línea distinta. En el caso de fichero, es rotativo
por tamaño y fecha, pero le hemos puesto un tamaño máximo muy pequeño para verlo
rotar en un par de ejecuciones.
Verás que se ha indicado un nivel en tres puntos:
• En la primera línea, tenemos status = warn. Ahí estamos indicando el nivel de trazas
del propio sistema de trazas. Si lo bajas hasta debug, comprobarás que salen muchas
trazas que no siempre nos resultan útiles.
• En la línea cinco, tenemos [Link] = info. En este caso sí estamos precisando
el nivel de trazas de un logger, en concreto, el de [Link]. Será con este que
jugaremos para ver las diferencias.
• En la penúltima línea, [Link] = warn, indica el nivel para el logger de raíz,
del que heredarán el resto.
Clase de ejemplo [Link]
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class App {
private static Logger logger = [Link]([Link]);
public static void main(String[] args) {
[Link]([Link]("Nivel actual del log: {0}",
[Link]()));
[Link]("hola trace");
[Link]("hola debug");
[Link]("hola info");
[Link]("hola warn");
[Link]("hola error");
[Link]("hola fatal”);
[Link]([Link], "otra forma de hacerlo");
if ([Link]()) {
[Link](calculoPesado(1));
}
[Link](calculoPesado(2));
}
Ejemplo práctico 335
public static String calculoPesado(int n) {
String mensaje = "*** Aquí hacemos algo muy muy pesado " + n;
[Link](mensaje);
return mensaje;
}
}
En el método main imprimimos primero el nivel de trazas configurado, para luego
imprimir una traza de cada nivel.
En un segundo bloque, imprimimos de distintas formas mensajes de nivel info. En el if
aplicamos la buena práctica número ocho que comentábamos en párrafos anteriores.
Salida por consola
Mostraré la salida de solamente tres casos, pero te sugiero probarlos todos por tu cuenta.
Nivel debug
Ponemos [Link] = debug:
2021-03-24 19:24:59.466 [main] App [FATAL] - Nivel actual del log: DEBUG
2021-03-24 19:24:59.468 [main] App [DEBUG] - hola debug
2021-03-24 19:24:59.468 [main] App [INFO ] - hola info
2021-03-24 19:24:59.468 [main] App [WARN ] - hola warn
2021-03-24 19:24:59.469 [main] App [ERROR] - hola error
2021-03-24 19:24:59.469 [main] App [FATAL] - hola fatal
2021-03-24 19:24:59.469 [main] App [INFO ] - otra forma de hacerlo
*** Aquí hacemos algo muy muy pesado 1
2021-03-24 19:24:59.469 [main] App [INFO ] - *** Aquí hacemos algo muy muy pesado 1
*** Aquí hacemos algo muy muy pesado 2
2021-03-24 19:24:59.469 [main] App [INFO ] - *** Aquí hacemos algo muy muy pesado 2
Comprobamos que se muestran casi todos los mensajes, solo falta el de nivel trace, que
es inferior al nivel configurado, debug.
Nivel info
Si subimos el nivel a info ([Link] = info):
2021-03-24 19:27:04.193 [main] App [FATAL] - Nivel actual del log: INFO
2021-03-24 19:27:04.195 [main] App [INFO ] - hola info
2021-03-24 19:27:04.196 [main] App [WARN ] - hola warn
2021-03-24 19:27:04.196 [main] App [ERROR] - hola error
2021-03-24 19:27:04.196 [main] App [FATAL] - hola fatal
2021-03-24 19:27:04.196 [main] App [INFO ] - otra forma de hacerlo
*** Aquí hacemos algo muy muy pesado 1
2021-03-24 19:27:04.196 [main] App [INFO ] - *** Aquí hacemos algo muy muy pesado 1
*** Aquí hacemos algo muy muy pesado 2
2021-03-24 19:27:04.196 [main] App [INFO ] - *** Aquí hacemos algo muy muy pesado 2
En este caso hay una línea menos, la de nivel debug.
336 Trazas de ejecución
Nivel error
Elevando el nivel hasta error ([Link] = error), el resultado debería ser mucho
más escueto:
2021-03-24 19:28:38.854 [main] App [FATAL] - Nivel actual del log: ERROR
2021-03-24 19:28:38.857 [main] App [ERROR] - hola error
2021-03-24 19:28:38.857 [main] App [FATAL] - hola fatal
*** Aquí hacemos algo muy muy pesado 2
Ya solo vemos las trazas de nivel error y fatal, pero ¡espera! Siguen saliendo esos tres
asteriscos. Veamos la aplicación de la octava buena práctica.
Disponemos de un método que se supone que calcula algo muy pesado, aunque en realidad
solo pinta un mensaje por la consola, al que con el parámetro 1 solo lo llamamos cuando
está cierto nivel de trazas activado, mientras que con el parámetro 2 lo llamamos siempre.
if ([Link]()) {
[Link](calculoPesado(1));
}
[Link](calculoPesado(2));
Con la técnica de poner el if ([Link]<nivel>Enabled()) evitamos que se prepare ese
mensaje tan pesado.
Ficheros de logs
Hemos ejecutado tres veces la aplicación, cada vez con un nivel de trazas distinto.
FATAL [Link] > Nivel actual del log: DEBUG
DEBUG [Link] > hola debug
INFO [Link] > hola info
WARN [Link] > hola warn
ERROR [Link] > hola error
FATAL [Link] > hola fatal
INFO [Link] > otra forma de hacerlo
INFO [Link] > *** Aquí hacemos algo muy muy pesado 1
INFO [Link] > *** Aquí hacemos algo muy muy pesado 2
FATAL [Link] > Nivel actual del log: INFO
INFO [Link] > hola info
WARN [Link] > hola warn
ERROR [Link] > hola error
FATAL [Link] > hola fatal
INFO [Link] > otra forma de hacerlo
INFO [Link] > *** Aquí hacemos algo muy muy pesado 1
INFO [Link] > *** Aquí hacemos algo muy muy pesado 2
FATAL [Link] > Nivel actual del log: ERROR
ERROR [Link] > hola error
FATAL [Link] > hola fatal
El formato de cada línea es distinto a lo que veíamos por consola, porque así lo configuramos.
En el fichero de trazas no vemos la diferencia entre aplicar o no la octava buena práctica, pero
aun así es posible afirmar que sí se ha ejecutado calculoPesado(2), el código pesado en el tercer
intento, el de nivel error, aunque no utilicemos su resultado para nada.
Si ejecutas más veces, el fichero pronto superará el tamaño máximo establecido y verás
en funcionamiento los ficheros rotativos, como en la figura 13.1
Ejemplo práctico 337
Figura 13.1. Estructura de ficheros del proyecto
Test y ejercicios
Test 13.1. ¿Qué secuencia de niveles es correcta (aunque incompleta)?
a) INFO - TRACE - WARN - FATAL
b) DEBUG - INFO - WARN - ERROR
c) TRACE - WARN - DEBUG - FATAL
d) FATAL - ERROR - DEBUG - INFO
Test 13.2. Si el nivel de trazas está configurado en WARN, ¿qué niveles de
trazas se imprimirían?
a) TRACE - DEBUG - INFO
b) ERROR - FATAL
c) WARN - ERROR - FATAL
d) TRACE - DEBUG - INFO - WARN
Test 13.3. ¿Cuál de estos mensajes cumple mejor las buenas prácticas?
a) Pago correcto - Pedido: 8765432 - CCC: 1234 5678 9012 3456 7890
b) Pago correcto
c) Pago correcto - Pedido: 8765432
d) 8765432
Ejercicio 13.1. Mis primeras trazas
Sustituye los [Link] de este fragmento de código por trazas usando un logger llamado logger.
private static void pintaResultadoPlantar(IPlanta planta,
IMaceta maceta) {
if (maceta != null) {
[Link]("He plantado " + [Link]() + " en
"
+ [Link]());
} else {
[Link]("No he podido plantar " + [Link]());
}
}
338 Trazas de ejecución
Soluciones
Test 13.1. ¿Qué secuencia de niveles es correcta (aunque incompleta)?
b) DEBUG - INFO - WARN - ERROR: Aunque falten niveles extremos, debug es para un
mayor nivel de detalle que info, que a su vez lo es de warn, que lo es de error.
Test 13.2. Si el nivel de trazas está configurado en WARN, ¿qué niveles de
trazas se imprimirían?
c) WARN - ERROR - FATAL: El nivel indicado en la traza sí se muestra, ese y los de niveles
superiores.
Test 13.3. ¿Cuál de estos mensajes cumple mejor las buenas prácticas?
c) Pago correcto - Pedido: 8765432: la opción a) incluye un número de cuenta, a la b) le
falta contexto y a la d) también.
Ejercicio 13.1. Mis primeras trazas
private static void pintaResultadoPlantar(IPlanta planta,
IMaceta maceta) {
if (maceta != null) {
[Link]("He plantado " + [Link]() + " en "
+ [Link]());
} else {
[Link]("No he podido plantar " + [Link]());
}
}
Repara en que he decidido, para el primer mensaje de éxito, un nivel info, mientras que,
para el caso fallido de no lograr plantar, un nivel warn. No he puesto nivel error, porque
en el macetohuerto si no se logra plantar en una maceta, se intenta en otra, así que puede
que no sea grave.
Soluciones 339
14 Proyecto
«Gestión de récords»
En este capítulo aprenderás a:
• Utilizar las excepciones y generar trazas en un proyecto real.
• Realizar test unitarios sobre un proyecto real.
Introducción
Llegando al final de la tercera parte, implementaremos el tercer proyecto.
Hemos aprendido un montón de cosas sobre buenas prácticas profesionales: manejo de
excepciones, test unitarios y trazas de ejecución. Ahora, pongámoslas en práctica con un
proyecto: un programa para gestionar el tablero de récords de un juego, RecordsManager,
lo llamaremos.
• Este programa leerá los registros de todos los jugadores de un fichero de texto
(«[Link]»), en el que en cada línea aparecerán el nombre del jugador y su
puntuación máxima, separados por un espacio.
• Si el nombre de algún jugador es demasiado corto (menos de seis caracteres), se
añadirá a dicho nombre un número aleatorio de las cifras que le falten para alcanzar
dicha longitud, y se informará al usuario del nuevo nombre.
• Si la puntuación de algún jugador es demasiado baja (menos de mil puntos), se
descartará y no será incluida en la salida.
• Una vez preparada la salida, se le mostrará al usuario por pantalla y se le pedirá
confirmar los datos. Si los confirma, se volcará el resultado en un fichero de salida
([Link]).
Abordaremos una posible solución. Pero puede que tú lo hagas diferente. No pasa nada.
Tienes criterio suficiente para ver si vas bien o no. Pero si se te presentan dudas, puedes
plantearme preguntas, que encantada responderé. Mi solución no es necesariamente la
mejor ni desde luego tampoco la peor. He escogido esta porque, aparte de que es la que
se me ha ocurrido, he buscado que sea lo más didáctica posible en cuanto a las técnicas
que queremos afianzar, a la vez que he intentado mantener simple el resto del código.
Verás que el código está en inglés, aunque la interfaz de usuario está en castellano.
Bien, programar en inglés es una buena práctica que favorece la colaboración
internacional de programadores. Tanto en proyectos opensource como en empresas,
al poner el código en inglés (idioma que también deberíamos usar para los
comentarios) nos aseguramos de poder trabajar con gente de otros países con otras
lenguas maternas. Imagina que tu empresa se asocia, compra o es comprada por
una empresa, no sé, ¡danesa! ¿Cómo llevas el danés? ¿Y cómo llevan los daneses el
español? Mejor si todos programamos en inglés, ¿no?
Veamos la solución por fases, según la fui codificando. Te mostraré una versión del código
e intentaré explicar por qué lo hice así. Al final, te propondré unas cuantas evoluciones
que puedes desarrollar por tu cuenta, si te has quedado con ganas de mejorarlo.
342 Proyecto «Gestión de récords»
Funcionalidad básica del programa
En esta primera fase, empezamos a escribir el código ignorando los posibles problemas
que puedan surgir, centrándonos en la funcionalidad del programa.
• Creamos un método main en el que implementaremos los pasos que nos han pedido
en el enunciado.
• Declaramos unas cuantas constantes que nos serán útiles más adelante, como los
nombres de los ficheros de entrada/salida o los mínimos exigidos por el programa.
1. Mediante Scanner, leemos el fichero de entrada. Por cada línea, la troceamos
usando el espacio en blanco como separador, y recogemos nombre y puntuación
de cada jugador. Llamamos a un método para validar los datos (que ya escribiremos
más tarde) y los añadimos al diccionario de jugadores (usaremos un Map para
eso, que es una estructura de datos tipo diccionario que conoceremos mejor en
el capítulo 15), con el nombre del jugador como clave y su puntuación como
valor. Al final de esta fase, trazamos a nivel info, que la línea ha sido tratada.
2. Por cada uno de los elementos del diccionario (nuestros jugadores) imprimimos
por consola su nombre y su puntuación.
3. Pedimos confirmación al usuario sobre la corrección de los datos mostrados
y recogemos su respuesta mediante otro Scanner, esta vez para leer de la
entrada estándar.
4. Si su respuesta es afirmativa, escribimos en el fichero de salida los datos del
diccionario.
Como hemos ignorado completamente los problemas, esta versión del código no compila.
Para lograrlo, tendremos que tratar, al menos, las excepciones (no runtime) que lanzan
los métodos que hemos usado.
Clases RecordsManager
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
// Atención, esta versión del código no compila
public class RecordsManager {
private static Logger logger = [Link]([Link]);
Funcionalidad básica del programa 343
private static final String INPUT_FILE = "[Link]";
private static final String OUTPUT_FILE = "[Link]";
private static final int MIN_NAME_LENGTH = 6;
private static final int MIN_SCORE = 1000;
public static void main(String[] args) {
// usaremos un map (o diccionario) para guardar los datos leídos
// nombre como clave y puntuación como contenido
Map<String, Integer> players = new HashMap<>();
/// (1) Leer fichero de entrada
Scanner s = new Scanner(new File(INPUT_FILE));
// mientras tenga más líneas
while ([Link]()) {
// cogemos la siguiente línea
String line = [Link]();
// y la troceamos por los espacios
String[] data = [Link](" ");
// el primer fragmento será el nombre del jugador
String name = data[0];
// y el segundo fragmento su puntuación
int score = [Link](data[1]);
// comprobamos que todos los datos del jugador son correctos
validatePlayer(name, score);
// añadimos este jugador al diccionario
[Link](name, score);
[Link]("Línea tratada correctamente: " + name + " - " + score);
}
/// (2) Mostrar por consola todos los datos guardados
[Link]("Datos procesados: ");
for (String name : [Link]()) {
[Link](name + ":\t" + [Link](name));
}
/// (3) Pedir confirmación al usuario
[Link]("¿Son correctos? [S]i/[N]o");
// leemos la respuesta del usuario de la entrada estándar
Scanner sConsole = new Scanner([Link]);
String answer = [Link]();
// y comprobamos si es afirmativa
boolean confirmed = [Link]("S");
/// (4) Escribir a fichero
if (confirmed) {
[Link]("Procedemos al volcado de datos del fichero...");
// abrimos el fichero de salida para escribir en él
FileOutputStream fos = new FileOutputStream(OUTPUT_FILE);
// por cada uno de los jugadores en nuestro diccionario
for (String name : [Link]()) {
// escribimos una línea en el fichero de salida
[Link]((name + " " + [Link](name) + "\n").getBytes());
}
}
}
private static void validatePlayer(String name, int score) {
}
}
344 Proyecto «Gestión de récords»
Validaciones
El enunciado indicaba como requisito un par de condiciones sobre cada uno de los
jugadores: que su nombre tuviera al menos seis caracteres y que la puntuación fuera de
al menos mil puntos. Como en el caso de que falle la validación del nombre hay que
resolver el problema, pero si falla la validación de la puntuación vamos a descartar esa
línea, nos resultará más fácil independizar ambas validaciones, aunque inicialmente
habíamos pensado en hacer un método que lo validara todo.
5. En un método privado validateName, que recibe el nombre, validamos la longitud
de este y lanzamos una PlayerNameTooShortException si no cumple. Tendremos
que crear esa excepción.
En este proyecto usaré una aproximación distinta a la que habíamos visto en el
ejemplo, ¿te acuerdas de la BusinessException y de la TechnicalException? Esta
vez crearé una excepción distinta para cada tipo de error. Le ponemos un
constructor que reciba el nombre, que guardamos en un atributo. También
sobrescribimos el getMessage para que devuelva «El nombre del jugador
(<nombre>) es demasiado corto».
6. Para validar la puntuación mínima, hacemos algo parecido y lanzamos una
ScoreTooLowException, que también hemos de crear. Con nombre y puntuación en
el constructor (y como atributos) y mensaje «El usuario <nombre> tiene menos puntos
(<puntos>) de los requeridos».
7. Volviendo al main, validamos el nombre, envolviendo la llamada en un try/catch en
el que capturaremos la PlayerNameTooShortException. Informaremos al usuario del
problema sucedido, mediante una traza de nivel de aviso, pues, aunque sea un error,
lo resolvemos generando nuevo nombre según las especificaciones y seguimos con
el curso normal del programa.
Para generar el nuevo nombre, veremos cuántos caracteres nos faltan e iremos
añadiendo una cifra aleatoria hasta llegar al mínimo requerido, en el método
generateNewName.
8. Validamos la puntuación, envolviendo la llamada con un try/catch en el que
capturaremos la ScoreTooLowException. Informaremos al usuario del problema
sucedido, con una nueva traza, esta vez de error, y cortaremos la ejecución de esta
iteración del bucle con un continue, que nos llevará a la siguiente línea del fichero,
sin registrar los datos de este jugador.
Aún no hemos tratado las excepciones que teníamos pendientes, solo las «nuestras», así
que este código sigue sin compilar, todavía es imposible probarlo.
Validaciones 345
Clase RecordsManager
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
// Atención, esta versión del código no compila
public class RecordsManager {
private static Logger logger = [Link]([Link]);
private static final String INPUT_FILE = "[Link]";
private static final String OUTPUT_FILE = "[Link]";
private static final int MIN_NAME_LENGTH = 6;
private static final int MIN_SCORE = 1000;
private static Random r = new Random();
public static void main(String[] args) {
Map<String, Integer> players = new HashMap<>();
/// (1) Leer fichero de entrada
Scanner s = new Scanner(new File(INPUT_FILE));
while ([Link]()) {
String line = [Link]();
String[] data = [Link](" ");
String name = data[0];
int score = [Link](data[1]);
/// (7) Validamos el nombre
try {
validateName(name);
} catch (PlayerNameTooShortException pntse) {
[Link]([Link]());
name = generateNewName(name);
[Link]("El nuevo nombre de usuario es " + name);
}
/// (8) Validamos la puntuación
// los tratamos por separado para poder validar la puntuación
// después de haber arreglado el nombre
try {
validateScore(name, score);
} catch (ScoreTooLowException stle) {
[Link]([Link]());
continue; // nos saltamos este jugador, vamos a la siguiente línea
}
[Link](name, score);
[Link]("Línea tratada correctamente: " + name + " - " + score);
}
346 Proyecto «Gestión de récords»
/// (2) Mostrar por consola todos los datos guardados
[Link]("Datos procesados: ");
for (String name : [Link]()) {
[Link](name + ":\t" + [Link](name));
}
/// (3) Pedir confirmación al usuario
[Link]("¿Son correctos? [S]i/[N]o");
Scanner sConsole = new Scanner([Link]);
String answer = [Link]();
boolean confirmed = [Link]("S");
/// (4) Escribir a fichero
if (confirmed) {
[Link]("Procedemos al volcado de datos del fichero...");
FileOutputStream fos = new FileOutputStream(OUTPUT_FILE);
for (String name : [Link]()) {
[Link]((name + " " + [Link](name) + "\n").getBytes());
}
}
}
/// (5) Validamos la longitud del nombre
private static void validateName(String name)
throws PlayerNameTooShortException {
if ([Link]() < MIN_NAME_LENGTH) {
throw new PlayerNameTooShortException(name);
}
}
/// (6) Validamos la puntuación mínima
private static void validateScore(String name, int score)
throws ScoreTooLowException {
if (score < MIN_SCORE) {
throw new ScoreTooLowException(name, score);
}
}
private static String generateNewName(String name) {
int randomSize = MIN_NAME_LENGTH - [Link]();
for (int i = 0; i < randomSize; i++) {
int randomNum = [Link](10);
name += randomNum; // añadimos un número más al final
}
return name;
}
}
Validaciones 347
Clase PlayerNameTooShortException
package [Link];
public class PlayerNameTooShortException extends Exception {
private String name;
public PlayerNameTooShortException(String name) {
super(name);
[Link] = name;
}
public String getName() {
return name;
}
@Override
public String getMessage() {
return "El nombre del jugador (" + name + ") es demasiado corto.";
}
}
Clase ScoreTooLowException
package [Link];
public class ScoreTooLowException extends Exception {
private String name;
private int score;
public ScoreTooLowException(String name, int score) {
[Link] = name;
[Link] = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
@Override
public String getMessage() {
return "El usuario " + name + " tiene menos puntos ("
+ score + ") de los requeridos.";
}
}
348 Proyecto «Gestión de récords»
Control de errores
Hasta ahora no nos hemos preocupado de los errores que se podían producir en el código
que habíamos escrito. Vamos a ello, a ver si logramos que esto compile y lo hacemos
funcionar.
9. En el scanner del fichero de entrada, necesitamos cerrarlo y controlar posibles errores,
como el FileNotFoundException. Usamos un try con recursos para asegurarnos de
cerrarlo siempre (usaríamos un finally si estuviéramos con una versión de Java anterior
a la 7) y un catch por si no encontramos el fichero, capturando la FileNotFoundException.
10. Lo mismo haremos para el scanner de la entrada estándar (cuando le preguntamos
al usuario si están bien los datos). De nuevo usamos un try con recursos para garantizar
el cierre, pero esta vez no se requiere añadir bloque catch, pues no hay nada que
capturar. Como la respuesta la vamos a necesitar fuera del try, hemos de declarar el
String antes del try, inicializar su valor en el try, y así ya podemos usarla luego.
11. Tenemos la misma situación para la escritura en el fichero de salida, así que de nuevo
utilizaremos el try con recursos y esta vez capturaremos cualquier problema de
entrada/salida (IOException) e informaremos al usuario.
Ya está listo para probarlo.
Fíjate en dónde están ubicados los ficheros de entrada salida: en la raíz del proyecto. Si
no estás seguro de dónde debes ponerlos en tu entorno, haz una prueba generando el
fichero de salida y mira dónde te lo deja.
Clase RecordsManager
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class RecordsManager {
private static Logger logger = [Link]([Link]);
Control de errores 349
private static final String INPUT_FILE = "[Link]";
private static final String OUTPUT_FILE = "[Link]";
private static final int MIN_NAME_LENGTH = 6;
private static final int MIN_SCORE = 1000;
private static Random r = new Random();
public static void main(String[] args) {
/// (9) Cerramos el scanner del fichero de entrada
// usando el try con recursos nos aseguramos de que el scanner se cierra
try (Scanner sInput = new Scanner(new File(INPUT_FILE))) {
Map<String, Integer> players = new HashMap<>();
/// (1) Leer fichero de entrada
while ([Link]()) {
String line = [Link]();
String[] data = [Link](" ");
try {
String name = data[0];
int score = [Link](data[1]);
/// (7) Tratamos los problemas de validación del jugador
try {
validateName(name);
} catch (PlayerNameTooShortException pntse) {
[Link]([Link]());
name = generateNewName(name);
[Link](" El nuevo nombre de usuario es " + name);
}
/// (8) Validamos la puntuación
try {
validateScore(name, score);
} catch (ScoreTooLowException stle) {
[Link]([Link]());
[Link](" Jugador descartado");
continue;
}
[Link](name, score);
[Link]("Línea tratada correctamente: " + name + " - " + score);
} catch (IndexOutOfBoundsException | NumberFormatException e) {
[Link]("La línea no contiene los datos esperados ("
+ line + ")");
// y seguimos con el while
}
}
/// (2) Mostrar por consola todos los datos guardados
[Link]("Datos procesados: ");
for (String name : [Link]()) {
[Link](name + ":\t" + [Link](name));
}
/// (3) Pedir confirmación al usuario
[Link]("¿Son correctos? [S]i/[N]o");
/// (10) Cerramos el escáner de consola
String answer;
try (Scanner sConsole = new Scanner([Link])) {
answer = [Link]();
}
boolean confirmed = [Link]("S");
350 Proyecto «Gestión de récords»
/// (4) Escribir a fichero
if (confirmed) {
[Link]("Procedemos al volcado de datos del fichero...");
/// (11) Cerramos el fichero de salida y controlamos IOE
try (FileOutputStream fos = new FileOutputStream(OUTPUT_FILE)) {
for (String name : [Link]()) {
[Link]((name + " " + [Link](name) + "\n").getBytes());
}
} catch (IOException ioe) {
[Link]("No hemos podido escribir los resultados en el " +
"fichero porque algo ha fallado: " + [Link]());
}
}
} catch (FileNotFoundException fnfe) {
[Link]("No podemos ejecutar el programa porque no se encuentra "
+ "el fichero de entrada esperado: " + INPUT_FILE);
}
}
private static void validateName(String name)
throws PlayerNameTooShortException {
/// (5) Validamos la longitud del nombre
if ([Link]() < MIN_NAME_LENGTH) {
throw new PlayerNameTooShortException(name);
}
}
private static void validateScore(String name, int score)
throws ScoreTooLowException {
/// (6) Validamos la puntuación mínima
if (score < MIN_SCORE) {
throw new ScoreTooLowException(name, score);
}
}
private static String generateNewName(String name) {
int randomSize = MIN_NAME_LENGTH - [Link]();
for (int i = 0; i < randomSize; i ++) {
int randomNum = [Link](10);
name += randomNum;
}
return name;
}
}
[Link]
Margarita 12345
Pedro 2134
Juanjo 165
An 123456
Li 12
Alex 987
Control de errores 351
Salida por consola
2021-03-24 21:12:01.838 [main] RecordsManager [WARN ] - El nombre del jugador
(Pedro) es demasiado corto.
El nuevo nombre de usuario es Pedro9
2021-03-24 21:12:01.841 [main] RecordsManager [ERROR] - El usuario Juanjo tiene
menos puntos (165) de los requeridos.
Jugador descartado
2021-03-24 21:12:01.841 [main] RecordsManager [WARN ] - El nombre del jugador (An)
es demasiado corto.
El nuevo nombre de usuario es An2555
2021-03-24 21:12:01.841 [main] RecordsManager [WARN ] - El nombre del jugador (Li)
es demasiado corto.
El nuevo nombre de usuario es Li8726
2021-03-24 21:12:01.841 [main] RecordsManager [ERROR] - El usuario Li8726 tiene
menos puntos (12) de los requeridos.
Jugador descartado
2021-03-24 21:12:01.842 [main] RecordsManager [WARN ] - El nombre del jugador
(Alex) es demasiado corto.
El nuevo nombre de usuario es Alex60
2021-03-24 21:12:01.842 [main] RecordsManager [ERROR] - El usuario Alex60 tiene
menos puntos (987) de los requeridos.
Jugador descartado
Datos procesados:
An2555: 123456
Margarita: 12345
Pedro9: 2134
¿Son correctos? [S]i/[N]o
S
Procedemos al volcado de datos del fichero...
Vemos que el programa ha ido informándonos de todo lo que ha pasado. Los datos de
Margarita los ha dejado tal cual, sin corregir nada. A Pedro le ha metido un número de
más para llegar a los seis caracteres del nombre. Juanjo ha sido descartado por tener
menos de mil puntos. An ha recibido un montón de números porque tenía el nombre
muy corto.
Y el pobre Li ha sido descartado después de rebautizarlo. Alex tampoco ha conseguido
mantenerse en la lista de récords.
Fichero de salida ([Link])
An2555 123456
Margarita 12345
Pedro9 2134
352 Proyecto «Gestión de récords»
Test unitarios
Hemos hecho una prueba a mano, pero a estas alturas del libro ya sabrás que eso no es
suficiente. Realicemos algunos test unitarios.
Recuerda que, para testar los métodos privados, debemos convertirlos a protegidos (protected).
Tenemos tres métodos para probar. Los dos primeros no devuelven nada (void), así que
es imposible comprobar el resultado, pero sí podemos verificar si lanzan o no la excepción
esperada. Si esperamos que salte la excepción, ponemos un fail después de la llamada al
método y los asserts en el catch.
Probaremos con nombres largos, de la longitud límite, más cortos y con las puntuaciones
límite 1000 y 999.
Para el tercer método, el que completa los nombres cortos, que sí devuelve un resultado,
comprobaremos que el resultado obtenido cumple los requisitos esperados: que tiene cierta
longitud, que se ha completado con números, incluso probamos con un nombre vacío.
Clase RecordsManagerTest
package [Link];
import static [Link];
import static [Link];
import static [Link];
import static [Link];
import [Link];
class RecordsManagerTest {
private static final String ALEX = "Alex";
private static final String ALEJANDRO = "Alejandro";
@Test
void validateNameLargoTest() {
try {
[Link](ALEJANDRO);
assertTrue(true);
} catch (PlayerNameTooShortException e) {
fail([Link]());
}
}
@Test
void validateNameCortoTest() {
try {
[Link](ALEX);
fail("Debería haber fallado, " + ALEX + " es corto");
} catch (PlayerNameTooShortException e) {
assertTrue(true);
assertTrue([Link]().contains(ALEX));
}
}
@Test
void validateNameCasiTest() {
String nombre = "Alejo";
try {
Test unitarios 353
[Link](nombre);
fail("Debería haber fallado, " + nombre + " es corto");
} catch (PlayerNameTooShortException e) {
assertTrue(true);
assertTrue([Link]().contains(nombre));
}
}
@Test
void validateNameExactoTest() {
String nombre = "Alejor";
try {
[Link](nombre);
assertTrue(true);
} catch (PlayerNameTooShortException e) {
fail([Link]());
}
}
@Test
void validateScore1000Test() {
try {
[Link](ALEJANDRO, 1000);
assertTrue(true);
} catch (ScoreTooLowException e) {
fail([Link]());
}
}
@Test
void validateScore999Test() {
try {
[Link](ALEJANDRO, 999);
fail("Debería haber fallado, 999 son pocos puntos");
} catch (ScoreTooLowException e) {
assertTrue([Link]().contains(ALEJANDRO));
assertTrue([Link]().contains("999"));
}
}
@Test
void generateNewNameLargoTest() {
String nuevoNombre = [Link](ALEJANDRO);
assertEquals(ALEJANDRO, nuevoNombre);
}
@Test
void generateNewNameCortoTest() {
String nuevoNombre = [Link](ALEX);
assertNotEquals(ALEX, nuevoNombre);
assertTrue([Link](ALEX));
assertTrue([Link](ALEX));
String numero = [Link]([Link]());
assertTrue([Link](numero) > 0);
assertEquals(6, [Link]());
}
@Test
void generateNewNameVacioTest() {
String nuevoNombre = [Link]("");
assertNotEquals("", nuevoNombre);
assertTrue([Link](nuevoNombre) > 0);
assertEquals(6, [Link]());
}
}
354 Proyecto «Gestión de récords»
Mejoras y evoluciones
Hemos hecho ya un buen trabajo, lo vamos a dejar en este punto. Pero si quieres, aquí
tienes unas cuantas posibles evoluciones por si quieres mejorar aún más tu programa:
A. Toma el primer argumento del main como nombre del fichero de entrada y, si no te
lo pasan o te lo pasan mal, usas "[Link]".
B. Toma el segundo argumento del main como el tamaño mínimo del nombre de los
jugadores y, si no te lo pasan o te lo pasan mal, usas el valor 6 por defecto.
C. Toma el tercer argumento del main como la puntuación mínima de los argumentos
que se le pasan al main y, si no te la pasan o te lo pasan mal, usas el valor 1000 por
defecto.
D. Crea una clase Jugador para almacenar los datos de cada línea del fichero y úsala
en tu programa.
E. Ordena los resultados de la salida en orden decreciente de la puntuación (el que más
puntos tenga, primero).
F. Lleva un contador de por qué línea del fichero de entrada vas y muéstralo al sacar
los mensajes de error.
G. Controla que no haya nombres de jugadores repetidos.
H. Crea métodos auxiliares para aligerar la cantidad de código que tiene el main.
I. Haz pruebas más exhaustivas para comprobar que todos los posibles errores están
tratados y que nuestro usuario jamás verá trazas por la consola (pero ¡no te comas
las excepciones, ni te olvides de ellas, ni generalices!).
Mejoras y evoluciones 355
4
Parte
Parte
Datos en Java
15 Estructuras de datos
En este capítulo aprenderás a:
• Operar con cadenas de texto e internacionalizarlas.
• Manejar números y fechas y formatearlos.
• Utilizar distintos tipos de colecciones.
• Identificar y trabajar con las funciones hash, así como su uso en
las estructuras de datos.
Introducción
Además de conocer las estructuras básicas de un lenguaje, cómo utilizar la orientación
a objetos y algunas buenas prácticas, también es importante identificar y trabajar
con las estructuras de datos que nos proporcionan. La alternativa es reinventar la
rueda programando esas estructuras, pero eso es un sinsentido. Mejor nos centramos
en las particularidades de nuestro proyecto y reutilizamos las librerías existentes.
En este capítulo aprenderemos estructuras de datos del API de Java relacionadas
con cuatro ámbitos: cadenas de texto, números, fechas y colecciones. Existen muchas
más, pero, para empezar, con estas será suficiente.
Cadenas de texto
Las cadenas de texto, en inglés strings, son estructuras de datos para almacenar
textos. Probablemente sea una de las estructuras más utilizadas en cualquier lenguaje.
Son una secuencia de caracteres. En Java se implementan con la clase String y, por
tanto, sus instancias son objetos.
Aunque otros lenguajes no, Java distingue entre comillas dobles (") y comillas simples
o apóstrofo ('). Las comillas dobles se utilizan para representar strings, mientras que
las simples son para los caracteres (ver ejemplos en la tabla 15.1).
Tabla 15.1. Las comillas en Java: Strings y caracteres.
Ejemplo Interpretación Ejemplo Interpretación
"a" Cadena de texto con una letra a 'a' Carácter con la letra a.
(longitud uno).
"" Cadena de texto vacía (longitud cero). '' No válido, no se puede tener un
«carácter vacío».
" " Cadena de texto con un espacio ' ' Carácter con un espacio.
(longitud uno).
"texto" Cadena de texto con el valor texto 'texto' No válido, no se puede tener más de
(longitud cinco). un carácter.
"\n" Cadena de texto con un salto de línea '\n' Carácter especial salto de línea.
(longitud uno).
NOTA:
Puedes consultar más información sobre los caracteres especiales en el capítulo 4.
360 Estructuras de datos
Clase String
La mejor referencia para conocer una clase es la propia documentación del API de
Java, aun así, veamos algunos elementos de la clase String. A partir de la versión 1.4
de Java, se incluyó una interfaz para CharSequence de la que hereda String, para
permitir el manejo de diferentes tipos de secuencias de caracteres.
ADVERTENCIA:
String es una clase inmutable. Esto significa que una vez se crea un objeto de tipo String,
este no se puede modificar. Todos los métodos que parece que modifican un String, en
realidad devuelven un nuevo objeto con el resultado correspondiente.
Teniendo en cuenta que estos métodos se aplican sobre una cadena de texto en
concreto, veamos algunos de los más interesantes:
• length(): devuelve la longitud.
• isEmpty(): comprueba si la longitud es cero. Es mejor elección [Link]() que
[Link]() == 0.
• indexOf(String buscado): devuelve la posición de la primera ocurrencia de
buscado.
• substring(int inicio): devuelve la subcadena desde inicio (incluido) hasta el final.
• substring(int inicio, int fin): devuelve la subcadena desde inicio (incluido) hasta
fin (excluido).
• lastIndexOf(String buscado): devuelve la posición de la última ocurrencia de
buscado.
• toUpperCase(): devuelve el mismo texto pero en mayúsculas.
• toLowerCase(): devuelve el mismo texto pero en minúsculas.
• trim(): devuelve el mismo texto, pero quitando los delimitadores (espacios en
blanco, tabuladores, saltos de línea…) del principio y el final de la cadena,
manteniendo los interiores.
• charAt(int indice): devuelve el carácter en la posición solicitada.
• equals(Object otroObjeto): compara el otro objeto con nuestro String. Devolverá
cierto si el objeto es un String y ambos representan la misma secuencia de
caracteres.
• equalsIgnoreCase(String otroString): compara el otro String con el nuestro,
sin distinguir mayúsculas y minúsculas.
• replace(char letraAntes, char letraDespues): reemplaza todas las ocurrencias
de letraAntes por letraDespues.
Cadenas de texto 361
• split(String expresionRegular): devuelve un array de Strings con los fragmentos
resultantes de trocear nuestro String cortando por los fragmentos que casen con
la expresionRegular. Por ejemplo: "hola-qué tal-adiós".split("-") devolverá
["hola", "qué tal", "adiós"].
NOTA:
Recuerda que los índices, en Java, empiezan en 0.
Esta clase también cuenta con algunos métodos estáticos interesantes. Al ser estáticos,
no se aplican sobre un objeto ya instanciado:
• format(Locale l, String formato, Object… args), format(String formato,
Object… args): devuelve una cadena formateada, reemplazando los comodines
en formato por los valores de args formateados. Si incluimos el argumento locale
(configuración de idioma y país o región), tendrá en cuenta esa información en
el formateo, considerando el orden de los elementos en las fechas, el uso de
puntos y comas en las cifras… Por ejemplo: [Link]("%s -> %f", "PI", Math.
PI) devolverá "PI -> 3,141593".
• join(CharSequence delimitador, CharSequence… elementos): devuelve una
cadena compuesta por los elementos unidos por el delimitador. Por ejemplo:
[Link]("<", "1", "2", "3") devolverá "1<2<3".
T15.1
• valueOf(x): devuelve una representación en String del valor recibido, que puede
ser de varios tipos.
E15.1
NOTA:
E15.2 Los puntos suspensivos en la declaración del último argumento significa que el método
acepta cualquier número de elementos de ese tipo.
Clase Character
No es muy habitual trabajar directamente con caracteres, pero a veces es necesario.
La clase Character envuelve el tipo básico char. Nos proporciona métodos que pueden
resultar muy interesantes, entre otros:
• isAlphabetic
• isDigit
• isIdeographic
• isLetter
• isLetterOrDigit
• isLowerCase
362 Estructuras de datos
• isUpperCase
• …
El nombre de cada método nos da pistas de qué hace cada uno. Ya hemos comentado
que el inglés es necesario para el desarrollo.
Clases StringBuffer y StringBuilder
Los Strings son objetos inmutables, eso quiere decir que no se modifican. Si paso a
mayúsculas, realmente tendré un nuevo String con un contenido como el del otro
pero en mayúsculas, y no se modificará el original. Este mecanismo tiene algunas
ventajas, como que sea seguro para entornos multihebra (que no veremos en este
curso), pero es muy poco eficiente a la hora de construir Strings a base de concatenar
fragmentos.
Desde el JDK 1.0, tenemos disponible la clase StringBuffer que el propio API de Java
dice que «es como un String, pero puede ser modificado». Esta implementación
también es segura para multihebras, así que en Java 1.5, añadieron la clase
StringBuilder. Esta ya no nos ofrece garantías de sincronización, sin embargo, es
más rápida que StringBuffer, así que será nuestra clase de elección para la construcción
de Strings cuando no estemos trabajando en entornos multihebra.
Su uso más habitual sería algo así:
StringBuilder sb = new StringBuilder("¡Hola!");
[Link](" ").append(nombre);
[Link](" ").append(apellido);
[Link]([Link]());
El método append añade el texto recibido al final del que ya tiene y devuelve un
StringBuilder, así que podemos ir concatenando tantas llamadas como necesitemos.
Para obtener el String ya construido, se requiere llamar a toString.
Esto resulta en:
¡Hola! Pepe García
Internacionalización y localización
La internacionalización es el proceso de preparación de un programa para soportar
distintos idiomas, formatos, matices culturales… Es una parte básica de una
aplicación multiidioma. La localización permite tener en cuenta las diferencias locales
en el formateo de algunos datos, como los separadores en las cifras o el orden de
representación de los datos en las fechas (día, mes, año en algunos países; mes, día,
año en otros).
Internacionalización y localización 363
NOTA:
La internacionalización y la localización frecuentemente se abrevian como i18n y l10n
(se pone la primera y la última letra y rodeando al número de letras que hay entre ellas).
El paquete [Link] nos proporciona clases e interfaces para el manejo de textos,
fechas, números y mensajes de forma independiente de los lenguajes naturales. Es
decir, nos permite escribir los programas sin depender de un idioma en concreto,
pudiendo relegar a ficheros independientes los textos de cada uno de los idiomas.
Clase MessageFormat
Con la clase MessageFormat, del paquete [Link], se concatenan mensajes de
manera neutral en cuanto al idioma, para construir mensajes que se mostrarán al
usuario final. Esta clase no implementa comportamientos específicos para cada locale.
El comportamiento adaptado a cada localización se lo daremos en el patrón. Tiene
mucho potencial, formateando números, fechas, singulares y plurales… pero lo
vemos en un ejemplo simple:
Clase Mensajes
import [Link];
import [Link];
import [Link];
public class Mensajes {
public static void main(String[] args) {
ResourceBundle bundle = [Link]("estructuras", [Link]);
// ResourceBundle bundle = [Link]("estructuras");
String mensaje = [Link]("mensaje");
String objeto = [Link]("objeto");
String color = [Link]("color");
[Link]([Link](mensaje, objeto, color));
}
}
En este ejemplo:
• Declaramos un ResourceBundle para leer el fichero de propiedades llamado
"estructuras" en nuestro ejemplo. Podemos decirle qué Locale queremos emplear (en
el ejemplo [Link]) o no indicar nada (línea comentada en el ejemplo).
• Recogemos el patrón del mensaje, un objeto (coche o lápiz aleatorio) y un color (azul
o rojo, también aleatorio).
• Formateamos el mensaje con MessageFormat.
364 Estructuras de datos
Propiedades en español: estructuras_es.properties
mensaje = Es un {0} {1}.
color = rojo
objeto = coche
Propiedades en inglés: estructuras_en.properties
mensaje = This is a {1} {0}.
color = red
objeto = car
Resultado
Si ejecutamos el código indicando que estamos en el Reino Unido, el resultado será:
This is a red car.
Mientras que, si no indicamos locale, cogerá por defecto el de nuestro equipo, así que en
mi caso el resultado será:
Es un coche rojo.
En el código no hacemos diferencia entre un idioma u otro, es en los ficheros de
propiedades en los que no solo ponemos las palabras traducidas, sino también el orden
adecuado para que el mensaje sea gramaticalmente correcto en cada idioma. Utilizamos
un número entre llaves, desde cero, para indicar en qué posición pondremos cada dato:
• {0} se corresponde con la posición en la que queremos poner el objeto.
• {1} se corresponde con la posición en la que queremos poner el color.
• En el fichero _es.properties ponemos {0} {1} para conseguir "coche rojo".
• Pero en la versión en inglés, _en.properties, debemos girarlo a {1} {0} para conseguir
"red car".
Si preparamos ficheros de propiedades en múltiples idiomas, por ejemplo, francés, alemán
y ruso, cuando el programa se ejecute en equipos configurados en esos idiomas cogerá
directamente el fichero adecuado.
NOTA:
Los ficheros de propiedades deben tener todos el mismo nombre, pero ser prefijados con el idioma
y/o el país: estructuras_zh_cn.properties para chino de China y estructuras_zh_tw.properties
para chino de Taiwán, o estructuras_fr.properties para francés (sin distinguir entre Francia y
Canadá). Todas las versiones del fichero deben contener la misma lista de propiedades, cambiamos
los valores, no las etiquetas.
T15.2
TRUCO:
Es buena idea tener siempre un fichero global que será utilizado para los casos que no hemos E15.5
contemplado: [Link], normalmente, en inglés.
Internacionalización y localización 365
Números
Aunque con los textos es posible hacer muchas más virguerías, pasemos ya a ver los
números. Solo quiero ponerte la miel en los labios para que tengas ideas sobre qué seguir
investigando.
Java cuenta con una clase abstracta Number (en el paquete [Link], ese que no hay que
importar) de la que heredan el resto de representaciones de valores numéricos. Esta clase
nos dice que cualquier clase que quiera ser considerada un número debe ser capaz de
devolver su valor en cada uno de los tipos básicos, aunque puede que se pierda algo de
precisión a causa del truncado o el redondeo.
Figura 15.1. Métodos de la clase abstracta Number.
En la figura 15.1 consulta el resumen de métodos de Number. Es una imagen sacada
de la documentación oficial del API de Java, una muy buena fuente de referencia
para aprender más detalles sobre las clases de las librerías de Java.
NOTA:
Algunos de estos métodos ya están implementados, byteValue y shortValue, pero es
solamente porque llaman a intValue y luego hacen un casting (o conversión) al tipo
primitivo que necesitan.
Autoboxing y unboxing
Hemos visto los tipos primitivos en la tabla 2.1 del capítulo 2. Por cada tipo primitivo,
Java nos proporciona una clase que envuelve a ese tipo primitivo y que, normalmente,
tiene el mismo nombre que el tipo primitivo, pero con la inicial en mayúscula, salvo
para char, que es Character. Cuando oigas hablar o leas sobre wrapping classes se
refiere a estas.
366 Estructuras de datos
Cuando empecé con Java, por allá por la versión 1.2, era muy capaz de distinguir
entre tipos primitivos y sus clases envolventes, porque si requería convertir de uno
a otro, debía hacer yo misma la conversión. A partir de la versión 1.5 añadieron el
autoboxing, que es muy cómodo y limpio a la hora de escribir el código, pero muy
poco didáctico a la hora de asimilar estos conceptos.
Antes, sí tenía que escribirse así:
List<Integer> lista = new ArrayList();
for (int i = 0; i < 5; i ++) {
[Link]([Link](i));
}
Porque las listas son de objetos y no podemos añadir tipos primitivos, así que había
que convertir el int en un Integer. Pero gracias al autoboxing, hoy escribimos
directamente:
List<Integer> lista = new ArrayList();
for (int i = 0; i < 5; i ++) {
[Link](i);
}
Ahora es el compilador quien hace automáticamente esa conversión.
Por otro lado, si queremos hacer operaciones con esos números, han de ser tipos primitivos
y no objetos. Antes:
int res = 0;
for (Integer i : lista) {
res += [Link]();
}
Pero con unboxing, que es exactamente la operación inversa:
int res = 0;
for (Integer i : lista) {
res += i;
}
De nuevo es el compilador (en lugar de nosotros) quien hace la conversión, dejando el
código es mucho más cómodo de escribir, pero ya no somos conscientes de cuándo T15.3
estamos empleando el tipo primitivo o el objeto.
Grandes números y alta precisión
Además de las clases envolventes de los tipos básicos, también hallamos Number:
BigDecimal y BigInteger. Utilizaremos la primera para realizar operaciones
aritméticas de alta precisión o cuando necesitemos un especial control sobre la escala
o el redondeo, mientras que BigInteger será una buena elección para enteros muy
muy grandes, más grandes que long. Se usa frecuentemente en aplicaciones de
seguridad y criptografía.
Números 367
Una de las diferencias entre usar los tipos primitivos o estas dos clases es que con
los big numbers, en vez de utilizar los operadores, usaremos los métodos que nos
proporcionan estas clases para hacer los cálculos.
Clase Números
import [Link];
public class Numeros {
public static void main(String[] args) {
BigInteger numGrande = new BigInteger("12345678901234567890123456789");
[Link]("Número grande: " + numGrande);
BigInteger masUno = [Link]([Link]);
[Link]("Más uno: " + masUno);
BigInteger alCuadrado = [Link](numGrande);
[Link]("Al cuadrado: " + alCuadrado);
}
}
Resultado
Número grande: 12345678901234567890123456789
Más uno: 12345678901234567890123456790
Al cuadrado: 152415787532388367504953515625361987875019051998750190521
T15.4
Intenta hacer esto con tipos básicos… no creo que lo consigas.
Clase NumberFormat
Al igual que la clase MessageFormat, el paquete [Link] también nos ofrece la clase
NumberFormat y su hija DecimalFormat que, como supongo que ya imaginarás, nos
ayudan a formatear números. Son clases muy potentes y flexibles, así que solo mencionaré
un par de detalles.
Para usarlo, necesitamos conseguir una instancia, con o sin locale, para formatear enteros,
moneda…
Clase Monedas
Veamos en un ejemplo simple cómo se formatearía un precio en dos países distintos:
import [Link];
import [Link];
public class Monedas {
public static void main(String[] args) {
double precio = 1234.56;
NumberFormat nf = [Link]([Link]);
[Link]("Precio en Francia: " + [Link](precio));
NumberFormat nf2 = [Link]([Link]);
[Link]("Precio en EE. UU.: " + [Link](precio));
}
}
368 Estructuras de datos
Resultado
Precio en Francia: 1234,56 €
Precio en EE. UU.: $1,234.56
Mismo número, dos formatos de moneda distintos, cambia la divisa, los separadores …
según la costumbre de cada país.
Fechas
¡Ay, las fechas! La guerra que pueden dar. Nunca ha sido fácil el manejo de fechas
en la programación. ¿Recuerdas o has oído hablar del «Efecto 2000»? En vísperas a
la llegada del año 2000 se temía que muchos sistemas informáticos pudieran dejar
de funcionar, porque en los años 80 muchos programadores optaron por utilizar
solo dos dígitos para almacenar el año al tratar con fechas, cada byte ahorrado en
memoria o en disco era una gran ganancia, y el año 2000 quedaba muy lejos. En esas
condiciones, pasar del 99 al 00 era todo un drama. Al final no pasó nada, pero fue
porque a finales de los 90 se trabajó para adaptar esas aplicaciones (o reemplazarlas
por otras).
En Java tenemos muchas maneras de tratar las fechas y el tiempo. La más básica es
[Link]() que nos devuelve un long con el número de milisegundos
transcurridos desde la fecha de base, que se considera la medianoche del 1 de enero
de 1970 UTC (hora universal coordinada), es decir, de Londres.
Si queremos medir cuánto tiempo ha pasado entre dos instantes, calcularemos la diferencia
entre los valores devueltos en las distintas llamadas a [Link]().
Pero si el usuario nos tiene que facilitar una fecha, no le pediremos que nos dé ese
valor, sino el día, el mes y el año. ¡Necesitamos manejar fechas, no milisegundos!
Fechas a la antigua
En Java 1.0, en el siglo pasado, la clase [Link] era capaz de interpretar fechas
si le pasábamos año, mes, día, hora, minuto y segundo, pero desde el JDK 1.1 ese
funcionamiento se marcó como deprecated, ahora funciona con milisegundos. Es
decir, new Date() representará una fecha con el instante de ahora y new Date(long
date) la fecha del instante dado. Para manejar fechas con año, mes, día, hora, minuto
y segundo hay que recurrir a un calendario, es decir, una clase hija de Calendar,
siendo la más famosa GregorianCalendar. A partir de ahí, ya recuperamos los
milisegundos para construir un objeto Date.
Fechas 369
Para mostrar de forma legible esas fechas, podemos utilizar DateFormat, en especial,
SimpleDateFormat. Es una clase que nos permite convertir de String a fecha (parsear)
o de fecha a String (formatear). Para crear un objeto SimpleDateFormat tenemos que
proporcionarle el patrón de fecha que deseemos y opcionalmente un locale. Para
construir el patrón, usamos letras para definir cada elemento. Por ejemplo, EEE nos
da el día de la semana abreviado, pero EEEE completo, para un lunes, y en español,
nos daría lun o lunes. Consulta la documentación de Java para conocer con detalle
el significado de cada letra y de sus repeticiones.
NOTA:
Un elemento deprecated es aquel que, aunque aún podemos utilizar, no se recomienda su
uso, ha quedado obsoleto y es posible que en futuras versiones desaparezca. Si quieres
marcar algún elemento de tu código como obsoleto, usa la anotación @Deprecated y explica
la alternativa que recomiendas.
ADVERTENCIA:
Cuando quieras usar la clase Date de la que estamos hablando, asegúrate de importar
[Link] y no [Link].
Clase FechasViejas
En este ejemplo vemos un uso simple de las clases mencionadas:
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class FechasViejas {
public static void main(String[] args) {
long ahora = [Link]();
[Link]("Ahora: " + ahora);
Date fecha = new Date();
[Link]("Fecha: " + fecha);
Calendar cal = [Link]();
[Link](2020, 0, 1, 2, 3, 4); // los meses también empiezan en cero
Date fechaCal = [Link]();
[Link]("Calendar: " + fechaCal);
DateFormat df = new SimpleDateFormat("EEEE, d MMMM yyyy HH:mm:ss Z",
[Link]);
[Link]("Formateo: " + [Link](fechaCal));
}
}
370 Estructuras de datos
Resultado
Ahora: 1617128743877
Fecha: Tue Mar 30 20:25:43 CEST 2021 T15.5
Calendar: Wed Jan 01 02:03:04 CET 2020
Formateo: Mittwoch, 1 Januar 2020 02:03:04 +0100
Si no se lo indicamos, nos saca las fechas en inglés, pero poco le cuesta dárnoslas en T15.6
alemán, si se lo pedimos bien.
Fechas a partir de Java 8 (paquete [Link])
Hemos avanzado mucho desde el manejo de fechas en formato texto con dos letras
para el año, pasando por los milisegundos y llegando a los calendarios, pero aun
así no siempre satisface las necesidades de los programas.
No sé si te has fijado, pero la fecha que hemos impreso en alemán lleva zona horaria.
Un mismo instante, en China supone una fecha distinta a la de España, que a su vez
es distinta a la de México. Habrá ocasiones en las que precisemos conocer el instante,
por ejemplo, para una aplicación de agenda, gestora de reuniones, en las que queremos
que los tres participantes se conecten a la vez, aunque para uno sea medianoche, para
otro por la tarde y para otro por la mañana. Sin embargo, si estamos hablando de
una fecha de nacimiento, la cosa cambia. Si tu cumpleaños es el 1 de julio, no queremos
que el sistema registre el instante, porque ese instante puede ser interpretado como
1 de julio en Europa, pero en América aún será 30 de junio. Y lograr eso con la clase
Date es complicado. Hace unos años se usaba la librería jodatime, que se convirtió
en estándar de facto, pero no dejaba de ser una librería externa.
Afortunadamente, con Java 8 llegó el paquete [Link], inspirado en la mencionada
librería, pero ya integrado en Java.
En este paquete encontramos clases para representar los distintos conceptos que
manejamos sobre el tiempo: instantes, duraciones, periodos, fechas con o sin hora,
con o sin zona horaria… De esta forma, podemos escoger el tipo adecuado para la
noción de tiempo que representamos. Y hacer las cuentas que requiera nuestro
programa: identificar el último viernes del mes, el próximo lunes, la fecha de hace
tres meses o de aquí a dos semanas…
Para el parseo y formateo, contamos con el paquete [Link], con un
funcionamiento por patrones parecido al de SimpleDateFormat.
Fechas 371
Clase FechasNuevas
Son muchas clases con muchos métodos, te dejo este ejemplo a modo de muestrario:
• Recuperamos el instante actual.
• Recuperamos la fecha actual sin hora y sin zona horaria y le sumamos un mes.
• Recuperamos la hora actual sin fecha (y sin zona horaria, claro).
• Recuperamos la fecha actual con hora.
• Recuperamos la fecha actual con hora y con zona horaria.
• Creamos una duración de 1 día.
• Calculamos el periodo entre dos fechas.
Imprimimos cada uno de los datos, sin preocuparnos del formateo, pero con el potencial
de poder hacerlo.
import [Link].*;
public class FechasNuevas {
public static void main(String[] args) {
Instant instant = [Link]();
[Link]("Instant: " + instant);
LocalDate localDate = [Link]().plusMonths(1);
[Link]("LocalDate +1m: " + localDate);
LocalTime localTime = [Link]();
[Link]("LocalTime: " + localTime);
LocalDateTime localDateTime = [Link]();
[Link]("LocalDateTime: " + localDateTime);
ZonedDateTime zonedDateTime = [Link]();
[Link]("ZonedDateTime: " + zonedDateTime);
Duration duration = [Link](1);
[Link]("Duration: " + duration);
LocalDate inicio = [Link](2019, [Link], 23);
LocalDate fin = [Link](2023, [Link], 18);
Period period = [Link](inicio, fin);
[Link]("Period: " + period);
}
}
Resultado
Instant: 2021-03-31T18:08:25.291Z
LocalDate +1m: 2021-04-30
LocalTime: 20:08:25.379
LocalDateTime: 2021-03-31T20:08:25.380
ZonedDateTime: 2021-03-31T20:08:25.380+02:00[Europe/Madrid]
Duration: PT24H
Period: P3Y8M26D
¿Qué nos dicen estos resultados? Presta atención a los detalles:
372 Estructuras de datos
• La hora del instante son las 18, pero la de la hora local son las 20… Porque mi reloj
dice que son las 20 h, pero no estoy en el meridiano de Greenwich.
• Que sumarle un mes al 31 de marzo nos da el 30 de abril, así que supongo que, si
sumamos un mes al 31 de enero, tendremos el 28 de febrero.
• La fecha con zona horaria sí nos indica las 20 h, pero también nos dice que la zona
horaria es de + 2 h, pues estoy en Madrid. ¡Ahora ya sabes cuándo y dónde ejecuté
este ejemplo!
• Una duración de un día la imprime como 24 horas, pero sí, es correcto. E15.3
• Parece que el periodo que calculamos es de tres años, ocho meses y 26 días.
Colecciones
Ahora que ya tenemos controlados los datos individuales que podemos necesitar, estudiemos
las colecciones, es decir, estructuras para manejar cierta cantidad de objetos del mismo tipo.
El API de colecciones de Java nos permite manipularlas independientemente de los detalles
de su implementación. En otras palabras, en la medida de lo posible, pues hay diferencias
insalvables, manejaremos igual una lista que un conjunto o un diccionario.
Figura 15.2. Algunas interfaces y clases del API de colecciones.
Colecciones 373
Los métodos que deben cumplir todas las colecciones son:
• add(E e): añade elementos a la colección.
• addAll(Collection c): agrega todos los elementos de una colección a la colección.
• clear(): borra todos los elementos de la colección.
• contains(Object o): comprueba si la colección contiene el objeto recibido.
• containsAll(Collection c): corrobora si todos los elementos de la colección
recibida como parámetro están contenidos en la colección.
• isEmpty(): devuelve cierto si la colección no tiene elementos.
• iterator(): devuelve un iterador para poder recorrer la colección.
• remove(Object o): elimina el objeto de la colección, si existe, claro.
• removeAll(Collection c): elimina todos los elementos de la colección recibida
de la colección.
• retainAll(Collection c): conserva en la colección solo los elementos de la colección
recibida como parámetro.
• size(): devuelve el número de elementos de la colección.
• toArray(): devuelve un array con los elementos de la colección.
Ahora aprenderemos las diferencias entre una lista y un conjunto, pero en cualquier
caso, sea una cosa o la otra, podremos añadir, contar, borrar, buscar elementos.
Tipos genéricos
Al mostrar la firma del método add, entre paréntesis, como argumento he puesto
E e. En Java 1.5 se incluyeron los tipos genéricos, muy útiles para las colecciones.
Mediante los tipos genéricos, podemos definir cómo va a ser una colección sin
importarnos qué tipo de objetos va a almacenar. A la hora de declarar un objeto de
una clase que use genéricos, sea una clase del API de Java o una implementada por
nosotros, indicaremos de qué tipo serán esos elementos.
Las declaraciones de List y Map empiezan por:
public interface List<E>
public interface Map<K,V>
Cuando vamos a utilizarlas, hacemos:
List<Integer> numeros = new ArrayList();
Map<Character, String> abc = new HashMap();
De forma que las E de List serán Integer, las K de Map, Character y sus V, String,
T15.7 resultando numeros una lista de números enteros y abc un diccionario con clave
una letra y valor un texto.
374 Estructuras de datos
Listas
¿Qué es una lista en el mundo real, aparte de una chica que sabe mucho? Existen la
lista de canales de la tele, la lista de alumnos de un grupo de clase, la lista de espera
para una operación, la lista de la compra… ¿Qué cosas comunes tienen estas listas?
Todas ellas disponen de muchos elementos distintos, pero del mismo tipo. No hay
una lista en la que tengamos canales, niños y pacientes. Es ilógico. Otra característica
típica de las listas es la ordenación. Es importante en qué posición está cada elemento.
Necesitamos saber qué cadena de televisión tenemos en el canal uno de la tele y cuál
en el dos, para cambiar rápidamente de canal usando el mando. Las listas de alumnos
suelen estar ordenadas por apellido, y al menos en primaria, los niños suelen saber
cuál es su número de lista. En mi cole, ese orden se usaba, por ejemplo, para los
exámenes de educación física, ¡imagínate qué lío para el profe si no siguieran los
alumnos ese orden durante las pruebas! En una lista de espera para una operación,
anda si no es importante si estás en la posición uno o en la mil.
Sin embargo, en la lista de la compra, ¿hay alguna diferencia si cojo antes los huevos
o las manzanas? Parece que, en este caso, aunque coloquialmente llamemos «lista
de la compra» a la lista de la compra, para un programador eso no es una lista, ya
descubriremos otros tipos de datos para las listas de la compra.
¿Puede haber huecos en una lista? Dependerá de la lista, claro, pero habrá casos en
los que sí, como cuando alguien le guarda el sitio a otro en una cola, ¿no? Eso
informáticamente se traduciría en que la lista acepte elementos nulos.
¿Puede haber elementos repetidos en una lista? También dependerá mucho del caso.
No creo que un alumno salga dos veces en la lista de una clase, podría ser que un
pobre paciente esté dos veces en la lista de espera, porque tiene que operarse la rodilla
derecha y el pie izquierdo, pero si pensamos en la lista de visitantes de una biblioteca,
sí puede haber usuarios que vayan varias veces en semana a coger un libro.
Tenemos ya clara la idea de qué es y cómo es una lista. En la vida real, el primero
de la lista ocupa la posición uno, en Java empiezan por cero.
En resumen, una lista es una secuencia ordenada de elementos del mismo tipo, sobre
la que podemos insertar, borrar, consultar o buscar elementos.
Colecciones 375
Clase ArrayList
Java tiene una interfaz List que extiende de la interfaz Collection. List dispone de
unas cuantas clases hijas: LinkedList, Stack, Vector… y ArrayList, que es la reina de
las listas: su nombre viene de la forma en la que está implementada, utilizando un
array redimensionable. Permite todos los elementos, incluido null. Esto quiere decir
que podemos meter nulos en un objeto ArrayList. Puede que lo necesitemos o no,
pero es importante saber si se puede.
También tiene métodos para manipular el tamaño del array y se parece mucho a la
implementación Vector, pero no es sincronizada. Este dato es importante si lo vamos
a usar en un entorno concurrente, es decir, en el que varios usuarios o programas
podrían estar manipulando los datos. Si fuera nuestro caso, descartaríamos ya
ArrayList e iríamos a ver la documentación de Vector.
Si miramos el nivel de complejidad de sus métodos size, isEmpty, get, set, así como
los métodos que nos darán iteradores para recorrer la lista, apreciamos una
complejidad constante: no dependerá del número de elementos de la lista. Añadir
elementos costará tanto como elementos queramos añadir. Sin embargo, el resto de
operaciones sí sería de complejidad lineal.
La capacidad de la lista siempre será al menos tan grande como el tamaño de la lista
(que es el número real de objetos que alberga). Si prevemos necesitar más espacio,
mejor aumentarlo antes de añadir demasiados elementos, de forma que no vaya
incrementándose poco a poco muchas veces.
Yo creo que, de momento, cuando necesites una lista, has de utilizar ArrayList, y
según vayas avanzando en la programación Java, investiga y aprende otras
implementaciones más adecuadas en cada momento.
En el proyecto macetohuerto utilizamos los elementos de los siguientes apartados.
Conjuntos
¿Recuerdas las clases de matemáticas hablando de conjuntos en la escuela primaria?
¿Uniones, intersecciones y todo eso? Bueno, eso necesitarás recuperarlo cuando
trabajes bases de datos y sus queries, pero con los conjuntos de los que hablaremos
ahora no trabajaremos su aritmética.
Los conjuntos (sets, en inglés) como estructura de datos son bastante parecidos a las
listas, pero con un par de diferencias básicas:
• Los elementos no tienen orden.
• Y, por tanto, no puede repetirse ninguno.
376 Estructuras de datos
Pensemos en qué conjuntos hay en la vida diaria: la lista de la compra (aunque la
llamemos lista, poco importa el orden, así que poco «lista» es), la lista de invitados
a tu fiesta de cumpleaños (a no ser que tengas muchísimos amigos y poco espacio
en el local de la fiesta o algún otro tipo de limitación y, por tanto, debas descartar a
gente que te gustaría que fuera, no creo que el orden de tus invitados sea importante).
Quizá también deberíamos decir el conjunto de invitados, en vez de la lista de
invitados.
En programación, muchas veces tendemos a usar listas porque estamos más
familiarizados con el conjunto no porque realmente necesitemos que sea una lista
en vez de un conjunto.
En resumen, un conjunto es una colección sin orden de elementos del mismo tipo,
sobre la que podemos insertar, borrar, consultar o buscar elementos.
Clase HashSet
Al igual que en el caso de las listas, Java nos ofrece varias implementaciones, lo
mismo sucede con los conjuntos, siendo la más utilizada HashSet, que está respaldada
por una tabla hash, concretamente una instancia de HashMap. No ofrece ninguna
garantía sobre el orden de iteración del conjunto, ni que este vaya a ser el mismo
con el paso del tiempo, es decir, cada vez que queramos recorrer el conjunto. Esta
implementación permite elementos nulos.
Sobre la complejidad de las operaciones, serán de complejidad constante las
operaciones básicas: añadir, quitar, contiene y tamaño, asumiendo que la función
hash funcione bien y disperse bien los elementos en el espacio disponible. Iterar un
conjunto requiere un tiempo proporcional a la suma del número de elementos y la
capacidad del mapa, por eso no conviene inicializar la estructura con un tamaño
demasiado grande si nos importa el rendimiento de la iteración.
Funciones hash
También llamadas funciones resumen o de compresión, las funciones hash son
algoritmos que toman un dato y generan un valor de un tamaño determinado.
A un mismo dato de entrada, tendremos un mismo dato de salida. Al más ligero
cambio en el dato de entrada, grandes diferencias en el dato de salida. Son, por
consiguiente, funciones de un solo sentido.
Colecciones 377
Tabla 15.2. Ejemplo de hashing.
Entrada Salida (CRC32)
pato 95301eac
pata 728833ab
pala f093abf2
palacio 4e5700de
abracadabra 728833ab
Vemos en la tabla 15.2 las grandes diferencias entre los hashs de palabras muy
parecidas. Puede que varios datos de entrada generen un mismo valor de salida.
Eso es muy complicado de conseguir, así que en este ejemplo me lo invento. Cuando
esto sucede, se llama colisión. Las colisiones tienen sus pegas, pero también sus
ventajas: si cuentas con el valor de salida es imposible conocer cuál fue el dato de
entrada, por eso me he tenido que inventar la colisión entre pata y abracadabra.
En la implementación de estructuras de datos esto es irrelevante, pero es que el
origen de las funciones hash está en la criptografía.
Algunos de los otros usos que se les dan a las funciones hash son:
• La gestión de identificadores y contraseñas: en vez de almacenar los datos
originales, se almacenan los resultados del hash que son los que realmente se
usan para autenticar al usuario. Así los valores reales no quedan expuestos a
ataques.
• Si nos envían un fichero y su checksum, obtenido aplicando una función hash, es
posible comprobar la integridad de dicho fichero volviendo a aplicar esa función
hash y comparando el resultado con el checksum recibido.
• Ese mismo checksum también podemos usarlo para identificar los ficheros, por
ejemplo, en sistemas de almacenamiento en la nube.
• También las usan los antivirus para determinar la firma de los virus y así
detectarlos y distinguirlos.
• En criptografía, se emplea para la firma digital, no para el cifrado, pues no se
puede deshacer.
Es fundamental que el rendimiento de la función hash escogida sea bueno (pues será
usada de forma intensiva) y determinará el rendimiento de las estructuras de datos
que la usen. Por eso, en las estructuras de datos no se suelen utilizar las funciones
T15.8 hash típicas de la criptografía, sino implementaciones adaptadas a la longitud
necesaria según el tamaño de la estructura en cuestión.
378 Estructuras de datos
Diccionarios
Si te hablo de diccionarios, ¿en qué piensas? Supongo que en ese libro tan gordo en
el que aparecen las definiciones de las palabras de un idioma o los sinónimos de
esas palabras o su traducción a otro idioma. Hay una clave, que es la palabra en sí,
y un valor que, según el tipo de diccionario, será la definición o la traducción o lo
que sea. En informática, también contamos con diccionarios y hemos de verlos como
eso: como unas tablas en las que tenemos una clave y un valor para cada clave. Son
estructuras muy útiles para organizar la información.
En Java, los diccionarios se implementan con Map, que no es hija de Collection, pero
tiene un enfoque bastante coherente.
Cuando definamos un diccionario, especificaremos de qué tipo de datos serán las
claves y los valores. Buscaremos que las claves sean tipos más o menos simples,
pero en el tipo de los valores podemos utilizar tipos más complejos, si lo necesitamos,
claro.
Recuerdo un proyecto en el que como valores del diccionario teníamos una lista de
diccionarios o algo así. Tuve que pintarlo en mi cuaderno para aclararme, pero
necesitábamos esa información.
Tablas hash
Las tablas hash son una estructura de datos que asocia claves con valores. Pero ¿cómo
lo hace?
Se implementa con un array de tamaño fijo, es decir, una lista estática, junto a listas
enlazadas cuando se requiera. Una lista enlazada es una lista construida mediante
enlaces entre sus elementos, siendo mucho más flexible que un array.
La clave con el valor es simplemente una tupla. Y serán esas tuplas las que
necesitamos colocar en una tabla. El criterio de colocación será el resultado de aplicar
una función hash sobre la clave. Mejor lo ilustramos con un ejemplo, para que
entiendas cómo lo hace Java internamente, pero no tenemos que implementarlo
nosotros, Java se encarga:
• Diccionario con clave una letra y valor un nombre de mujer corto.
• Función hash: la posición de la letra en el alfabeto.
• Datos a insertar: ver tabla 15.3.
• Tamaño inicial de la tabla: 4.
• Ampliación de la tabla: 8.
Colecciones 379
Tabla 15.3. Datos a insertar.
Clave Valor Hash Hash % 4 Hash % 8
e Eva 5 1 5
f Fina 6 2 6
a Ana 1 1 1
b Bea 2 2 2
i Isa 9 1 1
Tomamos el primer valor, e, Eva, con hash 5, que módulo 4 (pues la tabla tiene
tamaño 4) da 1, así que colocamos a Eva en la posición 1. Seguidamente, colocamos
a Fina en la posición 2. Le toca a Ana, en la posición 1, pero la posición 1 ya está
ocupada por Eva. Y esto no es como en el parchís, que si caes en una casilla ocupada
por otra ficha te comes la otra ficha y vuelves a empezar… Hay que encontrar la
forma de meter a ambas en la misma casilla y esa forma es mediante una lista
enlazada. Bea colisiona con Fina, así que será su siguiente. Finalmente, Isa, con la
i, caería también en la casilla 2, y la meteríamos como siguiente a Ana. Vemos el
resultado en la figura 15.3.
Figura 15.3. Distribución en tabla de tamaño 4.
A la hora de buscar elementos en una de estas tablas, el proceso sería algo así: Si
buscamos Isa, clave i, calculamos el hash de i, 1, así que buscamos en la primera
posición. Encontramos a Eva, que tiene como siguiente a Ana, que tiene como
siguiente a Isa. En tres pasos la hemos localizado.
380 Estructuras de datos
La verdad es que nos ha costado demasiados pasos hallar la respuesta. Y eso ha sido
porque la tabla tiene demasiados elementos para su tamaño o es demasiado pequeña
para tantos elementos. Normalmente controlamos estos parámetros mediante el
factor de carga de la tabla.
Si la carga de la tabla no es elevada, la búsqueda será muy rápida. Si está sobrecargada,
será más lenta. Digamos que la complejidad será mayor que constante, pero menor
que lineal, es decir, complejidad logarítmica.
Cuando una tabla está demasiado llena, una buena implementación de una tabla
hash lo que hace es crecer la tabla. Por lo general, se duplica su tamaño y, en ese
momento, hay que recolocar todos los elementos, tras recalcular sus hash, ahora
utilizando el módulo con el nuevo tamaño. Ya calculamos ese dato en la tabla 15.3.
Recolocamos los cinco elementos en la tabla de 8 posiciones y obtenemos el resultado
de la figura 15.4.
Figura 15.4. Redistribución en tabla de tamaño 8.
Clase HashMap
La implementación de Map que más utilizo yo es HashMap. Ahora que ya hemos
visto cómo funcionan las tablas hash, supongo que ya imaginas cómo está
implementada. Para descubrirlo, te recomiendo una actividad muy interesante:
intenta recrear el ejemplo que hemos visto, u otro que se te ocurra, en Java, y depúralo
paso a paso. Mejorarás tu soltura depurando y descubrirás la implementación interna E15.5
de la estructura.
Colecciones 381
Test y ejercicios
Test 15.1. ¿Qué valores deben tomar x e y para que esta sentencia devuelva
«diente»?
"independientemente".substring(x, y)
a) x = 8, y = 13
b) x = 8, y = 12
c) x = 7, y = 13
d) x = 7, y = 12
Test 15.2. ¿Cómo debe llamarse el fichero [Link] con la versión
francesa?
a) estructuras_fr.properties
b) fr_estructuras.properties
c) [Link] (o como se diga «estructuras» en francés)
d) [Link]
Test 15.3. ¿En qué versiones de Java sería correcto este fragmento de código?
int num = 8;
List<Integer> lista = new ArrayList();
[Link](num);
a) En todas
b) A partir de la 1.5
c) A partir de la 1.8
d) En ninguna
Test 15.4. ¿Qué valores deben tomar x e y para obtener el valor 12,346?
[Link](12.3456789).setScale(x, y)
a) x = 3, y = RoundingMode.HALF_UP
b) x = 3, y = 5
c) x = 5, y = RoundingMode.HALF_DOWN
d) x = 3, y = [Link]
Test 15.5. ¿Qué patrón es el correcto para representar una fecha en este
formato «Son las 9 horas y 23 minutos»?
a) hh y mm
b) "'Son las' h 'horas y' mm 'minutos'"
c) "'Son las' hh 'horas y' mm 'minutos'"
d) "Son las h horas y mm minutos"
Test 15.6. ¿Qué patrón es el correcto para representar una fecha en este
formato «20210301_102342»?
a) "yyymmdd_hhmmss"
b) "yyyMMdd_hhMMss"
c) "yyymmdd_hhMMss"
d) "yyyMMdd_hhmmss"
382 Estructuras de datos
Test 15.7. Dada la siguiente declaración, ¿de qué tipo serán los valores de este
diccionario?
Map<List<BigDecimal>, Set<String>> map = new HashMap();
a) BigDecimal
b) String
c) List<BigDecimal>
d) Set<String>
Test 15.8. En una tabla hash, si el hash de dos claves distintas, a y b, da el
mismo valor, por ejemplo 3, ¿qué sucede?
a) a irá en la posición 3 y b en la 4
b) a irá en la posición 3 y b será descartada
c) a y b irán en la posición 3
d) a irá en la posición 3 y b será su siguiente
Ejercicio 15.1. Contando comas
Escribe un programa que cuente el número de comas en el texto «Con diez cañones por banda,
viento en popa a toda vela, no corta el mar, sino vuela un velero bergantín».
Ejercicio 15.2. Separando textos
Escribe un programa que separe por comas el texto «Con diez cañones por banda, viento en popa
a toda vela, no corta el mar, sino vuela un velero bergantín», sin incluir espacios ni al principio ni
al final, y sin incluir la coma.
Ejercicio 15.3. Calcula tu edad
Calcula tu edad exacta a día de hoy.
Ejercicio 15.4. Estadísticas
Escribe un programa que genere mil sílabas de dos letras (legibles o no) y luego contabilice cuántas
empiezan por cada letra.
Ejercicio 15.5. Estadísticas chinas
Internacionaliza el programa del ejercicio 15.4 de forma que funcione por defecto en inglés, pero
tenga versiones distintas para China y para Taiwán.
TRUCO:
Puedes utilizar un traductor online para conseguir el texto en chino simplificado para China
o en chino tradicional para Taiwán, pero también puedes inventarte un texto cualquiera.
Test y ejercicios 383
Soluciones
Test 15.1. ¿Qué valores deben tomar x e y para que esta sentencia devuelva
«diente»?
"independientemente".substring(x, y)
c) x = 7, y = 13: La d de diente está en la posición 8, que para Java es la 7. La e final de
diente está entonces en la posición 12 para Java, pero en substring debemos indicar la
primera posición que no queremos incluir. Si te lías, lo mejor es probarlo y ver qué pasa.
Test 15.2. ¿Cómo debe llamarse el fichero [Link] con la versión
francesa?
a) estructuras_fr.properties
Test 15.3. ¿En qué versiones de Java sería correcto este fragmento de código?
int num = 8;
List<Integer> lista = new ArrayList();
[Link](num);
b) A partir de la 1.5
Test 15.4. ¿Qué valores deben tomar x e y para obtener el valor 12,346?
[Link](12.3456789).setScale(x, y)
a) x = 3, y = RoundingMode.HALF_UP: x debe indicar el número de posiciones
decimales, no total, y el redondeo que necesitamos para obtener un 6 final es hacia arriba.
Test 15.5. ¿Qué patrón es el correcto para representar una fecha en este
formato «Son las 9 horas y 23 minutos»?
b) "'Son las' h 'horas y' mm 'minutos'": Necesitamos poner entre comillas
simples los textos del patrón para que no intente interpretarlo como letras a reemplazar.
Solo ponemos una h, porque queremos sacar las 9 y no las 09-.
Test 15.6. ¿Qué patrón es el correcto para representar una fecha en este
formato «20210301_102342»?
d) "yyyMMdd_hhmmss": Minutos y meses empiezan ambas por m. Alguien decidió que
para los meses se usaría la M mayúscula y para los minutos la m minúscula.
Test 15.7. Dada la siguiente declaración, ¿de qué tipo serán los valores de este
diccionario?
Map<List<BigDecimal>, Set<String>> map = new HashMap();
d) Set<String>: List<Decimal> es el tipo de las claves, Set<String> el tipo de los valores.
Tendríamos un diccionario con clases una lista de grandes decimales, y valores un conjunto
de textos. Esa clave no parece muy realista, pero es lo que dice esa declaración.
Test 15.8. En una tabla hash, si el hash de dos claves distintas, a y b, da el
mismo valor, por ejemplo 3, ¿qué sucede?
d) a irá en la posición 3 y b será su siguiente: recuerda que las tablas hash utilizan una
combinación de lista estática con listas dinámicas para las colisiones.
384 Estructuras de datos
Ejercicio 15.1. Contando comas
• Creamos una constante para el texto y otra para la coma (tipo primitivo char).
• Creamos el método main para llamar al método auxiliar que hará el trabajo e imprimimos el
resultado.
• Creamos un método privado cuentaComas que:
• Inicializa el contador a cero.
• Recupera la posición de la primera aparición de una coma en el texto.
• Si la encuentra (pos != -1), incrementa el contador y busca la siguiente, a partir de la
posición siguiente.
public class Ejercicio15_01 {
private static final char COMA = ',';
public static final String TEXTO = "Con diez cañones por banda, "
+ "viento en popa a toda vela, no corta el mar, sino vuela "
+ "un velero bergantín";
public static void main(String[] args) {
int res = cuentaComas(TEXTO);
[Link]("El texto tiene " + res + " comas.");
}
private static int cuentaComas(String texto) {
int res = 0;
int pos = [Link](COMA);
while (pos != -1) {
res ++;
pos = [Link](COMA, pos+1);
}
return res;
}
}
Ejercicio 15.2. Separando textos
Partimos de una estructura muy similar a la del ejercicio anterior, pero la constante para la coma
esta vez será un String.
En el método separa:
• Creamos un StringTokenizer sobre el texto recibido, utilizando como separador la coma.
• Creamos un array de String del tamaño necesario para albergar todos los fragmentos que
obtendremos.
• Mientras haya más fragmentos, los añadimos al array, no sin antes limpiar espacios al principio
y al final (trim()).
import [Link];
public class Ejercicio15_02 {
private static final String COMA = ",";
public static final String TEXTO = "Con diez cañones por banda, "
+ "viento en popa a toda vela, no corta el mar, sino vuela "
+ "un velero bergantín";
public static void main(String[] args) {
String[] res = separa(TEXTO);
for (String string : res) {
[Link](string);
}
}
Soluciones 385
private static String[] separa(String texto) {
StringTokenizer st = new StringTokenizer(texto, COMA);
String[] res = new String[[Link]()];
int i = 0;
while ([Link]()) {
res[i++] = [Link]().trim();
}
return res;
}
}
También podríamos haber utilizado una List<String> y, en ese caso, inicializarla con un tamaño
concreto habría sido opcional.
Ejercicio 15.3. Calcula tu edad
• Creamos un objeto LocalDate con nuestra fecha de nacimiento.
• Y otro con la fecha de hoy.
• Calculamos el periodo entre una fecha y la otra.
• Y lo imprimimos recuperando años, meses y días.
import [Link];
import [Link];
import [Link];
public class Ejercicio15_03 {
public static void main(String[] args) {
LocalDate miNacimiento = [Link](1980, [Link], 1);
LocalDate now = [Link]();
Period edad = [Link](miNacimiento, now);
[Link]("Tengo " + [Link]() + " años, "
+ [Link]() + " meses y "
+ [Link]() + " días.");
}
}
NOTA:
Aunque intuitivamente podría parecer que necesitamos una duración y no un periodo, la clase
Duration de [Link] tiene en cuenta los segundos. Para calcular «duraciones» entre fechas
sin hora, hay que usar Period.
Ejercicio 15.4. Estadísticas
Este ejercicio lo haremos por partes:
1. Generación de las sílabas aleatorias.
• Generamos las constantes:
• Un objeto Random (del paquete [Link]).
• El número de letras del abecedario.
• El carácter con la primera letra.
• Creamos un método para generar una letra:
• Le sumamos a la primera letra, que en esta operación Java la considerará un entero,
un número aleatorio entre 0 y el número de letras menos uno, y convertimos el
resultado mediante un casting al tipo básico char.
• Convertimos el resultado a String y lo devolvemos.
386 Estructuras de datos
• Creamos un método para generar una sílaba de dos letras, que simplemente concatenará
el resultado de llamar dos veces a generaLetra.
• Llamamos a generaSilaba desde el main y pintamos el resultado para comprobar que
vamos bien, por ejemplo, op.
import [Link];
public class Ejercicio15_04 {
private static final Random RND = new Random();
private static final int NUM_LETRAS = 26;
private static final char PRIMERA_LETRA = 'a';
public static void main(String[] args) {
[Link](generaSilaba());
}
private static String generaSilaba() {
return generaLetra() + generaLetra();
}
private static String generaLetra() {
char c = (char) (PRIMERA_LETRA + [Link](NUM_LETRAS));
return [Link](c);
}
}
2. Generación de la lista de sílabas.
• Creamos una lista del tamaño indicado. Como es una lista bastante grande, de mil
elementos, así evitamos que Java tenga que ir haciéndola crecer desde su capacidad inicial
por defecto (diez) hasta los mil poco a poco.
• Iteramos mil veces para añadir a la lista una sílaba generada.
• Desde el main llamamos a generaLista en vez de a generaSilaba.
import [Link];
import [Link];
import [Link];
public class Ejercicio15_04 {
private static final Random RND = new Random();
private static final int NUM_LETRAS = 26;
private static final char PRIMERA_LETRA = 'a';
private static final int TAM = 1000;
public static void main(String[] args) {
List<String> lista = generaLista();
[Link](lista);
}
private static List<String> generaLista() {
List<String> lista = new ArrayList<>(TAM);
for (int i = 0; i < TAM; i++) {
[Link](generaSilaba());
}
return lista;
}
…
}
Soluciones 387
Probando con diez elementos, obtendremos un resultado parecido a este:
[fz, ay, hn, ya, dd, eu, lg, rp, qv, ht]
3. Contamos el número de sílabas que empiezan por cada letra.
Este tipo de operaciones se suelen resolver con un diccionario, en el que tendremos como clave
la letra y como valor el contador de apariciones, así pues:
• Creamos un método cuentaLetras que recibe la lista y devuelve un diccionario.
• Dentro del método, creamos un diccionario vacío, con clave Character y valor Integer.
Usamos las clases envolventes porque las colecciones no admiten tipos primitivos.
• Por cada una de las sílabas de la lista:
• Obtenemos el primer carácter (llamando a un método auxiliar que recupera el carácter
en la primera posición del String).
• Recuperamos el contador actual en el diccionario para ese carácter.
• Si el contador aún no existe, lo inicializamos a cero.
• Añadimos al mapa el carácter como clave y el contador preincrementado en uno
como valor.
• Devolvemos el mapa, que pintaremos en el main.
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class Ejercicio15_04 {
private static final Random RND = new Random();
private static final int NUM_LETRAS = 26;
private static final char PRIMERA_LETRA = 'a';
private static final int TAM = 1000;
public static void main(String[] args) {
List<String> lista = generaLista();
Map<Character, Integer> mapa = cuentaLetras(lista);
[Link](mapa);
}
private static Map<Character, Integer> cuentaLetras(
List<String> lista) {
Map<Character, Integer> mapa = new HashMap<>();
for (String silaba : lista) {
Character c = getPrimero(silaba);
Integer cont = [Link](c);
if (cont == null) {
cont = 0;
}
[Link](c, ++cont);
}
return mapa;
}
private static Character getPrimero(String silaba) {
return [Link](0);
}
…
}
Probando con diez elementos, obtendremos un resultado parecido a este:
{a=1, g=1, w=1, x=1, h=3, l=1, n=2}
388 Estructuras de datos
4. Formateamos la salida.
El método toString de las colecciones hace un trabajo decente y nos deja entrever el contenido del
diccionario, pero lo formatearemos un poco mejor, utilizando el método pintaMapa que:
• Crea un StringBuilder vacío
• Por cada una de las entradas en el diccionario
• Formatea el mensaje
• Lo concatena al String que estamos construyendo
• Para pintarlo por la salida estándar.
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class Ejercicio15_04 {
private static final Random RND = new Random();
private static final int NUM_LETRAS = 26;
private static final char PRIMERA_LETRA = 'a';
private static final int TAM = 1000;
public static void main(String[] args) {
List<String> lista = generaLista();
Map<Character, Integer> mapa = cuentaLetras(lista);
pintaMapa(mapa);
}
private static void pintaMapa(Map<Character, Integer> mapa) {
StringBuilder sb = new StringBuilder();
for (Entry<Character, Integer> entry : [Link]()) {
String m = [Link]("{0} ha salido {1} veces\n",
[Link](), [Link]());
[Link](m);
}
[Link]([Link]());
}
…
}
Probando con diez elementos, obtendremos un resultado parecido a este:
p ha salido 2 veces
q ha salido 1 veces
a ha salido 1 veces
b ha salido 2 veces
e ha salido 1 veces
k ha salido 1 veces
n ha salido 2 veces
Fíjate en que no nos devuelve un orden concreto, pues los diccionarios, los mapas, no son listas
ordenadas. Puedes intentar ordenarlo por clave o por valor, ¡a ver si lo consigues! Quizá necesites
consultar la documentación de Java o el capítulo 17.
Soluciones 389
Ejercicio 15.5. Estadísticas chinas
En el ejercicio 15.4 estamos sacando los resultados en español, pero ahora nos piden
internacionalizarlos, con una versión por defecto en inglés y dos versiones específicas para China
y Taiwán.
Para internacionalizar este código, solo se verá afectado el método pintaMapa:
• Cambiamos el mensaje escrito «a pelo» por un mensaje recuperado de un recurso.
• Creamos un fichero de propiedades con el mensaje en inglés.
private static void pintaMapa(Map<Character, Integer> mapa) {
ResourceBundle bundle = [Link]("ejercicio15_05");
String mensaje = [Link]("mensaje");
StringBuilder sb = new StringBuilder();
for (Entry<Character, Integer> entry : [Link]()) {
String m = [Link](mensaje,
[Link](), [Link]());
[Link](m);
}
[Link]([Link]());
}
Fichero ejercicio15_05.properties:
mensaje = {0} came out {1} times\n
Probando con diez elementos, obtendremos un resultado parecido a este:
q came out 2 times
d came out 1 times
t came out 1 times
w came out 1 times
x came out 1 times
j came out 3 times
m came out 1 times
Ya tenemos la versión internacional. Vamos a por la china, lo que sería crear un fichero
ejercicio15_05_zh.properties, pues zh es el código para el idioma chino (como dice la documentación
de la clase Locale, para el idioma se usa el estándar ISO 639). Pero precisamos de dos versiones
para un mismo idioma, aunque para distintos países. Esto se consigue añadiendo un segundo
sufijo al nombre del fichero, para indicar el país: CN para China y TW para Taiwán (esta vez
siguiendo el estándar ISO 3166).
Para forzar el uso de una u otra versión sin tocar la configuración de nuestro equipo es suficiente
con añadir una de estas dos líneas en el main:
[Link]([Link]);
[Link]([Link]);
(e importar la clase Locale, claro).
ADVERTENCIA:
Como nuestros sistemas no están muy preparados para el manejo de caracteres chinos (ni
simplificados ni tradicionales), al copiar y pegar el resultado del traductor online en Eclipse
veremos la codificación en UTF-8 de los caracteres utilizados. Esto también nos puede suceder
cuando escribimos en español o francés, pues el encoding puede dar mucha guerra con los
caracteres acentuados.
390 Estructuras de datos
Fichero ejercicio15_05_zh_CN.properties:
mensaje = {0}\u51FA\u6765\u4E86{1}\u6B21\n
Probando con diez elementos, obtendremos un resultado parecido a este:
q出来了1次
r出来了2次
e出来了4次
y出来了1次
m出来了2次
Fichero ejercicio15_05_zh_TW.properties:
mensaje = {0}\u51FA\u4F86\u4E86{1}\u6B21\n
Probando con diez elementos, obtendremos un resultado parecido a este:
b出來了1次
r出來了3次
d出來了1次
e出來了1次
g出來了2次
i出來了1次
l出來了1次
En este texto tan corto la diferencia es mínima, está solo en el segundo carácter chino, en grande en la
figura 15.5. Sean diferencias mínimas o brutales, debemos respetar las peticiones de nuestros clientes.
Figura 15.5. Diferencias en un carácter entre el chino tradicional (a la izquierda)
y el simplificado (a la derecha).
Soluciones 391
16 Bases de datos:
Mapeo Objeto-Relacional
En este capítulo aprenderás a:
• Configurar el acceso a base de datos desde Java.
• Mapear entidades Java en tablas de base de datos.
• Escoger el buen tipo de relación entre entidades o tablas.
• Realizar consultas desde Java, utilizando Hibernate.
Introducción
A estas alturas del libro ya sabes escribir métodos, clases, algoritmos, ejecutar
programas, probarlos… Y eso está muy bien, pero en el mundo real los programas
informáticos manejan datos. Si tenemos una aplicación de citas, necesitaremos
disponer de los datos personales de las personas apuntadas, sus preferencias, sus
fotos, su historial… Si hacemos una aplicación para el seguimiento de la salud,
necesitaremos registrar las constantes, los síntomas, las medicaciones, y luego usarlos
para sacar gráficos o calcular cosas. Incluso si estamos haciendo un juego, precisamos
controlar los rankings de los jugadores, los premios disponibles, sus requisitos y
quién los ha conseguido.
Da igual qué programa estemos haciendo. Las probabilidades de que requiramos
manejar datos son elevadísimas, como también lo son que estos datos estén en una
base de datos.
La informática «desde siempre» ha necesitado manejar datos, pero no fue hasta los
años sesenta del siglo pasado que aparecieron los primeros sistemas de bases de
datos, en los que había que ir recorriendo los registros uno a uno, sin posibilidades
de buscar datos concretos (como mucho, algunos sistemas, conseguían guardarlos
ordenados).
Ya en la década de los setenta, aparecen los modelos relacionales. Un trabajador de
IBM tuvo la idea de usar tablas, de forma que los datos se dividían en series de
tablas (o relaciones), enlazados entre sí con claves. Pero no fue hasta finales de esa
década que se estandarizó el uso de SQL (Structured Query Language, es decir, lenguaje
de consulta estructurada). Ya había por ese entonces varios sistemas gestores de
bases de datos, como Postgres u Oracle, que aún se usan hoy en día (con alguna que
otra evolución, claro).
Con el surgir de la programación orientada a objetos en los ochenta, se intentaron
algunos sistemas de bases de datos también orientados a objetos, pero nunca llegaron
a triunfar. Ya entrados en el siglo XXI aparecieron los sistemas noSQL, como por
ejemplo MongoDB. Aunque estos sistemas se usan hoy en día en multitud de
aplicaciones, los sistemas relacionales y SQL siguen siendo los reyes del manejo de
datos. Pero, como hemos comentado, son sistemas anteriores a la programación
orientada a objetos, y la verdad es que su interacción directa no es sencilla. A finales
de los noventa se usaron las Enterprise Java Beans (EJB), pero con la llegada del
nuevo siglo irrumpió Hibernate para desbancarlas. Se trata de una herramienta que
intenta hacer casar el modelo de datos que se usa durante la ejecución del programa
(que está en memoria y cuando programamos en Java es orientado a objetos) con la
base de datos (por lo general almacenada en el disco y con un modelo relacional).
La idea gustó, y en 2006 se lanzaron las especificaciones de la interfaz de programación
(API) de persistencia de Java, conocida como JPA. Se llama «persistencia» a la capa
394 Bases de datos: Mapeo Objeto-Relacional
de las aplicaciones que se encarga de gestionar los datos que se guardan, que persisten
incluso cuando el programa no está funcionando, es decir, las bases de datos. Tras
esa estandarización, Hibernate se adaptó a ella.
En este capítulo crearemos las clases necesarias para realizar el Mapeo Objeto-
Relacional (ORM) entre Java y la base de datos. Utilizaremos como ejemplo un gestor
de pedidos y facturas.
Herramientas necesarias
Para seguir los ejemplos y ejercicios de este capítulo, y también para aplicar los
conocimientos adquiridos en tus propios proyectos, necesitarás instalarte y manejar
unas cuantas herramientas.
Algunas herramientas de las que menciono deben ser exactamente esas, pero la
mayoría ofrecen varias opciones para escoger, pues hay herramientas equivalentes.
Yo propongo y usaré una, tú escoge la más conveniente según tus circunstancias.
Con las versiones pasa lo mismo, puede que cuando leas esto ya haya versiones
nuevas. En ese caso, convendría que comprobaras si hay diferencias importantes
con las que propongo yo, aunque probablemente te sirvan a la perfección.
Necesitamos un servidor de bases de datos. Te puede servir (casi) cualquiera, pero,
claro, en función de esta elección, cambiarán el resto de herramientas, así que te
aconsejaría imitarme ahora. Luego para tus proyectos, ya buscarás la mejor opción.
Yo he escogido mySQL, porque es muy extendida y para juguetear como vamos a
hacer nosotros es gratuita. Voy a usar la versión 8.0.19. Al instalar, la opción de
desarrollador te vale. La hallarás en la sección de descargas de [Link]. Es
solo el motor de la base de datos, el servidor, así que tras instalarlo no verás ningún
programa para acceder a la base de datos ni nada (eso sería un cliente). En la
documentación encontrarás cómo levantarlo, pararlo… Toma nota de la contraseña
que le pongas al usuario root porque luego vas a necesitarla. Es posible crear más
usuarios, pero con el root nos apañamos.
ADVERTENCIA:
En efecto, en un entorno de producción jamás usaremos el usuario root para esto, pero ahora
solo estamos jugando.
En alguna documentación encontrarás que hay que descargarse el conector. Es una
opción, pero prefiero usar maven para gestionar las dependencias, así que no
requerimos descargar expresamente el driver o conector de mySQL (o la base de
datos que decidas usar).
Lo mismo sucede con Hibernate, yo voy a escoger la versión 5.4, última versión
estable. Asimismo, usaré maven para gestionar la dependencia.
Introducción 395
Como ya sospecharás, también necesitarás maven, pero es muy probable que ya
vaya integrado en el IDE, al menos en Eclipse.
Aunque no es 100 % imprescindible, resulta muy útil tener un cliente para visualizar
y manejar la base de datos. Hay muchos y muy buenos, pero yo he escogido mySQL
Workbench, que también podrás descargar de la página oficial de mySQL, ahí donde
descargaste el servidor de base de datos.
TRUCO:
Por cierto, en la página de mySQL, cuando vayas a descargar las cosas, te darás cuenta
de que no estás obligado a crear una cuenta… debajo de los botones aparece la opción de
descargar sin identificarte.
Configuración del proyecto
En Eclipse hay que crear un proyecto maven utilizando el arquetipo quickstart. Se
usa la paquetización del proyecto como id de grupo y su nombre como id de artefacto.
Para el ejemplo de este capítulo pondré groupId: [Link] y artifactId: gestor.
Dependencias
En el fichero [Link] añadiremos las siguientes dependencias:
• conector java para mySQL
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<ersión>8.0.19</ersión>
</dependency>
• Hibernate
<dependency>
<groupId>[Link]</groupId>
<artifactId>hibernate-agroal</artifactId>
<version>[Link]</version>
<type>pom</type>
</dependency>
TRUCO:
La información adecuada para añadir dependencias aparece en [Link].
Tras configurar las dependencias, el proyecto en Eclipse tendrá un aspecto parecido al
de la figura 16.1.
396 Bases de datos: Mapeo Objeto-Relacional
Figura 16.1. Proyecto maven en Eclipse para Hibernate.
Creación de la base de datos
Para conectar el código Java con una base de datos, antes hay que crear ese esquema de
base de datos. Según configuremos Hibernate, se requerirá o no crear antes las tablas de
la base de datos. En este ejemplo dejaremos que Hiberante cree de manera automática
las tablas, así que simplemente haremos el esquema, que llamaremos «gestor». Te
recomiendo utilizar la interfaz gráfica de un cliente como mySQL Workbench, pero si
necesitaras hacerlo con SQL la sentencia sería:
CREATE DATABASE `gestor` ;
Configuración de Hibernate
Ahora configuraremos la persistencia en nuestro proyecto, es decir, contaremos a Hibernate
a qué base de datos se conectará, con qué datos… Hay distintas formas de hacerlo.
Como Hibernate (que es una implementación en concreto) fue anterior a JPA (que sería
más bien un estándar a seguir), los ejemplos más antiguos utilizan la forma de hacerlo
con Hibernate: un fichero de configuración llamado [Link]. Pero JPA propone
otra forma y es así como lo realizaremos. Puede parecer un poco lioso a la hora de buscar
ejemplos en los que inspirarse, pues, si no han escogido el mismo camino, puede que no
nos valgan tal cual, pero en esencia es lo mismo.
Introducción 397
Bueno, entonces, el modo de hacerlo según JPA es con un fichero de configuración en
formato XML llamado [Link] y que ha de estar dentro de la carpeta META-INF.
Así pues, paso a paso:
• Creamos una carpeta META-INF (respetando mayúsculas, guion…) dentro de
src/main/java.
• Dentro de esta carpeta, creamos un fichero de texto que llamaremos, como hemos
comentado ya, [Link] (cuidado con las eses y las ces).
• En el fichero, tenemos que escribir en XML, que suele ser un poco laborioso, pero es
interesante hacerlo al menos una vez a mano. En cualquier caso, entre los ficheros
de los ejercicios, encontrarás el ficherito de marras.
• En la primera línea, como en todo fichero XML debemos definir versión y encoding.
Así, tal cual, sin mucho que discutir. Los ficheros XML funcionan con etiquetas de
apertura y cierre, por lo que hay que ser cuidadoso con la sintaxis.
<?xml version="1.0" encoding="UTF-8"?>
• La etiqueta raíz en este fichero debe ser persistence, y en ella indicamos la ruta del
esquema y el espacio de nombres que vamos a usar.
<persistence version="2.1" xmlns="[Link]
xmlns:xsi="[Link]
xsi:schemaLocation="[Link] [Link]
org/xml/ns/persistence/persistence_2_1.xsd">
</persistence>
Quizá mejor copiar y pegar, ¿no? Hazlo como puedas, ¡pero escribe esto en tu fichero
[Link]!
• Dentro de la etiqueta persistence, meteremos una persistence-unit, y aquí ya
empezamos a personalizar.
<persistence-unit name="gestor" transaction-type="RESOURCE_LOCAL">
</persistence-unit>
Le pondremos el nombre que queramos, pero tenemos que recordarlo porque luego
lo referenciaremos en el código Java. Llamaremos «gestor» a esta unidad de persistencia.
• Dentro de persistence-unit, indicamos el proveedor que usaremos. En nuestro caso,
el de Hibernate:
<provider>[Link]</provider>
• Y luego las propiedades: una etiqueta que las engloba, en plural, y una etiqueta por
cada una en singular, dentro de esta.
<properties>
</properties>
• Hay que especificar la URL de nuestra base de datos. En proyectos reales, no podrá
ser localhost, sino que tendrá un nombre o IP de un servidor. Y si en lugar de ser mySQL
es de otro motor de base de datos, pues la sintaxis de esa URL variará un poco.
<property name="[Link]"
value="jdbc:mysql://localhost:3306/gestor"/>
398 Bases de datos: Mapeo Objeto-Relacional
En el caso de mySQL, la URL llevará el protocolo jdbc (eso todas), el subprotocolo
mysql (protocolo y subprotocolo reemplazan lo que en una URL de una web sería
http o https), así que ¿ahora qué toca? ¡://! ¿servidor? localhost ¿puerto? :3306 y,
luego, el nombre que le dimos a nuestro esquema.
• Ahora vamos a indicar el driver que evidentemente será el de mySQL.
<property name="[Link]" value="[Link]"/>
• Después ponemos el nombre de usuario. Que como no hemos creado ninguno, para
nuestro caso sería root, usuario que jamás usarás en un proyecto de verdad, ¡claro!
<property name="[Link]" value="root"/>
• Y la contraseña… Yo puse RootRoot. De nuevo, una opción no válida en proyectos
en producción, en los que además ¡nos tendríamos que currar una forma de no poner
la contraseña en claro en un fichero de configuración del proyecto!
<property name="[Link]" value="RootRoot"/>
• Para terminar, especificamos cómo queremos que Hibernate gestione la base de datos.
Hay varias opciones, en la que no nos meteremos ahora. Lo pondremos en update que
significa que, si hacemos cambios en el código de nuestro mapeo (añadir o quitar
columnas, por ejemplo), el propio Hibernate se encargará de modificar la base de datos.
<property name="[Link]"
value="update"/>
Existen un montón de propiedades más, pero, de momento, con estas nos vale.
T16.1
Ya le hemos dado a Hibernate los datos mínimos que precisa para ponerse a funcionar.
Entidades
Recuerda que el objetivo del mapeo entre Java y la base de datos es lograr representar
en programación orientada a objetos la estructura de datos que tenemos en la base de
datos. Para ello, recurriremos a unas clases que llamaremos entidades. En la mayoría de
los casos (en algunos casos peculiares no será exactamente así), habrá una correspondencia
casi directa entre clases y tablas, es decir, por cada tabla habrá una clase.
Para hacer el mapeo hay dos opciones: por el fichero de configuración XML (la primera
forma que hubo de hacerlo) o por anotaciones. Puede que en código viejo encontréis
configuraciones por XML, pero, en serio, si vais a empezar algo nuevo… las anotaciones
son mucho más cómodas. ¿Por qué? Porque van metidas dentro del código Java, justo
por encima de cada elemento, mientras que la opción XML está en otro fichero y,
sinceramente, ¡era un engorro!
Entidades 399
NOTA:
Las anotaciones en Java se usan para muchas cosas, no solo para el mapeo objeto relacional.
Son esas arrobas (@) que se ponen antes de ciertos elementos. Incluso es posible crear tú
anotaciones propias, no hay más que escribir una clase que cumpla ciertos requisitos.
Para marcar como entidad una clase Java, es decir, para vincularla con una tabla en base
de datos, utilizaremos la anotación @Entity. Le estamos diciendo al sistema de persistencia
que esta clase es una entidad de esas que tiene que mapear. Al incluir esta anotación se
ha importado de manera automática el paquete [Link]. Como decía en
la nota precedente, las anotaciones están implementadas en clases y, por tanto, requerimos
importarlas como cualquier otra clase que quisiéramos usar.
TRUCO:
Fíjate también en el paquete. Como las cosas se pueden hacer por Hibernate o por JPA, hay
muchas anotaciones con nombres coincidentes que están disponibles en ambas paquetizaciones.
Las de Hibernate cuentan con el paquete [Link]; las de JPA, [Link].
Así que cada vez que tengamos que elegir, acuérdate de que estamos haciéndolo por JPA. Hay
que mantener la coherencia o no va a funcionar.
Ahora hay que decirle con qué tabla queremos mapear esta clase, esta entidad. Pues
debajo del @Entity meteremos la anotación de tabla: @Table(name = "nombre_tabla").
Si una clase se mapea a una tabla, un atributo se mapea a una columna, a cada uno le
pondremos un @Column indicando el nombre de la columna en la tabla: @Column
(name = "nombre_columna"). Por cada atributo tendremos los métodos getter y setter,
para que Hibernate acceda a ellos.
TRUCO:
En Eclipse podemos crear los getters y setters de uno o varios atributos haciendo clic derecho
dentro de la clase Fuente>Generar getters y setters.
Para marcar la clave primaria, utilizaremos la anotación @Id, sin más.
Y si alguna columna queremos que sea autogenerada, también podemos hacerlo:
@GeneratedValue(strategy = [Link]). Y es posible escoger entre
varias estrategias.
T16.2 Pero ¡voy a contarte un secreto! Cuando las columnas se llaman igual que los atributos
nos ahorramos los @Column.
400 Bases de datos: Mapeo Objeto-Relacional
Entidad Pedido
Llegó el momento de hacer nuestro primer mapeo. Empecemos por la clase Pedido, muy
sencillita. Le pondremos solo tres campos: id, referencia y fecha, sin relacionarla aún con
otras tablas.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@Entity
@Table(name = "pedido")
public class Pedido {
@Column(name = "id")
@Id
@GeneratedValue(strategy = [Link])
private int id;
@Column(name = "referencia")
private String referencia;
@Column(name = "fecha")
private Date fecha;
public int getId() {
return id;
}
public void setId(int id) {
[Link] = id;
}
public String getReferencia() {
return referencia;
}
public void setReferencia(String referencia) {
[Link] = referencia;
}
public Date getFecha() {
return fecha;
}
public void setFecha(Date fecha) {
[Link] = fecha;
}
}
¿A que no ha sido nada complicado anotar nuestra primera entidad?
Objetos de Acceso a Datos (DAO)
Las cosas se suelen poder hacer de mil formas, y muchas de esas formas las podemos
clasificar dentro de la categoría «hacerlo mal», así que lo que vamos a ver ahora no es
obligatorio. Eso sí, es muy común, muy habitual, así que casi casi lo puedes tomar como
obligatorio.
Objetos de Acceso a Datos (DAO) 401
Existe hoy en día la tradición de utilizar objetos DAO (objetos de acceso a datos) para
acceder a los datos. La frase es un poco redundante, pero lo es porque es lógica. Esta
tradición no viene de la nada, sino de un patrón de diseño software (de hecho, de uno
de los más usados). No me voy a meter en los patrones de diseño, porque dan para
páginas y páginas, pero dejémoslo en que este patrón nos invita a tener una clase con
los métodos para acceder a los datos. Si por un lado tenemos las entidades que se
emparejan con las tablas de la base de datos, por el otro están los métodos del DAO que
emparejan con las operaciones CRUD (de creación, petición, actualización y borrado de
datos) de SQL.
En las clases DAO, que habrá una por entidad o tabla, tendremos métodos para realizar
consultas (genéricas o adaptadas a nuestro negocio), creaciones, actualizaciones,
borrados… Pero algunos de estos métodos serán muy parecidos, por no decir iguales
para unas y otras tablas. No distan mucho cómo encontrar una persona por id, cómo
encontrar una dirección por id o cómo borrar un expediente por id. Por ello, en los
proyectos se suele crear una parte común. Una interfaz que defina todas las operaciones
comunes para cualquier entidad (recuperar todo, recuperar por id, guardar, actualizar,
borrar), para luego seguir con una clase abstracta que implemente la parte común o
genérica para todas las entidades y, finalmente, crear una clase DAO por cada entidad,
en la que añadir métodos particulares a dicha entidad.
Interfaz Dao
Creamos una interfaz, llamada por ejemplo Dao, dentro del subpaquete dao. Como
trataremos distintos tipos de datos (entidades) en cada dato, hagamos parametrizable
esta interfaz, así que le pondremos un tipo genérico T.
Esta interfaz contará con cinco métodos:
• get: Recupera un registro en concreto, según su id. Como puede que no encuentre
ninguno, retornamos un objeto Opcional. Recibirá el id y devolverá (o no) un objeto
del tipo genérico de la clase.
• getAll: Por lo general, también requerimos recuperar todos los registros: no recibe
nada y devuelve una lista de objetos de ese tipo genérico.
• save: Fundamental para poder crear nuevos objetos, hemos de guardarlos.
Recibirá uno de estos objetos y no devolverá nada, porque lo habrá guardado
en base de datos.
• update: Como los datos no suelen estar cincelados en mármol, ofreceremos la
posibilidad de actualizarlos. Este método recibirá el objeto a actualizar y de nuevo
no nos devolverá nada.
• delete: Finalmente, nos queda el borrado, como en el resto de caso, recibirá el objeto
y no nos dará ni las gracias.
402 Bases de datos: Mapeo Objeto-Relacional
Ya disponemos de la interfaz que implementarán nuestros DAO:
package [Link];
import [Link];
import [Link];
public interface Dao<T> {
Optional<T> get(long id);
List<T> getAll();
void save(T t);
void update(T t); T16.3
void delete(T t);
}
Gestor de entidades
Partiendo de la interfaz Dao podemos implementar una clase abstracta. Nosotros solo
haremos una versión de esta clase, para acceder a la base de datos a través de Hibernate,
pero si en nuestro proyecto pudiéramos guardar datos distribuidos en distintos tipos de
sistemas, podríamos disponer de distintas implementaciones de la interfaz, por ejemplo,
para acceder a los datos de la base de datos, para acceder a los datos a través de
microservicios o para acceder a los datos en una base de datos noSQL.
Pero antes prepararemos una clasecilla de utilidad que nos ayude a establecer y manejar
las conexiones con la base de datos. Es una clase muy sencilla (de pocas líneas), pero
muy importante para acceder a la base de datos.
• La llamamos EntityManagerUtil y la ponemos dentro del paquete utiles.
package [Link];
public class EntityManagerUtil {
}
• En esta clase, creamos un único método estático, getEntityManager(), que no recibe
nada pero devuelve un objeto EntityManager (de la librería de persistencia de Java).
• Creamos un objeto factoría, EntityManagerFactory, de la misma librería.
A la hora de crear la factoría, es importante que le indiquemos el nombre correcto
que le hayamos dado a la unidad de persistencia en el fichero de configuración
[Link]. En nuestro caso «gestor».
• Y ahora que ya tenemos la factory, le pedimos el gestor de entidades, el entityManager
que hay que devolver.
public static EntityManager getEntityManager() {
EntityManagerFactory factory =
[Link]("gestor");
EntityManager manager = [Link]();
return manager;
}
Ya está. ¡Ya he dicho que era corta! Pero vital.
Objetos de Acceso a Datos (DAO) 403
Y, como es vital, convendría probarla, así que añadamos un pequeño método main,
simplemente para comprobar que todo está bien conectado. El código sería este:
public static void main(String[] args) {
EntityManager manager = [Link]();
[Link]("EntityManager class ==> " +
[Link]().getCanonicalName());
}
Es decir, llamamos al método que hemos creado y luego pintamos «cualquier cosa» por
pantalla. Lo importante es la llamada, ya verás. Ejecutémosla:
[Link] logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [name: gestor]
[Link] logVersion
INFO: HHH000412: Hibernate ORM core version [Link]
[Link] <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {[Link]}
[Link]
configure
WARN: HHH10001002: Using Hibernate built-in connection pool (not for production
use!)
Loading class `[Link]'. This is deprecated. The new driver class
is `[Link]'. The driver is automatically registered via the SPI
and manual loading of the driver class is generally unnecessary.
[Link]
buildCreator
INFO: HHH10001005: using driver [[Link]] at URL [jdbc:mysql://
localhost:3306/gestor]
[Link]
buildCreator
INFO: HHH10001001: Connection properties: {user=root, password=****}
[Link]
buildCreator
INFO: HHH10001003: Autocommit mode: false
[Link]
pl$PooledConnections <init>
INFO: HHH000115: Hibernate connection pool size: 20 (min=1)
[Link] <init>
INFO: HHH000400: Using dialect: [Link].MySQL8Dialect
[Link].
DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [[Link].
[Link]$ConnectionProviderJdbcConnecti
onAccess@5baaae4c] for (non-JTA) DDL execution was not in auto-commit mode; the
Connection 'local transaction' will be committed and the Connection will be set
into auto-commit mode.
[Link]
initiateService
INFO: HHH000490: Using JtaPlatform implementation: [[Link].
[Link]]
EntityManager class ==> [Link]
Dedícale, aunque sea solo esta vez, un ratito a leer toda esta salida. Todas las líneas salvo
la última, que verás en rojo en tu pantalla, se generan al llamar al método getEntityManager.
Deberíamos reconocer algunos de los datos de configuración de nuestro proyecto, como
el nombre de la unidad de persistencia (gestor), el driver mysql, el usuario root… Pero
no parece que haya ningún error, muchos mensajes de información y uno solo de warning,
de aviso, que nos advierte de que la configuración no es adecuada para aplicaciones en
producción. No hay problema, no tenemos planes de mandar «gestor» a producción.
404 Bases de datos: Mapeo Objeto-Relacional
El DAO abstracto
Ya contamos con la interfaz y el EntityManagerUtil, así que ha llegado el momento de
(medio) implementar la clase abstracta.
• Empezamos creando la AbstractDao, en el paquete dao, abstracta, y que implemente
la interfaz Dao.
package [Link];
public abstract class AbstractDao<T> implements Dao<T> {
}
TRUCO:
Como es una clase abstracta, no da ningún error si la dejamos vacía. Puedes quitar eso del
abstract, para que Eclipse nos genere el esqueleto de todos los métodos que debemos implementar,
y luego volver a poner el abstract.
• Lo primero que requerimos, porque lo usaremos en todos los métodos, es un
entityManager. Así pues, declaremos e inicialicemos el atributo:
private EntityManager entityManager = [Link]();
• Otro atributo necesario es uno que lleve la clase con la que vamos a trabajar. Su
declaración sería algo así:
private Class<T> clazz;
• Pero no podemos inicializarla, pues en la clase abstracta no sabemos aún qué valor tomará,
así que le haremos un método set. Debajo del esqueleto de delete, meteremos esto:
public void setClazz(Class<T> clazz) {
[Link] = clazz;
}
Vamos a por las consultas. Recuerda que las consultas a la base de datos no la modifican.
Hemos decidido tener dos métodos de consulta: por id y todos.
• Empecemos por la búsqueda por id:
@Override
public Optional<T> get(long id) {
return [Link]([Link](clazz, id));
}
Le pedimos al entityManager que busque (find). Le pasamos la clase de la entidad (para
que sepa en qué tabla buscar) y el id (para que sepa darnos el registro que estamos
buscando). Lo envolvemos todo con [Link] para asegurarnos de que se
maneja bien en caso de que no encuentre nada.
• El otro método get es el de devolver todos los registros de una tabla.
@Override
public List<T> getAll() {
String qlString = "FROM " + [Link]();
Query query = [Link](qlString);
return [Link]();
}
Objetos de Acceso a Datos (DAO) 405
Preparamos una petición (query, en inglés), que será como un trocito de lo que sería la
SELECT en SQL: FROM y el nombre de la tabla. Y eso se lo pasamos al entityManager
para que cree la query, y a esta le pedimos la lista de resultados. Fácil, ¿no?
• Ahora toca el turno de los métodos de modificación: guardar, actualizar, borrar. Como
modifican la base de datos, deberíamos ser capaces de deshacer si hay algún problema y
también de indicar cuándo empezamos y terminamos la transacción… Y como eso va a
ser lo mismo, hagamos la operación que hagamos, vamos a sacarlas a un método privado.
private void executeInsideTransaction(Consumer<EntityManager> action) {
EntityTransaction tx = [Link]();
try {
[Link]();
[Link](entityManager);
[Link]();
} catch (RuntimeException e) {
[Link]();
throw e;
}
}
Le pedimos una transacción al entityManager, intentamos iniciarla, ejecutar la acción de
turno y comitear la transacción. Y si hay algún problema, deshacemos (rollback) y lanzamos
la excepción para que la manejen más arriba.
Con este tema resuelto, verás cómo quedan de sencillas las tres operaciones pendientes.
NOTA:
Los métodos siguientes utilizan expresiones lambda. Las tratamos en el próximo capítulo.
• La primera, guardar, save:
@Override
public void save(T t) {
executeInsideTransaction(entityManager -> [Link](t));
}
Al entityManager le pedimos que haga persistir el objeto recibido, y esa acción se la
pasamos al método que hemos hecho para gestionar las transacciones. Ya está.
• Para la actualización, update, exactamente lo mismo, pero en vez de persistir,
mergeamos. (¡Mira que se complican con los nombres!).
@Override
public void update(T t) {
executeInsideTransaction(entityManager -> [Link](t));
}
• Y para borrar, que lo hemos llamado delete, pues tiraremos del método remove del
entityManager:
@Override
public void delete(T t) {
executeInsideTransaction(entityManager -> [Link](t));
}
Aunque ahora mismo aún no podemos hacer nada con esto… verás cuánto trabajo hemos
hecho en cuanto creemos el Dao de cada entidad.
406 Bases de datos: Mapeo Objeto-Relacional
Los DAO del gestor
Tenemos ya la interfaz Dao y la clase AbstractDao. Necesitamos una implementación
por cada entidad. Así que empezamos por PedidoDao que extiende de AbstractDao.
• Si la generamos con el asistente de Eclipse, el resultado será este:
package [Link];
public class PedidoDao extends AbstractDao<T> {
}
• Sale un minierror en la T. Ha llegado el momento de concretar. Ahora tenemos que
sustituir esta T genérica por una clase concreta. ¿Cuál? La de la entidad a la que
queremos hacerle el Dao, es decir, Pedido.
package [Link];
import [Link];
public class PedidoDao extends AbstractDao<Pedido> {
}
• En la clase abstracta nos falta determinar la clase con la que trabajamos, ¿no? Pues
añadimos el constructor de PedidoDao e informamos de nuestra clase:
public PedidoDao() {
setClazz([Link]);
}
Y listo. Ya hemos hecho nuestro PedidoDao. Sí, en serio. Lo comprobamos.
• En la clase App que nos regaló maven, cambiamos el código que trae por crear un
PedidoDao y le pedimos que nos dé todos los pedidos.
public static void main(String[] args) {
PedidoDao pedidoDao = new PedidoDao();
List<Pedido> pedidos = [Link]();
[Link]("***Pedidos: " + pedidos);
}
Ejecutamos y comprobamos que no devuelve nada, porque tenemos la base de datos
vacía, pero tampoco da ningún error, y eso son buenas noticias.
***Pedidos: []
• Añadimos la creación y guardado de un pedido, y analizamos los resultados:
public static void main(String[] args) {
PedidoDao pedidoDao = new PedidoDao();
Pedido pedido = new Pedido();
[Link](new Date());
[Link]("001");
[Link](pedido);
List<Pedido> pedidos = [Link]();
[Link]("***Pedidos: " + pedidos);
}
Ejecutamos:
***Pedidos: [[Link]@6df7988f]
Objetos de Acceso a Datos (DAO) 407
Como se ve un poco feo, aprovechamos para hacerle un par de retoques a la clase Pedido,
la entidad.
• Primero le meteremos un toString para que, cuando listemos las reuniones, salga algo
legible. Podemos aprovecharnos de Eclipse para ello: en la propia clase, botón derecho,
Fuente>Generar toString y le pedimos que incluya los tres atributos.
Si volvemos a ejecutar ahora ya es posible comprobar que los pedidos que se muestran
son los que hemos generado. Me salen dos porque yo he ejecutado dos veces.
***Pedidos: [Pedido [id=1, referencia=001, fecha=2021-04-09 17:26:42.666], Pedido
[id=2, referencia=001, fecha=Fri Apr 09 17:27:03 CEST 2021]]
El otro retoque que quiero hacerle a Pedido, para jugar un poco más en serio, es crearle
un constructor para poder meter más cómodamente nuevos pedidos en la base.
• Añadimos un constructor que reciba la referencia y la fecha del pedido:
public Pedido(String referencia, Date fecha) {
[Link] = referencia;
[Link] = fecha;
}
• Y tras guardar, tenemos errores en App, porque el constructor de Pedido sin
parámetros no está, así que corregimos la forma de construir el pedido, dentro del
main de App:
Pedido pedido = new Pedido("001", new Date());
[Link](pedido);
Volvemos a ejecutar:
Exception in thread "main" [Link]: [Link].
InstantiationException: No default constructor for entity: : [Link].
Pedido
at [Link](ExceptionConverterImpl.
java:154)
at [Link].
list([Link])
at [Link]([Link])
at [Link]([Link])
at [Link]([Link])
Caused by: [Link]: No default constructor for entity:
: [Link]
No debería haber pasado nada, pero… ¡ha explotado!
Pánico fuera, leamos… No hay constructor por defecto en la entidad Pedido. Eso nos ha
pasado porque Java nos regala el constructor por defecto (sin argumentos) siempre y
cuando no creemos otros constructores. Pero como hemos creado uno de dos argumentos,
pues ha asumido que el de por defecto no lo queremos. Pero, como sí lo queremos, porque
lo usa Hibernate para crear los objetos… vamos a ponerle el constructor por defecto y
resolvemos este problema.
public Pedido() {
}
Volvemos a probarlo y vemos que ya se ha resuelto todo.
408 Bases de datos: Mapeo Objeto-Relacional
*** Pedidos: [Pedido [id=1, referencia=001, fecha=2021-04-09 17:26:42.666], Pedido
[id=2, referencia=001, fecha=2021-04-09 17:27:03.721], Pedido [id=3, referencia=001,
fecha=2021-04-09 17:31:43.719], Pedido [id=4, referencia=001, fecha=Fri Apr 09
17:33:55 CEST 2021]]
Ya tengo cuatro pedidos… Pero ¿dónde están? Vuelve unas páginas atrás y comprueba
en qué momento hemos creado la tabla en la base de datos. ¿No lo encuentras? ¡Normal!
Es que no la creamos. Creamos el esquema, pero lo dejamos vacío. Sin embargo, parece
que nuestro programa ha sido capaz de crear ya cuatro pedidos, y parece que los recuerda
de una ejecución a otra porque, si no, solo dispondría de uno.
Vamos un momento a mySQL Workbench a ver qué ha sucedido. La tabla pedido ha sido
creada y cuenta con cuatro registros, justo con los datos que nos está devolviendo nuestro
programa. Ha sido Hibernate quien ha creado no solo los registros, sino también la tabla.
Figura 16.2. Esquema gestor y tabla pedido vistas en mySQL Workbench.
Si observas con detalle, todos los pedidos tienen referencia «001», pues es la que le
estamos dando al crearlas, fechas bastante consecutivas, pues estamos ejecutándolo varias
veces en nuestras pruebas, ¿y el id? Uno, dos, tres, cuatro. ¿Qué ha pasado? Simplemente
que lo hemos hecho todo muy bien, y como el id lo hemos configurado como autogenerado,
cuando creamos un nuevo pedido no indicamos qué id tendrá, pero… cuando ya la
hemos guardado, ya dispone de su id. T16.4
¡Primer Dao hecho!
Consultas simples
Con la implementación que hemos montado, de PedidoDao concretando AbstractDao,
es posible hacer las operaciones básicas de acceso a datos: recuperar un registro por id,
recuperar todos los registros y guardar, modificar o borrar un registro. Sin embargo, para
muchas tablas tendremos necesidades específicas, sobre todo consultas. Pensando en los
pedidos… no sé, por ejemplo, podríamos necesitar recuperar la lista de pedidos de la
semana pasada o el pedido más reciente.
Para resolver estas dos peticiones necesitaremos añadir dos métodos a PedidoDao.
Consultas simples 409
Consulta del pedido más reciente
• Empecemos por pedidoMasReciente, un método que nos devolverá un pedido (o no).
Lo primero que haremos es preparar la query que queremos. Deseamos recuperar de
la clase/tabla Pedido aquellos registros con fecha inferior a la actual, y ordenamos
por fecha, para que nos dé el más reciente de los que cumplen esa condición.
Para saber la fecha actual, es posible usar la función now() en la query.
public Pedido pedidoMasReciente() {
String qlString = "FROM " + [Link]()
+ " WHERE fecha < now() order by fecha desc";
}
• Bien, ya tenemos la query, ahora toca usarla. Accedemos al entityManager para que
nos cree un objeto query con el String que hemos preparado.
Query query = [Link](qlString);
Pero en Eclipse entityManager se nos pone rojo, a ver qué vergüenzas tiene.
Figura 16.3. El error del entityManager en pedidoMasReciente.
Ah, pues sí, parece que es vergonzoso… nos dice que entityManager no es visible y
nos sugiere cambiarle la visibilidad a protected, pero esa opción no me mola, mejor
creamos un método getEntityManager en AbstractDao.
public EntityManager getEntityManager() {
return entityManager;
}
Ahora ya podremos acceder, «elegantemente» y siguiendo los usos habituales en Java,
a entityManager.
Query query = getEntityManager().createQuery(qlString);
Seguimos con problemas de compilación porque nuestro método debe devolver algo
y aún no lo estamos devolviendo. Pero pensemos antes… ¿Cuántos pedidos queríamos?
Uno, pues hagamos dos cosas:
• Por un lado, le diremos a la query que solo queremos un resultado:
Query query = getEntityManager().createQuery(qlString).setMaxResults(1);
• Y luego, a la hora de recuperar el resultado de la query, le pedimos un resultado único,
¡getSingleResult()!
public Pedido pedidoMasReciente() {
String qlString = "FROM " + [Link]()
+ " WHERE fecha < now() order by fecha desc";
410 Bases de datos: Mapeo Objeto-Relacional
Query query = getEntityManager().createQuery(qlString).setMaxResults(1);
return (Pedido) [Link]();
}
Lo único que nos falta es hacer un casting del objeto que devuelve la query hacia nuestro
objeto Pedido.
Para hacer la prueba, primero comprobamos que existen pedidos con fechas diversas.
Pero ya sabes que manejar fechas de Java es un dolor de cabeza… Mejor las cambiamos
del paquete [Link] al paquete [Link]. Entonces, nos vamos a la entidad, la clase
Pedido, y modificamos los Date por LocalDateTime. Escogemos LocalDateTime en vez
de LocalDate porque queremos los pedidos con hora.
package [Link];
import [Link];
…
@Entity
@Table(name = "pedido")
public class Pedido {
…
@Column(name = "fecha")
private LocalDateTime fecha;
public Pedido() {
}
public Pedido(String referencia, LocalDateTime fecha) {
[Link] = referencia;
[Link] = fecha;
}
…
public LocalDateTime getFecha() {
return fecha;
}
public void setFecha(LocalDateTime fecha) {
[Link] = fecha;
}
…
}
Al guardar habrá errores en la clase que tiene el main. En lugar de new Date() llamaremos
al método now() de LocalDateTime.
Pedido pedido = new Pedido("001", [Link]());
Ya las fechas son manejables, seguimos con lo de crear pedidos para fechas diversas, por
ejemplo, pasado mañana. A la fecha de ahora le sumamos dos días, en [Link]:
Pedido pedido2 = new Pedido("pedFut", [Link]().plus(2, ChronoUnit.
DAYS));
[Link](pedido2);
Añadimos una llamada a [Link] en el main e imprimimos el
resultado.
Pedido masReciente = [Link]();
[Link]("*** Pedido más reciente: " + masReciente);
Resultado:
*** Pedido más reciente: Pedido [id=5, referencia=001, fecha=2021-04-09T19:13:41.473]
Consultas simples 411
Que no es el de referencia "pedFut", sino el último de los de referencia "001" que seguimos
creando a cada ejecución.
Pero ¿qué pasaría en el caso de que no tuviéramos ninguna reunión prevista? Como
vaciar la base me da un poco de pereza y lo que queremos comprobar es qué pasa cuando
la query no encuentra nada, le ponemos una condición muy mala a la query, para
asegurarnos de que no lo va a encontrar y ¡listo! Así que volvamos al método
pedidoMasReciente de PedidoDao y le pedimos que, además, el id de la reunión sea
mayor que 100.
String qlString = "FROM " + [Link]()
+ " WHERE fecha < now() and id > 100 order by fecha desc";
ADVERTENCIA:
Cuidado, porque como cada vez que ejecutamos el main estamos creando uno o dos pedidos,
poco a poco ¡la base se va llenando!
Ejecutamos:
Exception in thread "main" [Link]: No entity found
for query
at [Link](Abstrac
[Link])
at [Link]([Link])
at [Link]([Link])
¡Ups! ¿Qué ha pasado? Bueno, tampoco podemos sorprendernos tanto, porque estamos
probando un caso complicado y no hemos hecho nada para tratarlo, así que normal que
falle. Pero leamos con detalle el mensaje de error: da una NoResultException, es decir,
una excepción porque no hay resultados. Suena lógico. Vemos además que el problema
viene de la línea 15 de PedidoDao, que es justo la línea en la que llamamos a getSingleResult.
Si ponemos el ratón encima del método nos saca el javadoc y ahí leemos qué excepciones
puede lanzar y cuándo (ver figura 16.4). En otras palabras, este es el comportamiento
previsto del método, quizá deberíamos haber leído la documentación antes de usarlo.
Figura 16.4. El error del entityManager en pedidoMasReciente.
Ante una excepción es posible salir corriendo, abandonar la profesión o tratarla con normalidad,
como aprendimos en el capítulo 10. En el método main, cuando estamos llamando a
pedidoMasReciente, meteremos un bloque try-catch para capturar y tratar esa excepción.
412 Bases de datos: Mapeo Objeto-Relacional
try {
Pedido masReciente = [Link]();
[Link]("*** Pedido más reciente: " + masReciente);
} catch (NoResultException nre) {
[Link]("No tienes ningún pedido reciente.");
}
Intentamos recuperar el pedido más reciente y se lo mostramos al usuario como teníamos
previsto, pero si hay un problema, concretamente una NoResultException, entonces le
contamos al usuario que no tiene pedidos recientes.
Si te estás preguntando por qué compilaba todo sin haberla tratado, ten en cuenta que
es una RuntimeException, es decir, de las que no hay que declarar, como la
NullPointerException. Probamos otra vez:
No tienes ningún pedido reciente.
Ok, genial, ahora ya quitamos eso de que el id sea mayor que 100. Y volvemos a probar
para asegurarnos de no haber roto nada.
*** Pedido más reciente: Pedido [id=11, referencia=001, fecha=2021-04-09T19:29:14.100]
Sí que nos saca el pedido más reciente. Bien.
Consulta de pedidos de la semana pasada
Además del pedidoMasReciente, en PedidoDao, queremos también un método que nos
devuelva todos los pedidos de la semana pasada.
• Este método se llamará pedidosSemanaPasada y devolverá una lista de pedidos.
public List<Pedido> pedidosSemanaPasada() {
}
• Preparamos la query:
String qlString = "FROM " + [Link]()
+ " WHERE fecha between ?1 and ?2";
Nos interesa recuperar todos los pedidos cuya fecha esté entre otras dos, que calcularemos
en breve. La query entonces tendrá dos parámetros que marcaremos con ?1 y ?2. Cuando
construyamos la query, le pasaremos valores para los dos parámetros.
public List<Pedido> pedidosSemanaPasada() {
String qlString = "FROM " + [Link]()
+ " WHERE fecha between ?1 and ?2";
Query query = getEntityManager().createQuery(qlString);
LocalDate esteLunes = getEsteLunes();
LocalDate lunesAnterior = [Link](1);
[Link](1, [Link]());
[Link](2, [Link]());
return [Link]();
}
private static LocalDate getEsteLunes() {
LocalDate now = [Link]();
DayOfWeek diaSemana = [Link]();
return [Link]([Link]() - 1);
}
Consultas simples 413
Primero creamos una query «como siempre», pidiéndoselo al entityManager y pasándole
el String que ya hemos preparado. Y en esta ocasión, además, le pasaremos los dos
parámetros. Para ello, buscamos la fecha del lunes de esta semana (mediante un método
auxiliar, porque no es muy directo de calcular), la utilizamos para calcular a su vez la
fecha del lunes anterior y se la pasamos como parámetro uno sin olvidar setear la hora
al principio del día. Y como parámetro dos seteando de nuevo la hora al principio del
día, para cubrir los siete días de una semana completos.
Con esto ya la query está lista para pedirle resultados y getResultList nos devuelve la lista
de resultados.
Cómo no, ahora toca probarlo. Así que nos vamos a la clase App, la que tiene el main, y
le añadimos unas de líneas más:
• Creamos un pedido de hace justo una semana, para tener algo.
Pedido pedido3 = new Pedido("pedPas",
[Link]().minus(1, [Link]));
[Link](pedido3);
• Y recuperamos la lista de pedidos de la semana pasada.
List<Pedido> pedidosSemanaPasada = [Link]();
[Link]("**Pedidos de la semana pasada: " + pedidosSemanaPasada);
Guardamos y ejecutamos:
*** Pedidos de la semana pasada: [Pedido [id=23, referencia=pedPas,
fecha=2021-04-02T20:03:44.978]]
Solo nos sale un pedido porque tenemos la base bastante vacía y solo hemos ejecutado
una vez este método desde que se crea un pedido de la semana pasada. Pero si volvemos
a ejecutarlo, irá creciendo la lista. Observa que ya voy por el id 23. Ejecutamos otra vez:
*** Pedidos de la semana pasada: [Pedido [id=23, referencia=pedPas,
fecha=2021-04-02T20:03:44.978], Pedido [id=26, referencia=pedPas,
fecha=2021-04-02T20:05:57.305]]
¡Sí! ¡Ya hay dos! Si observas con detenimiento, apreciarás que entre uno y otro pedido
hay tres id de diferencia, porque cada vez que ejecutamos el código estamos creando tres
pedidos (ahora, futura y pasada). Pero siempre hay que probar el peor caso. Podemos
vaciar la base de datos o modificar ligeramente el método para que nos dé las reuniones
de hace diez semanas:
LocalDate esteLunes = getEsteLunes().minusWeeks(10);
Y ejecutamos:
*** Pedidos de la semana pasada: []
Bien, no ha habido ningún problema, porque, al ser una lista el resultado, simplemente
nos devuelve una lista vacía; y con el tratamiento que le estamos haciendo (imprimirla),
no hay mayor peligro.
414 Bases de datos: Mapeo Objeto-Relacional
Bueno, llegados a este punto, ya disponemos de un método que devuelve un resultado,
otro que devuelve varios, pero solo contamos con una entidad. ¡Quizá va siendo hora
de ampliar nuestro universo!
ADVERTENCIA: T16.5
No olvides restaurar el método pedidosSemanaPasada.
Relaciones 1:N
Entidad Albaran
A partir de un pedido, cuando lo hayamos preparado, generaremos un albarán,
como documento de entrega. Añadamos la entidad Albaran al sistema. Para no
liarnos con todos los datos que debe llevar un albarán de verdad, le pondremos un
id, una referencia, una fecha de emisión, una fecha de recepción y un valor total. El
id será autogenerado y numérico; para la referencia, tomaremos la del pedido y le
concatenaremos el prefijo «ALB-». La fecha de emisión se establecerá al crearlo, y la
de recepción, en algún momento posterior. Generamos el constructor por defecto,
uno con alguno de los parámetros (los que conocemos al crearlo), getters, setters y
toString.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@Entity
@Table(name = "albaran")
public class Albaran {
private static final String PREFIJO = "ALB-";
@Id
@GeneratedValue(strategy = [Link])
private int id;
private String referencia;
private LocalDateTime fechaEmision;
private LocalDateTime fechaRecepcion;
public Albaran() {
}
public Albaran(String refPedido) {
referencia = PREFIJO + refPedido; Figura 16.5. Tablas
fechaEmision = [Link](); tras añadir la
} entidad Albaran.
// … getters, setters, toString
}
Relaciones 1:N 415
Ya solo con crear la entidad, sin siquiera modificar el main, si lo ejecutamos una vez,
veremos en mySQL Workbench que ya se ha creado la nueva tabla. Vacía, pero ahí está,
con sus columnas. Ejecútalo y compruébalo. Incluso puedes consultar su DDL para ver
la sentencia con la que se crearía esta tabla:
CREATE TABLE `albaran` (
`id` int NOT NULL AUTO_INCREMENT,
`fechaEmision` datetime(6) DEFAULT NULL,
`fechaRecepcion` datetime(6) DEFAULT NULL,
`referencia` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Vemos que Hibernate ha sido capaz de poner los tipos correctos, indicar la clave primaria
(porque le hemos puesto el @Id). Disponer de pedidos y albaranes está muy bien, pero
si no los relacionamos no nos servirá de mucho.
Cuando nos enfrentamos a una relación entre dos tablas, debemos pensar qué tipo de
relación van a tener. La más sencilla sería una relación 1:1: un pedido tiene una factura,
una factura es de un pedido. La relación más complicada sería la M:N: un pedido tiene
muchos productos, un producto está en muchos pedidos. En este caso estaríamos en algo
intermedio: cada albarán está asociado a un pedido, pero puede que de un pedido se
generen varios albaranes, si no se puede hacer en una sola entrega, por ejemplo. Este
tipo de relación se llama 1:N. Y la forma de representarla en la base de datos es añadir
una columna en la tabla Albaran con el id de la tabla Pedido. ¿Cómo decidimos en qué
tabla ponemos el id de la otra? Fácil. Piensa que en una casilla solo va un valor. ¿Puedo
indicar en el pedido el id de los albaranes? El pedido 123 se entregará en los albaranes
234, 235 y 238. No, no puedo, porque son muchos los albaranes relacionados con ese
T16.6
pedido. Y en el albarán, ¿podemos poner el id del pedido? En este caso sí, porque hemos
dicho que cada albarán se corresponde con un solo pedido. ¿Ves cómo es fácil decidir en
qué lado va la columna? Probablemente al principio has de tantear los dos casos para
T16.7 decidir cuál es el bueno, pero con la práctica te saldrá casi de manera automática.
Traducido a entidades, sería añadir un atributo Pedido en la entidad Albaran:
@ManyToOne(fetch = [Link])
private Pedido pedido;
public Pedido getPedido() {
return pedido;
}
public void setPedido(Pedido pedido) {
[Link] = pedido;
}
En este tipo de sistemas indicaremos cómo queremos que se comporte a la hora de recuperar
los datos. Cuando recupero de base de datos, ¿quiero que lleve todos los datos del pedido?
Si la respuesta es sí, el tipo de fetch sería EAGER. Si la respuesta es no, entonces es LAZY.
Eager (‘ansioso’) suena muy bien, pero es muy peligroso, hay que usarlo con cuidado. Pero
¿qué pasa si al recuperar un albarán recupero su pedido, y al recuperar su pedido recupero
todos sus albaranes? Bueno, en nuestra base de datos con solo dos tablas y una relación
416 Bases de datos: Mapeo Objeto-Relacional
no parece muy grave, pero intenta extrapolarlo a un sistema completo y ¡verás el festival
que se puede organizar! Mejor ser perezosos (lazy) o, al menos, evaluar esa opción.
Si estuviéramos manipulando la base de datos a mano, con añadir la columna sería suficiente
(ya nos tocaría currarnos el código más adelante), pero para que Hibernate lo entienda todo
bien, también hemos de mapear la relación al revés. Pero no te asustes, es sencillo también.
En la entidad Pedido añadimos un atributo que no será un Albaran, sino una lista de albaranes:
@OneToMany(mappedBy = "pedido")
private List<Albaran> albaranes;
public List<Albaran> getAlbaranes() {
return albaranes;
}
public void setAlbaranes(List<Albaran> albaranes) {
[Link] = albaranes;
}
Fíjate en que en el lado del albarán hemos dicho que con el pedido tienen una relación
ManyToOne, mientras que del lado del pedido la anotación es OneToMany. Es la forma
de reflejar la asimetría de la relación. En el atributo mappedBy de @OneToMany indicaremos
el nombre del atributo del otro lado de la relación en otra clase, en este caso, «pedido».
Volvemos a ejecutar, aún sin modificar el main, sin usar los albaranes. En la figura 16.5
vemos qué pasa en la base de datos
Nueva columna generada
Nueva clave foránea
Figura 16.6. Tablas tras añadir la relación 1:N entre Pedido y Albaran.
Vemos que la tabla pedido sigue teniendo las mismas columnas, pero en la tabla albaran
ha aparecido una columna pedido_id y no solo eso: en la sección de claves foráneas
(foreign keys) ha aparecido una con un nombre muy feo (autogenerado). La información
del objeto nos indica que va de pedido_id al id de pedido.
Relaciones 1:N 417
Figura 16.7. Detalles de la clave foránea.
Mantener la integridad referencial entre tablas es imprescindible para garantizar que en
nuestro sistema los datos sean coherentes y usables. Las claves foráneas nos fuerzan a ello.
El DAO de Albaran
Crear la entidad (y con ello la tabla y sus columnas) es importante, fundamental, pero…
necesitamos crear el DAO, el objeto de acceso a datos, para poder realmente hacer cosas
con esa entidad. Por tanto, creemos AlbaranDao, una nueva clase bajo el paquete DAO,
que extenderá AbstractDao, pero en vez de ser de T será de Albaran.
package [Link];
import [Link];
public class AlbaranDao extends AbstractDao<Albaran> {
public AlbaranDao() {
T16.8 setClazz([Link]);
}
}
T16.9 Solo con esto, y sin hacer nada más, podemos ir al main a crear albaranes, modificarlos
o borrarlos. Puedes intentarlo por tu cuenta.
Relaciones 1:N
Entidad Factura
Todo pedido que se precie debe tener su correspondiente factura, así que añadámoslas
al sistema.
Sin más preámbulos, creemos la entidad Factura, que de momento tendrá un id
autogenerado y un número de factura que, de hecho, es un String.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@Entity
public class Factura {
@Id
@GeneratedValue(strategy = [Link])
private int id;
private String numero;
// … getters, setters, toString
}
418 Bases de datos: Mapeo Objeto-Relacional
Observa que no he incluido en el código la anotación @Table. Al igual que con los
atributos o columnas, si no indicamos el nombre de la tabla, Hibernate la nombrará
como la clase. En este caso, sería Factura, con la F mayúscula.
Si nos parece bien que las tablas se llamen exactamente como las clases, nos
ahorramos el estar poniendo la anotación tabla en las clases. En este proyecto haremos
lo que no se tiene que hacer: mezclar criterios, pero es para que dispongas de un
muestrario de lo que es posible hacer y cómo.
Si ejecutamos el main, se creará la tabla con sus columnas en el esquema de la base
de datos. Podríamos ponerle montones de atributos a la entidad Factura, como fecha,
datos del cliente, datos del proveedor… pero de momento vamos a lo básico. Es una
factura. Vale, pero ¿de qué? De un pedido. Vale, pero ¿de cuál?
Evidentemente, la factura nos está pidiendo a gritos un atributo pedido. Lo hemos
dicho desde el principio: la factura es de un pedido. Facturas y Pedidos están
relacionados, pero ¿cómo? Llegó el momento de decidir si 1:1, 1:N o M:N. ¿Cuántas
facturas tiene un pedido? Poder, poder, podría tener muchas, pero lo suyo es que
tenga una sola, porque si no Hacienda nos cruje por no llevar bien las facturas.
Veámoslo del otro lado. ¿Cuántos pedidos se facturan en una factura? Dijimos que
uno. Pues ya lo tenemos: es una relación 1:1.
Muchas relaciones 1:1 no requieren tabla, por ejemplo, pongamos que contamos con
el número de empleado y DNI: a cada número de empleado le corresponderá un
DNI y a cada DNI le corresponderá un número de empleado, pero seguramente
acabemos teniendo ambos datos como dos columnas de una misma tabla o, traducido
a objetos, como dos atributos de una misma clase.
Sin embargo, el caso de los pedidos y las facturas sí son conceptos independientes
y, por tanto, requieren su propia entidad. En efecto, hay que añadir una referencia
a la factura en la clase pedido y una referencia al pedido desde la factura.
En la entidad Factura:
@OneToOne(mappedBy = "factura")
private Pedido pedido;
La anotación a utilizar esta vez es @OneToOne, mapeada por el nombre del atributo
que haga referencia a la factura en la clase pedido, aún no lo hemos añadido, pero
ya podemos adelantar que se llamará pedido. Añadámoslo antes de que se nos pase.
Vamos a la clase Pedido y debajo de contenido añadimos la factura.
@OneToOne
@JoinColumn
private Factura factura;
Esta vez, la anotación vuelve a ser la misma, @OneToOne, pero en vez de ponerle
información de mapeo, que ya está en el otro lado, le añadimos una segunda
Relaciones 1:N 419
anotación, @JoinColumn, que indica que el vínculo se hará por esta columna
(podríamos especificar el nombre de la columna, pero el predeterminado nos vale).
Quizá convenga hacer o rehacer los getters y setters, constructores y toString de
Factura.
Guardamos todo, ejecutamos de nuevo y vemos el efecto que tiene en base. En la
tabla Pedido no hay cambios, pero en Factura ha aparecido la columna pedido_id.
El DAO de Factura
Muy parecido a los otros:
package [Link];
import [Link];
public class FacturaDao extends AbstractDao<Factura> {
public FacturaDao() {
setClazz([Link]);
}
}
Relaciones M:N
Disponemos de pedidos, albaranes y facturas, pero ¿no echas de menos algo
importante? ¡Los productos! Si no hay productos en los pedidos, poco vamos a
entregar y poco a facturar.
Estudiemos la relación entre los productos y los pedidos. Primera pregunta: ¿Cuántos
productos hay en un pedido? Muchos: dos, diez, cien, uno, nos da igual el número
exacto. En bases de datos, si es más de uno, ya es multitud. Segunda pregunta: ¿En
cuántos pedidos puede estar un producto? Dependerá de las existencias, pero
seguramente en más de uno, en muchos. Por tanto estamos ante un nuevo tipo de
relación: muchos a muchos, M:N. Tercera pregunta: ¿En qué tabla ponemos el id de
la otra? No podemos poner los id de los productos en la tabla de pedidos, porque
las columnas admiten valores unitarios, no listas. Y lo mismo nos sucede al revés:
es imposible poner a un producto en una columna todos los pedidos en los que
aparece. ¿Cómo hacemos entonces? ¡Quítate de la cabeza tener varias (muchas)
columnas para ir poniendo un valor en cada una, es decir, en pedido tener producto01,
producto02, producto03…! ¡Eso es una burrada! ¡Va en contra de todas las buenas
prácticas existentes y por existir! (Además de que limita el número de productos en
un pedido).
No te preocupes, hay solución. Cuando tenemos una relación M:N, la solución es
generar tabla. Aparte de la tabla pedido y de la tabla producto (que, por cierto, aún
tenemos que crear), contamos con una tabla productos_pedido, o algo así, en la que
420 Bases de datos: Mapeo Objeto-Relacional
pondremos el id del pedido y el id del producto. Si lo requerimos, es posible añadir
más columnas a la tabla, columnas que aportarán información sobre la relación
como, por ejemplo, la cantidad de ese producto en ese pedido. De momento nosotros
no le pondremos más información que los dos id.
Entidad Producto
Los productos tendrán un id autonumérico, pero también una referencia interna que
queremos que sea única y una descripción. Para lograr que la referencia sea única, se lo
indicamos a Hibernate con la anotación @Column(unique = true).
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@Entity
public class Producto {
@Id
@GeneratedValue(strategy = [Link])
private int id;
@Column(unique = true)
private String referencia;
private String descripcion;
// … getters, setters, toString
}
Nos falta implementar la relación M:N. En la entidad Pedido
necesitamos un conjunto de productos.
@ManyToMany(mappedBy = "pedidos");
private Set<Producto> productos;
Y en Producto, un conjunto de pedidos: Figura 16.8. Tablas
tras añadir la
@ManyToMany relación M:N entre
private Set<Pedido> pedidos;
Pedido y Producto.
NOTA:
Podríamos utilizar List en vez de Set, pero como List se utiliza más a menudo, prefiero utilizar
Set y que así te acuerdes de que existe.
Ejecutamos el main y constatamos las consecuencias en la base de datos (figura 16.8):
ha aparecido la tabla Producto, pero también Producto_pedido. Si hubiéramos puesto
el mappedBy en Producto en vez de en Pedido, la tabla se hubiera llamado
Pedido_producto. Si nos fijamos en esta tabla veremos (figura 16.9) que posee dos
foreign key, que referencian a las columnas id, una de la tabla Pedido y la otra de la
tabla Producto.
Relaciones M:N 421
Hay que crear ProductoDao, ya sabes cómo. Te lo dejo con el resto de ficheros. Pero
no necesitamos crear un DAO para la tabla generada.
Figura 16.9. Detalles de las claves foráneas de Producto_pedido.
Gestor de Pedidos
Creamos una nueva clase GestorPedidos para hacer nuevas pruebas, los constructores
necesarios y unos métodos más en Pedido para poder:
• Añadir productos uno a uno.
public void addProducto(Producto producto) {
[Link](producto);
}
• Generar albarán.
public Albaran generaAlbaran() {
Albaran albaran = new Albaran(this);
[Link](albaran);
return albaran;
}
• Generar factura.
public Factura generaFactura() {
factura = new Factura(this);
return factura;
}
En el main de GestorPedidos, hacemos unas pruebas:
public static void main(String[] args) {
PedidoDao pedidoDao = new PedidoDao();
Producto libro = new Producto("libJava", "Manual Imprescindible Java");
Producto cuaderno = new Producto("cuaRojo", "Cuaderno rojo");
Producto lapiz = new Producto("lapHB", "Lápiz HB");
Pedido vueltaAlCole = new Pedido("153947", [Link]());
[Link](libro);
[Link](cuaderno);
[Link](lapiz);
[Link](vueltaAlCole);
Albaran albaran = [Link]();
Factura factura = [Link]();
[Link]("Pedido:\n" + vueltaAlCole);
}
El resultado se parecerá a:
422 Bases de datos: Mapeo Objeto-Relacional
Pedido:
Pedido [id=51, referencia=153947, fecha=2021-04-13T19:38:27.014,
albaranes=[Albaran [id=0, referencia=ALB-153947, fechaEmision=2021-
04-13T19:38:27.171, fechaRecepcion=null]], factura=Factura [id=0,
numero=FCT-153947], productos=[Producto [id=8, referencia=libJava,
descripcion=Manual Imprescindible Java], Producto [id=9, referencia=cuaRojo,
descripcion=Cuaderno rojo], Producto [id=10, referencia=lapHB,
descripcion=Lápiz HB]]]
Pedido dispone de id porque ha sido guardado, pero albarán y factura, cuando los
pintamos, aún no. Si miramos en la base de datos, se ha creado el pedido, pero el
resto de las tablas siguen vacías. Podríamos ir llamando al DAO de cada una, pero
si añadimos cascade = [Link] a las anotaciones de las relaciones, logramos
que al guardar el pedido también se guarden en cascada todos sus elementos.
@OneToMany(mappedBy = "pedido", cascade = [Link])
private List<Albaran> albaranes = new ArrayList<>();
@OneToOne(cascade = [Link])
@JoinColumn
private Factura factura;
@ManyToMany(mappedBy = "pedidos", cascade = [Link])
private Set<Producto> productos = new HashSet<>();
Añadimos al main este par de líneas para guardar los cambios en el pedido:
[Link](vueltaAlCole);
[Link]("Pedido actualizado:\n" + vueltaAlCole);
Si vuelves a ejecutar, ya verás todas las tablas con datos, incluso en la impresión vemos
ya los id.
Pedido actualizado:
Pedido [id=51, referencia=153947, fecha=2021-04-13T19:38:27.014, albaranes=
[Albaran [id=3, referencia=ALB-153947, fechaEmision=2021-04-13T19:38:27.171,
fechaRecepcion=null]], factura=Factura [id=2, numero=FCT-153947], productos=[Producto
[id=8, referencia=libJava, descripcion=Manual Imprescindible Java], Producto
[id=9, referencia=cuaRojo, descripcion=Cuaderno rojo], Producto [id=10,
referencia=lapHB, descripcion=Lápiz HB]]]
¿Todas? ¡No, todas no! La tabla Producto_pedido. Hasta ahora JPA se ha mostrado
muy listo, pero, como también es muy flexible, no lo puede adivinar todo. Así
que si queremos una relación bidireccional, que los pedidos tengan productos y
que los productos pedidos, mucho me temo que nos toca decirle las dos cosas en
el código Java.
Relaciones M:N bidireccionales
Una forma de lograr rellenar la tabla Producto_pedido es añadir el producto al pedido
y el pedido al producto. Pero eso es muy propenso a errores, porque podríamos añadir
un producto al pedido y olvidar añadir el pedido al producto. Tendríamos, yo creo, tres
opciones excluyentes:
Relaciones M:N bidireccionales 423
1. Que se puedan asignar productos a un pedido, que el pedido sea añadido directamente
al producto y que sea imposible añadir manualmente pedidos a los productos.
2. Que se puedan añadir pedidos a los productos, que el producto sea añadido
directamente al pedido en el que participa y que sea imposible añadir manualmente
productos a los pedidos.
3. Que se puedan añadir pedidos a los productos (en cuyo caso se añade el producto
automáticamente al pedido) y que se puedan añadir productos a los pedidos (en
cuyo caso se añade el pedido al producto).
Los dos primeros casos, que son simétricos, parecen los más sencillos de implementar y
puede que se adapten bien a la casuística de algunos sistemas, pero no creo que valga
para todos. En cambio, la tercera parece la más versátil, pero… eso tiene un precio, se
complica un poco la implementación, pues si no vamos con cuidado tal vez entremos en
un bucle infinito.
Implementemos la opción más flexible que hace que se puedan añadir en ambos sentidos.
Del lado de la clase Pedido, modificamos addProducto:
public void addProducto(Producto producto) {
[Link](producto);
if (![Link]().contains(this)) {
[Link](this);
}
}
Del lado de la clase Producto, modificamos addPedido, de forma totalmente simétrica:
public void addPedido(Pedido pedido) {
[Link](pedido);
if (![Link]().contains(this)) {
[Link](this);
}
}
Asimismo, suprimimos los métodos setProductos y setPedidos, y nos aseguramos de
que las colecciones estén inicializadas. Pruébalo y comprueba que ya no está vacía la
tabla Producto_pedido.
No es que me convenza mucho en cuanto a rendimiento, porque como empecemos a tener
pedidos con muchos productos o, sobre todo, productos en muchos pedidos (que se van
acumulando a lo largo del tiempo), este código quedaría un poco lento de ejecutarse.
Además, en cuanto a orientación a objetos y los principios esos de ocultación, ¿le importa
a un pedido todos los demás pedidos en los que aparece un producto? Creo que no.
ADVERTENCIA:
Si ejecutas el código varias veces, puede saltarte una ConstraintViolationException. Pusimos
que las referencias de los productos debían ser únicas: o quitas esa restricción o vacías la tabla
antes de cada ejecución.
424 Bases de datos: Mapeo Objeto-Relacional
Consultas con criterios de búsqueda
En páginas anteriores implementamos una consulta para obtener el pedido más reciente:
public Pedido pedidoMasReciente() {
String qlString = "FROM " + [Link]()
+ " WHERE fecha < now() order by fecha desc";
Query query = getEntityManager().createQuery(qlString).setMaxResults(1);
return (Pedido) [Link]();
}
Esta solución nos requiere saber un poco de SQL. Sin embargo, JPA (e Hibernate y cualquier
sistema ORM que se precie) nos ofrecen también la búsqueda por criterios. El código parece
un poco más engorroso. De momento vamos a verlo, ya discutiremos luego cuándo es mejor
cada opción. La implementación del mismo método usando criteria podría ser esta:
public Pedido pedidoMasRecienteCriteria() {
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Pedido> criteriaQuery = [Link]([Link]);
Root<Pedido> root = [Link]([Link]);
[Link](root).where([Link](
[Link]("fecha"), [Link]()));
[Link]([Link]([Link]("fecha")));
Query query = getEntityManager().createQuery(criteriaQuery).setMaxResults(1);
return (Pedido) [Link]();
}
Lo primero es pedirle al EntityManger un CriteriaBuilder al que le pediremos que nos
cree una CriteriaQuery de Pedido, a la que le pedimos un Root. Este objeto root es una
representación de nuestra entidad y nos servirá para ir construyendo los criterios. En
este caso: que la fecha sea anterior a now y el criterio de ordenación. Terminamos
pidiéndole una query «normal» al EntityManger a partir de la CriteriaQuery que hemos
construido y, como en la versión anterior, limitando el máximo de resultados a uno. Para
finalizar, pedimos a esa query un único resultado.
Llama a ambos métodos desde el main y comprueba que el resultado obtenido es el T16.10
mismo (o debería).
Pros y contras de criterios y queries
A la hora de hacer consultas a la base de datos, usando JPA, es posible recurrir a, por ejemplo,
las queries JPQL o los criterios. ¿Hay una opción mejor que la otra? Pues no, cada una tiene sus
pros y sus contras, y si los conoces, es más fácil decidir cuál es la más adecuada en cada caso.
• Por queries haremos tanto consultas como modificaciones de datos, mientras que con
criteria solo es posible hacer consultas. Por tanto, para realizar una inserción,
modificación o borrado, tendrás que usar JPQL. Si es solo una consulta, veamos más
diferencias para escoger.
• JPQL se suele recomendar para queries estáticas, mientras que usando criterios se
hacen más fáciles las queries dinámicas. Pero ¿cuáles son dinámicas o estáticas? Queries
estáticas son aquellas en las que siempre tenemos las mismas condiciones, por
Consultas con criterios de búsqueda 425
ejemplo, la fecha del pedido está entre tal y tal fecha, pudiendo recibir por parámetro
esos valores, no necesariamente siempre los mismos. Pero sí que siempre
comprobaremos la fecha.
Por otro lado, las dinámicas son aquellas en las que buscamos según los parámetros
que recibamos, por ejemplo, cuando si nos pasan el nombre buscamos por nombre,
si nos pasan el apellido buscamos por apellido y si nos pasan ambos buscamos por
nombre y por apellido.
• En cuanto a la paginación, criteria nos lo pone más fácil. ¿Paginación? ¡Si no estamos
en Word! Ya, pero quizá quieres tus resultados para mostrarlos en una web, y una
lista larguísima no suele ser muy buena idea. Por lo general solemos mostrar los
primeros, pongamos, cincuenta resultados, y ya si el usuario quiere ver la siguiente
página, pues vamos a por los siguientes cincuenta.
• Velocidad, rendimiento… suele ser un reto siempre. ¿Velocidad de programación o
de ejecución? ¡Qué dilema! Pero bueno, a la hora de ejecutar, usando criterios suele
ser más lento, aunque nunca está de más, si requieres rendimiento máximo, dedicar
un rato a valorar las dos opciones.
• Por último, pero desde luego no menos importante, está el tema de la seguridad.
¿Has oído hablar de la inyección de SQL, ese ataque que hace que si un hacker escribe
un trozo de query en un campo de un formulario web pueda acceder a las tripas de
nuestra base de datos? Eso se resuelve fácilmente protegiendo los campos con unas
comillas, por ejemplo. En JPQL lo tienes que hacer tú. Usando criteria, como es
Hibernate o, pongamos, quien se encarga de construir la query, ya sabe hacerlo bien.
¿Te has decidido ya? Pues espero que no, porque la decisión correcta dependerá de las
circunstancias ya no solo de cada proyecto, ¡sino de cada método! Lo importante es saber
hacer consultas de las dos formas, conocer estos pros y contras y así decidir con criterio.
Recuperando datos sin consultas
Si has programado antes sin utilizar Hibernate ni herramientas similares, tendrás la
costumbre de hacer queries para recuperar los datos de la base. Sin embargo, ahora están
los objetos mapeados, no solo las tablas. Eso significa que, si yo tengo un pedido, con
sus albaranes, su factura, sus productos, puedo ir pidiéndole esos datos al pedido e irlos
recuperando sin hacer ninguna query a la base de datos.
Clase de prueba SinQueries
Creamos una clase para probarlo:
package [Link];
426 Bases de datos: Mapeo Objeto-Relacional
import [Link];
public class SinQueries {
public static void main(String[] args) {
PedidoDao pedidoDao = new PedidoDao();
Pedido pedido = [Link]();
[Link]("*** Factura: " + [Link]());
[Link]("*** Albaranes: " + [Link]());
[Link]("*** Productos: " + [Link]());
}
}
Resultado:
*** Factura: Factura [id=5, numero=FCT-153947]
*** Albaranes: [Albaran [id=6, referencia=ALB-153947, fechaEmision=2021-
04-14T19:56:48.337, fechaRecepcion=null]]
*** Productos: [Producto [id=22, referencia=lapHB, descripcion=Lápiz HB], Producto
[id=21, referencia=cuaRojo, descripcion=Cuaderno rojo], Producto [id=23,
referencia=libJava, descripcion=Manual Imprescindible Java]]
Recuperando un pedido, hemos sido capaces de recuperar los datos del resto de tablas.
Las queries que no vemos
¡Hemos sacado la información sin hacer ni una query! Bueno, la verdad es que nosotros
no hemos hecho ninguna query, porque nos hemos limitado a hacer gets sobre los atributos
de nuestras entidades, pero ¿realmente no ha habido ninguna query ahí?
Recuerda que estamos usando las librerías de persistencia de Java, Hibernate en concreto,
para dejar que hagan su magia y nos aligeren nuestro trabajo. Pidamos a Hibernate que
nos enseñe qué magia está haciendo. Para ello, nos vamos al fichero de configuración
[Link] de nuestro proyecto y añadimos como última propiedad esta línea:
<property name = "hibernate.show_sql" value = "true" />
Es decir, a la propiedad hibertane.show_sql le damos el valor cierto. Activamos que nos
enseñe el código SQL que está usando. Guardamos y ejecutamos.
¡Así de entrada veo que se han ejecutado cinco queries! ¡Y nosotros que pensábamos que
habíamos hecho todo eso sin ni una query!
• La primera query recupera el pedido que nos interesa, con todas sus columnas.
Hibernate: select pedido0_.id as id1_2_, pedido0_.factura_id as factura_4_2_,
pedido0_.fecha as fecha2_2_, pedido0_.referencia as referenc3_2_ from pedido
pedido0_ where pedido0_.fecha<? order by pedido0_.fecha desc limit ?
• La segunda hace un join entre factura y pedido, filtrando por el id de la factura.
Hibernate: select factura0_.id as id1_1_0_, factura0_.numero as numero2_1_0_,
pedido1_.id as id1_2_1_, pedido1_.factura_id as factura_4_2_1_, pedido1_.fecha as
fecha2_2_1_, pedido1_.referencia as referenc3_2_1_ from Factura factura0_ left
outer join pedido pedido1_ on factura0_.id=pedido1_.factura_id where factura0_.
id=?
• La tercera también hace un join entre factura y pedido, filtrando por el id de la factura
del pedido.
Recuperando datos sin consultas 427
Hibernate: select pedido0_.id as id1_2_1_, pedido0_.factura_id as factura_4_2_1_,
pedido0_.fecha as fecha2_2_1_, pedido0_.referencia as referenc3_2_1_, factura1_.
id as id1_1_0_, factura1_.numero as numero2_1_0_ from pedido pedido0_ left outer
join Factura factura1_ on pedido0_.factura_id=factura1_.id where pedido0_.factura_
id=?
• La cuarta query, los albaranes con cierto id de pedido.
Hibernate: select albaranes0_.pedido_id as pedido_i5_0_0_, albaranes0_.id as
id1_0_0_, albaranes0_.id as id1_0_1_, albaranes0_.fechaEmision as fechaemi2_0_1_,
albaranes0_.fechaRecepcion as fecharec3_0_1_, albaranes0_.pedido_id as pedido_
i5_0_1_, albaranes0_.referencia as referenc4_0_1_ from albaran albaranes0_ where
albaranes0_.pedido_id=?
• Y la quinta hace lo propio con los productos.
Hibernate: select productos0_.pedidos_id as pedidos_2_4_0_, productos0_.productos_
id as producto1_4_0_, producto1_.id as id1_3_1_, producto1_.descripcion as
descripc2_3_1_, producto1_.referencia as referenc3_3_1_ from Producto_pedido
productos0_ inner join Producto producto1_ on productos0_.productos_id=producto1_.
id where productos0_.pedidos_id=?
Dos moralejas de esto:
1. No hay magia, Hibernate, o la librería de persistencia que usemos, sí hace peticiones
a la base de datos, solo que es transparente para nosotros.
2. Si hemos de entender qué demonios está haciendo Hibernate (en situaciones
complejas que no funcionan como esperas, lo necesitarás), siempre podemos activar
esa propiedad show_sql e intentar deducir qué está pasando. ¡Ánimo! ¡Tú puedes!
(Pero no lo dejes activado siempre, que ralentiza mucho).
Test
Test 16.1. ¿Qué valor debería tomar la variable name="[Link]"
si la base de datos destino se llama recursos y está en el puerto 4321
del servidor [Link]?
a) value="jdbc:mysql://[Link]/recursos"
b) value="jdbc:mysql://[Link]/recursos"
c) value="jdbc:mysql://[Link]/proyectos"
d) value="jdbc:mysql://[Link]/proyectos"
Test 16.2. ¿Cómo se llamará la tabla que se generará para esta entidad?
@Entity
@Table(name = "nombre1")
public class Nombre2 {
@Column(name = "nombre3")
@Id
@GeneratedValue(strategy = [Link])
private int nombre4;
}
a) nombre1
b) Nombre2
c) nombre3
d) nombre4
428 Bases de datos: Mapeo Objeto-Relacional
Test 16.3. Dado este código, ¿cuál sería la siguiente línea más adecuada?
Pedido ped1 = [Link](1);
[Link]("nuevaRef");
a) [Link](ped1);
b) [Link](ped1);
c) [Link](ped1);
d) [Link](ped1);
Test 16.4. ¿Qué significa el error "No default constructor for entity"?
a) Que la entidad no tiene constructores con parámetros.
b) Que la entidad no tiene ningún constructor.
c) Que la entidad no tiene constructor por defecto, porque no hemos creado ninguno.
d) Que la entidad no tiene constructor por defecto, porque hemos creado alguno con parámetros, pero
no el de sin defecto.
Test 16.5. ¿Cuál sería la traducción a SQL de este fragmento de código?
String qlString = "FROM " + [Link]()
+ " WHERE nombre = ?1 and apellido = ?2";
Query query = getEntityManager().createQuery(qlString);
[Link](1, "Jorge");
[Link](2, "López");
return [Link]();
a) SELECT * FROM Alumno WHERE nombre = Jorge and apellido = López
b) SELECT * FROM Alumno WHERE nombre = 1 and apellido = 2
c) SELECT * FROM Alumno WHERE nombre = 'Jorge' or apellido = 'López'
d) SELECT * FROM Alumno WHERE nombre = 'Jorge' and apellido = 'López'
Test 16.6. ¿Cómo sería una relación entre TarjetaCredito y CuentaBancaria?
a) 1:1
b) 1:N
c) M:N
d) ninguna
Test 16.7. ¿Cómo sería una relación entre Usuario y Contraseña?
a) 1:1
b) 1:N
c) M:N
d) ninguna
Test 16.8. En la relación entre TarjetaCredito y CuentaBancaria, ¿qué anotación
hemos de poner en el atributo de la entidad TarjetaCredito?
a) @OneToMany(mappedBy = "cuenta")
b) @OneToMany(mappedBy = "tarjeta")
c) @ManyToOne(mappedBy = "cuentas")
d) @ManyToOne(mappedBy = "tarjetas")
Test 16.9. En la relación entre TarjetaCredito y CuentaBancaria, ¿qué anotación
hay que poner en el atributo de la entidad CuentaBancaria?
a) @OneToMany(mappedBy = "cuenta")
b) @OneToMany(mappedBy = "tarjeta")
c) @ManyToOne(mappedBy = "cuentas")
d) @ManyToOne(mappedBy = "tarjetas")
Test 429
Test 16.10. ¿Cuál sería el código adecuado para recuperar todos los alumnos
apellidados García, ordenados de joven a mayor?
Root<Alumno> root = [Link]([Link]);
[Link](root).where([Link]( X ));
[Link]( Y ([Link]("fechaNacimiento")));
a) X: [Link]("nombre"), "García" / Y: [Link]
b) X: [Link]("apellido"), "Garcia" / Y: [Link]
c) X: [Link]("apellido"), "García" / Y: [Link]
d) X: [Link]("apellido"), "García" / Y: [Link]
Soluciones
Test 16.1. ¿Qué valor debería tomar la variable name="[Link].
url" si la base de datos destino se llama recursos y está en el puerto
4321 del servidor [Link]?
b) value="jdbc:mysql://[Link]/recursos": El orden correcto es
servidor:puerto/esquema
Test 16.2. ¿Cómo se llamará la tabla que se generará para esta entidad?
@Entity
@Table(name = "nombre1")
public class Nombre2 {
@Column(name = "nombre3")
@Id
@GeneratedValue(strategy = [Link])
private int nombre4;
}
a) nombre1: Si usamos el atributo name de @Table, ese será el nombre de la tabla.
Test 16.3. Dado este código, ¿cuál sería la siguiente línea más adecuada?
Pedido ped1 = [Link](1);
[Link]("nuevaRef");
c) [Link](ped1);: save vale para objetos nuevos, y aquí lo estamos
modificando. Tampoco tendría sentido borrarlo ni recuperar todos.
Test 16.4. ¿Qué significa el error "No default constructor for entity"?
d) Que la entidad no tiene constructor por defecto, porque hemos creado alguno con
parámetros, pero no el de sin defecto.
Test 16.5. ¿Cuál sería la traducción a SQL de este fragmento de código?
String qlString = "FROM " + [Link]()
+ " WHERE nombre = ?1 and apellido = ?2";
Query query = getEntityManager().createQuery(qlString);
[Link](1, "Jorge");
[Link](2, "López");
return [Link]();
d) SELECT * FROM Alumno WHERE nombre = 'Jorge' and apellido = 'López':
Es importante no olvidar las comillas simples al comparar con textos.
430 Bases de datos: Mapeo Objeto-Relacional
Test 16.6. ¿Cómo sería una relación entre TarjetaCredito y CuentaBancaria?
b) 1:N: Una cuenta bancaria puede tener asociadas varias tarjetas de crédito, pero cada tarjeta
va asociada a una sola cuenta.
Test 16.7. ¿Cómo sería una relación entre Usuario y Contraseña?
a) 1:1: Un usuario solo tendrá una contraseña, cada registro en contraseña será de un único
usuario (otra cosa será que varios usuarios tengan la misma contraseña, password123).
Test 16.8. En la relación entre TarjetaCredito y CuentaBancaria, ¿qué anotación
hemos de poner en el atributo de la entidad TarjetaCredito?
d) @ManyToOne(mappedBy = "tarjetas"): En el atributo cuenta de TarjetaCredito
haremos referencia al atributo tarjetas de CuentaBancaria.
Test 16.9. En la relación entre TarjetaCredito y CuentaBancaria, ¿qué anotación
hay que poner en el atributo de la entidad CuentaBancaria?
a) @OneToMany(mappedBy = "cuenta"): En el atributo tarjetas de CuentaBancaria
haremos referencia al atributo cuenta de TarjetaCredito.
Test 16.10. ¿Cuál sería el código adecuado para recuperar todos los alumnos
apellidados García, ordenados de joven a mayor?
Root<Alumno> root = [Link]([Link]);
[Link](root).where([Link]( X ));
[Link]( Y ([Link]("fechaNacimiento")));
c) X: [Link]("apellido"), "García" / Y: [Link]: asc nos dará primero las fechas
más antiguas, es decir, los estudiantes más jóvenes. Equals es una búsqueda exacta, así
que es importante no olvidar la tilde ni confundir nombre con apellido.
Soluciones 431
17 Expresiones lambda
y Streams
En este capítulo aprenderás a:
• Aplicar algunos conceptos de programación funcional en tus
programas Java.
• Entender y utilizar las expresiones lambda.
• Utilizar los Streams y sus operaciones para tratar flujos de datos.
Introducción
En la primera parte de este libro aprendimos programación imperativa, estructurada.
En la segunda descubrimos la programación orientada a objetos. En este capítulo veremos
unas nociones de programación funcional. Yo aprendí programación funcional utilizando
los lenguajes Hope y Haskell, y mi proyecto de final de carrera simulaba programación
funcional en Java. Fue en 2003, hasta 2004 no salió Java 5 con las colecciones genéricas.
Imagínate cuánto faltaba para Java 8, que fue cuando se introdujo la programación
funcional en Java, con las expresiones lambda (λ) y el API de Streams.
Dominar la programación funcional es una tarea ardua y extensa, pero conocerla un poco
sí está a nuestro alcance ahora mismo.
Expresiones lambda
Una expresión lambda consiste en:
• una lista de parámetros formales, entre paréntesis y separados por comas si son más
de uno. No es necesario indicar el tipo.
• un operador flecha: ->.
• un cuerpo, con una expresión o un bloque. Si es un bloque, necesitamos llaves y una
sentencia de return.
Por ejemplo:
(a, b) -> a + b;
texto -> {
int tam = [Link]();
return texto + " tiene longitud " + tam;
}
La primera expresión recibe dos elementos y devuelve su suma. La segunda, para un
texto, calcula su longitud y lo devuelve concatenado a " tiene longitud " y el tamaño
calculado.
No es más que un azúcar sintáctico, una forma de expresar algo que ya podíamos hacer,
pero de forma más simple. Y, en efecto, tiene su potencial. Fíjate en que la expresión
lambda se parece a una declaración de un método. Por eso se dice que podemos considerar
que son métodos anónimos, sin nombre.
Calculadora lambda
Lo vemos en un ejemplo: utilizamos expresiones lambda para hacer una calculadora.
• AritmeticaEntera es una interfaz con una operación, que recibe dos enteros y devuelve
un entero.
434 Expresiones lambda y Streams
• El método operacionBinaria recibe dos enteros y una operación, que será una instancia
de la interfaz AritmeticaEntera. Lo que hace es ejecutar esa operación pasándole los
parámetros recibidos.
• La clase Suma implementa AritmeticaEntera y su operación, devolviendo a + b. Esta
sería la forma clásica de hacerlo, sin utilizar expresiones lambda.
• En la primera línea del main, probamos operacionBinaria pasándole dos números y
un nuevo objeto de la clase Suma.
• Pero en la segunda línea ya usamos una expresión lambda, escribiéndola directamente T17.1
como tercer parámetro: (a, b) -> a - b). Parece más cómodo hacerlo así que creando
una clase Resta a imagen de la clase Suma.
• También es posible crear una constante, PRODUCTO, con la expresión lambda
necesaria para multiplicar dos números.
• Sin problema, utilizamos una expresión lambda para la división, equivalente a las demás.
• O para operaciones más complejas como calcular la potencia, que requiere llamar al
método pow de Math y hacerle un casting a entero, porque no nos vale el double que
devuelve.
public class Calculadora {
static final AritmeticaEntera PRODUCTO = (a, b) -> a * b;
interface AritmeticaEntera {
int operacion(int a, int b);
}
public static int operacionBinaria(int x, int y, AritmeticaEntera ae) {
return [Link](x, y);
}
static class Suma implements AritmeticaEntera {
@Override
public int operacion(int a, int b) {
return a + b;
}
}
public static void main(String... args) {
[Link]("6 + 2 = " + operacionBinaria(6, 2, new Suma()));
[Link]("6 - 2 = " + operacionBinaria(6, 2, (a, b) -> a - b));
[Link]("6 * 2 = " + operacionBinaria(6, 2, PRODUCTO));
[Link]("6 / 2 = " + operacionBinaria(6, 2, (a, b) -> a / b));
[Link]("6 ^ 2 = "
+ operacionBinaria(6, 2, (a, b) -> (int) [Link](a, b)));
}
}
El resultado de la ejecución:
6 + 2 = 8
6 - 2 = 4
6 * 2 = 12
6 / 2 = 3
6 ^ 2 = 36
E17.1
Parece que la calculadora funciona.
Expresiones lambda 435
Streams
Otra mejora de Java 8 relacionada con la programación funcional son los streams. Stream
en español sería arroyo, chorro, flujo… Míralo como un flujo de datos que soporta un
agregado de operaciones secuenciales y paralelas. Suena complicado, pero no lo es. La
clase Stream proporciona una serie de métodos que se aplican a todo ese flujo de datos,
a cada uno de sus elementos:
• filter: recibe un predicado y elimina del Stream todos esos elementos que no cumplen
el predicado.
• map: mapea, convierte cada objeto a otro valor, según la función que reciba.
• forEach: aplica la acción recibida a cada elemento.
• count: cuenta el número de elementos del flujo.
• reduce: combina los elementos según la función asociativa de acumulación que recibe.
Hay muchos más métodos, tienes todo un mundo por aprender aquí. Yo te abro la puerta,
tú decides si entras y hasta dónde.
Cuentas varias
• Crearemos una clase con un main a la que le pasaremos varios números como
parámetros e iremos haciendo algunas pruebas con streams. Esta vez usaremos la
sintaxis de los puntos suspensivos para indicar un número indeterminado de
parámetros en vez de String[], pero son sinónimos.
package lambdas;
public class CuentasVarias {
public static void main(String... args) {
}
}
• Para convertir el array de argumentos a Stream, hacemos [Link](args).
NOTA:
Los flujos «se gastan», es imposible recorrerlos varias veces. Por eso, tenemos que repetir la
conversión de los argumentos recibidos en stream para cada uso.
• Pintamos todos los parámetros recibidos, convertimos a Stream y por cada elemento
hacemos un [Link].
[Link]("Parámetros recibidos: ");
[Link](args).forEach(arg -> [Link](arg + " "));
[Link]();
• Contamos cuántos parámetros pares recibimos:
• Tras convertir en stream,
• mapeamos a entero, llamando a [Link] para cada elemento.
436 Expresiones lambda y Streams
• Los filtramos con la expresión lambda que comprueba si son pares
• y los contamos
long contador = [Link](args)
.mapToInt(e -> [Link](e))
.filter(n -> n % 2 == 0).count();
[Link]("Hay " + contador + " parámetros pares");
NOTA:
La sintaxis e -> [Link](e) es muy clara ahora que ya sabemos leer expresiones lambda,
pero las buenas prácticas de Java nos sugieren mejor utilizar la sintaxis Integer::valueOf,
llamada referencia a método, sintaxis también incorporada en Java 8-.
• Ahora sumamos todos los elementos, utilizando el método sum de Stream y mediante
la nueva sintaxis para la conversión a entero.
int suma = [Link](args).mapToInt(Integer::valueOf).sum();
[Link]("Suma: " + suma);
• Podemos encadenar tantas operaciones como necesitemos, así que aprovechemos
para hacer la suma de los cuadrados:
int sumaCuadrados = [Link](args).mapToInt(Integer::valueOf)
.map(n -> n n).sum();
[Link]("Suma de los cuadrados: " + sumaCuadrados);
• O incluso la suma de los cuadrados, pero solo de los números pares:
int sumaCuadradosPares = [Link](args).mapToInt(Integer::valueOf)
.filter(n -> n % 2 == 0).map(n -> n * n).sum(); T17.2
[Link]("Suma de los cuadrados pares: " + sumaCuadradosPares);
El resultado de todo esto es: T17.3
Parámetros recibidos: 1 2 3 4 5 6
Hay 3 parámetros pares
Suma: 21
Suma de los cuadrados: 91 E17.2
Suma de los cuadrados pares: 56
Test y ejercicios
Test 17.1. ¿Cuál sería la traducción correcta de esta clase a expresión lambda?
static class Recalculo implements AritmeticaEntera {
@Override
public int operacion(int a, int b) {
return (a * a + 2 * a * b - b * b);
}
}
a) (a, b) => a * a + 2 * a * b + b * b
b) (a, b) -> a * a + 2 * a * b - b * b
c) {a, b} -> a * a + 2 * a * b - b * b
d) {a, b} => (a * a) + (2 * a * b) - (b * b)
Test y ejercicios 437
Test 17.2. ¿Qué hace esta expresión?
String[] textos = {"hola","adios","ver","casa","cosa","lata",
"loto"};
[Link](textos).filter(s -> [Link]() % 2 == 0).limit(3)
.map(n -> [Link]()).forEach(e -> [Link](e));
a) holacasacosa
b) hola
c) HOLACASACOSA
d) HOLAADIOSVER
Test 17.3. ¿Por qué esta otra devolvería HOLA?
[Link](textos).limit(3).filter(s -> [Link]() % 2 == 0).map(
n -> [Link]()).forEach(e -> [Link](e));
a) Porque tiene el toUpperCase().
b) Porque el límite de 3 se hace antes del filtro por longitud par.
c) No devolvería HOLA, sino HOLAADIOSVER.
d) No devolvería HOLA, sino HOLACASACOSA.
Ejercicio 17.1. Filtrando textos con lambda
Escribe un programa que imprima por pantalla la frase de la lista de argumentos recibidos que son
cadenas largas (de longitud mayor que cinco), que son letras (longitud uno) y que no llevan la letra «a».
Ejercicio 17.2. Filtrando textos con streams
Mismo ejercicio, pero ahora intenta hacerlo directamente con streams.
Soluciones
Test 17.1. ¿Cuál sería la traducción correcta de esta clase a expresión lambda?
b) (a, b) -> a * a + 2 * a * b - b * b: Los parámetros deben ir entre paréntesis
y la flecha se hace con el guion no con el igual.
Test 17.2. ¿Qué hace esta expresión?
c) HOLACASACOSA: Primero filtra por elementos de longitud par, luego coge los tres
primeros, los pasa a mayúsculas y los pinta.
Test 17.3. ¿Por qué esta otra devolvería HOLA?
b) Porque el límite de 3 se hace antes del filtro por longitud par: y entre los tres primeros,
solo el primero tiene longitud par.
Ejercicio 17.1. Filtrando textos con lambda
package lambdas;
import [Link];
public class FiltrandoTextos {
public static void main(String[] args) {
[Link]("Cadenas largas: ");
procesar(args, s -> [Link]() > 5);
438 Expresiones lambda y Streams
[Link]("Letras: ");
procesar(args, s -> [Link]() == 1);
[Link]("Sin a: ");
procesar(args, s -> [Link]('a') == -1);
}
public static void procesar(String[] textos, Predicate<String> predicado) {
for (String n : textos) {
if ([Link](n)) {
[Link](n + " ");
}
}
[Link]();
}
}
El método procesar recibe un array de textos y un predicado, para cada uno de los elementos del
array, y si se cumple el predicado, lo imprime.
Desde el main, llamamos tres veces al método procesar, cambiando los predicados:
• cadenas largas: la longitud es mayor que cinco, s -> [Link]() > 5.
• letras: la longitud es uno, [Link]() == 1.
• sin a: el índice del carácter "a" es -1, es decir, no se encuentra, s -> [Link]('a') == -1.
Si lo ejecutamos con los argumentos abracadabra sol pelo a pila o salida y perdido el resultado es:
Cadenas largas: abracadabra salida perdido
Letras: a o y
Sin a: sol pelo o y perdido
Ejercicio 17.2. Filtrando textos con streams
En este caso la solución es aún más corta:
package lambdas;
import [Link];
public class FiltrandoTextosStreams {
public static void main(String[] args) {
[Link]("Cadenas largas: ");
[Link](args).filter(s -> [Link]() > 5)
.forEach(arg -> [Link](arg + " "));
[Link]();
[Link]("Letras: ");
[Link](args).filter(s -> [Link]() == 1)
.forEach(arg -> [Link](arg + " "));
[Link]();
[Link]("Sin a: ");
[Link](args).filter(s -> [Link]('a') == -1)
.forEach(arg -> [Link](arg + " "));
[Link]();
}
}
En los tres casos, convertimos a stream, filtramos por el predicado oportuno y, por cada uno,
lo pintamos.
Soluciones 439
18 Proyecto
«otra reunión más»
En este capítulo aprenderás a:
• Aplicar lo aprendido en los capítulos anteriores.
• Extraer datos e informes de una base de datos.
• Formatear fechas con formatos personalizados.
• Utilizar un diccionario para organizar datos.
• Usar expresiones lambda con naturalidad y para ordenar datos.
Introducción
Llegamos ya al proyecto de la cuarta y última parte de este libro. Pondremos en
práctica lo aprendido sobre un gestor de reuniones llamado «Otra reunión más».
A partir de una base de datos mapeada por Hibernate con datos sobre las reuniones
pasadas y futuras de una empresa, buscaremos sacar un cartel con las reuniones a
celebrar en un día en una sala determinada y, por cada una de ellas, un informe que
incluya todos los datos disponibles sobre la misma. Completaremos con todos los
informes de las reuniones con acta.
Código de base
En el repositorio de ficheros asociado a esta obra encontrarás el código sobre el que
trabajaremos. Se trata del proyecto otrareunionmas con las entidades Reunion, Sala,
Persona y Acta, sus DAO y las clases estructurales que ya vimos con el gestor de
pedidos, pero adaptadas a este nuevo proyecto: la interfaz Dao y las clases
EntityManagerUtil y AbstractDao, además de los ficheros de configuración [Link]
y [Link]. La clase GeneracionDatos inserta, como su nombre indica, unos
cuantos datos en la base, para poder hacer nuestras pruebas. Ejecútala solo una vez.
Sin embargo, mi recomendación es que intentes implementarlo tú. Si no te sale,
repasa el capítulo 16, pero siempre estarás a tiempo de utilizar los que te proporciono.
En la tabla 18.1 te doy unas pistas para crear las entidades.
Tabla 18.1. Las entidades de «Otra reunión más».
Entidad Reunión Sala Acta Persona
Clave id (entero id (texto) id (entero id (entero autogenerado)
primaria autogenerado) autogenerado)
Atributos fecha (fecha) descripción (texto) contenido (texto) numeroEmpleado (texto único)
asunto (texto) capacidad (entero) nombre (texto)
apellidos (texto)
Cartel de sala
Creamos una nueva clase CartelSala con un método main para preparar un cartel
(en modo texto, sin formato) con el listado de reuniones a celebrar. Tendremos que
preparar un método para recuperar las reuniones a celebrar en una sala y fecha
determinadas.
442 Proyecto «otra reunión más»
Clase ReunionDao
En la clase ReunionDao:
• Añadimos el método getBySalaAndFecha, que recibe la sala y la fecha.
• Creamos una query, utilizando criteria, que filtre por los dos criterios: por sala y
por fecha entre el inicio y el final del día recibido como parámetro, ordenando
por fecha ascendente, para que las primeras reuniones del día queden al principio
del cartel.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class ReunionDao extends AbstractDao<Reunion> {
public ReunionDao() {
setClazz([Link]);
}
public List<Reunion> getBySalaAndFecha(Sala sala, LocalDate fecha) {
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Reunion> criteriaQuery = [Link]([Link]);
Root<Reunion> root = [Link]([Link]);
Predicate predSala = [Link]([Link]("sala"), sala);
Predicate predFecha = [Link]([Link]("fecha"),
[Link](), [Link](1).atStartOfDay());
[Link](root).where([Link](predSala, predFecha));
[Link]([Link]([Link]("fecha")));
Query query = getEntityManager().createQuery(criteriaQuery);
return [Link]();
}
}
Clase CartelSala
• CartelSala recibirá las indicaciones por argumentos. El primer argumento es el
identificador de la sala y el segundo, opcional, la fecha deseada. Si no recibimos
ninguna fecha, usamos la de hoy.
• Utilizamos SalaDao y recuperamos la sala solicitada, controlando si existe.
• Conociendo la sala y la fecha, con ReunionDao recuperamos las reuniones que
necesitamos.
• Imprimimos cabecera con los datos de sala y fecha.
Cartel de sala 443
• Imprimimos cada una de las reuniones, fecha y asunto, formateando la fecha
para que muestre solamente horas y minutos, ya que la fecha del día ya está en
la cabecera.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link]
import [Link];
import [Link];
import [Link];
import [Link];
public class CartelSala {
private static final DateTimeFormatter FORMATO_HORA =
[Link]("HH:mm");
public static void main(String[] args) {
if ([Link] == 0) {
[Link]("Faltan parámetros. Indica id de sala, "
+ " y opcionalmente la fecha deseada (AAAA-MM-DD).");
return;
}
String salaId = args[0];
LocalDate fecha;
if ([Link] >= 2) {
fecha = [Link](args[1]);
} else {
fecha = [Link]();
}
SalaDao salaDao = new SalaDao();
Optional<Sala> optional = [Link](salaId);
if ([Link]()) {
Sala sala = [Link]();
ReunionDao reunionDao = new ReunionDao();
List<Reunion> reuniones = [Link](sala, fecha);
imprimirCabecera(sala, fecha);
imprimirReuniones(reuniones);
} else {
[Link]([Link]("La sala con id {0} no existe",
salaId));
}
}
private static void imprimirCabecera(Sala sala, LocalDate fecha) {
[Link]([Link]("* SALA: {0} ({1})",
[Link](), [Link]()));
[Link]([Link]("* FECHA: {0}", fecha));
}
private static void imprimirReuniones(List<Reunion> reuniones) {
[Link]("Reuniones previstas:");
for (Reunion reunion : reuniones) {
[Link]([Link]("{0}:\t{1}",
[Link]().format(FORMATO_HORA),
[Link]()));
}
}
}
444 Proyecto «otra reunión más»
Cartel para una sala
El resultado en mi caso es este:
* SALA: Reunión primera planta (S101)
* FECHA: 2021-04-20
Reuniones previstas:
10:28: Reunión de las 10
11:28: Reunión de las 11
12:28: Reunión de las 12
13:28: Reunión de las 13
14:28: Reunión de las 14
21:28: Otra Reunión de Test
Informe de reunión
Ahora crearemos un informe para una reunión concreta, que incluya además de sus
datos básicos, el acta y el listado de participantes. Esta vez recogemos el id de la
reunión de la entrada por teclado.
Clase InformeReunion
• Creamos la clase InformeReunion, con su método main.
• Intentamos obtener un Scanner de la entrada estándar.
• Le pedimos al usuario, y recogemos como entero, el id de la reunión.
• Con ReunionDao recuperamos esa reunión concreta, comprobando que existe
antes de trabajar con ella.
• Imprimimos esa reunión:
• Construimos mensajes formateados para la cabecera. Observa especialmente
el formato de fecha tan largo que utilizamos, así como el formateo del número
para evitar que al imprimir el id le aplique formato con separadores.
• Para imprimir los participantes, los recuperamos de la reunión (Hibernate
ya se encargará de ir a base de datos si no tuviera la información a mano).
Por cada uno, imprimimos en formato número de empleado, apellido en
mayúsculas, nombre, con algunos tabuladores para facilitar la lectura.
• Imprimir el acta es más sencillo: solo recuperamos su contenido.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
Informe de reunión 445
public class InformeReunion {
private static final DateTimeFormatter FORMATO_FECHA =
[Link](
"'el' EEEE, dd 'de' MMMM 'de' yyyy 'a las' HH:mm");
private static final String ID_FORMAT = ",number,#";
public static void main(String[] args) {
try (Scanner s = new Scanner([Link])) {
[Link]("Indica el id de la reunión: ");
int reunionId = [Link]();
ReunionDao reunionDao = new ReunionDao();
Optional<Reunion> optional = [Link](reunionId);
if ([Link]()) {
Reunion r = [Link]();
imprimirReunion(r);
} else {
[Link]([Link]("La reunión con id {0"
+ ID_FORMAT + "} no existe", reunionId));
}
}
}
private static void imprimirReunion(Reunion r) {
[Link]([Link](
"Informe de la reunión con asunto \"{0}\" (id {1"
+ ID_FORMAT + "})", [Link](), [Link]()));
[Link]([Link](
"\tcelebrada {0}\n\ten la sala {1} (id {2})",
[Link]().format(FORMATO_FECHA),
[Link]().getDescripcion(), [Link]().getId()));
imprimirParticipantes(r);
imprimirActa(r);
}
private static void imprimirParticipantes(Reunion r) {
[Link]("Participantes:");
Set<Persona> participantes = [Link]();
if ([Link]()) {
[Link]("No hay participantes");
}
for (Persona persona : participantes) {
[Link]([Link]("\t{2}\t{1}, {0}",
[Link](), [Link]().toUpperCase(),
[Link]()));
}
}
private static void imprimirActa(Reunion r) {
Acta a = [Link]();
[Link]("Acta:");
if (a == null) {
[Link]("No hay acta");
} else {
[Link]("\t" + [Link]());
}
}
}
446 Proyecto «otra reunión más»
Informe de reunión inexistente
Si la reunión no existe, el resultado es este:
Indica el id de la reunión:
1234
La reunión con id 1234 no existe
Informe de una reunión
Luce más si pedimos una reunión existente:
Indica el id de la reunión:
10
Informe de la reunión con asunto "Reunión de ayer" (id 10)
celebrada el lunes, 19 de abril de 2021 a las 19:28
en la sala Sala grande (id S203)
Participantes:
E003 PÉREZ PÉREZ, Santi
E002 GÓMEZ FERNÁNDEZ, Pedro
E001 GARCÍA LÓPEZ, Marta
E004 GUTIÉRREZ GONZÁLEZ, Luisa
Acta:
Preparación del lanzamiento de la aplicación "Otra Reunión Más".
Informes de todas las reuniones
La tercera función se parece mucho a la anterior, pero con ciertos matices y, además,
nos permite practicar con diccionarios y con streams.
Sacamos un listado con todas las reuniones con acta que tengan al menos tres
participantes y ordenamos los participantes por número de empleado, que en la
función anterior nos salieron un poco desordenados.
Clase InformeReuniones
• Creamos la clase InformeReuniones, con su método main.
• Con ActaDao recuperamos todas las actas. De esta forma, ya nos aseguramos
de que las reuniones contarán con acta.
• Creamos un diccionario (Map) con clave el objeto Reunion, con valor el informe
generado (un String). Le damos al mapa un tamaño inicial en función del número
de actas, para garantizar que no necesitaremos que se amplíe. Bueno, sinceramente,
lo hago para enseñarte cómo hacerlo, no es que nos resulte muy útil en este
ejemplo.
Informes de todas las reuniones 447
• Convertimos a stream la lista de actas para:
• Primero, filtrar y quedarnos solamente con aquellas con tres o más participantes
(usando una constante para evitar los números mágicos).
• Luego, por cada acta, insertar en el diccionario la clave reunión y el valor
informe, mediante el método informeReunion que analizaremos en detalle
unas líneas más abajo.
• Imprimimos informes, en un método auxiliar que:
• toma las entradas (los pares clave-valor) del diccionario.
• los ordena comparando la fecha de la reunión (que es la clave).
• para cada una de las entradas, imprimimos clave y valor, utilizando una
expresión lambda con más de una instrucción.
Entramos en el detalle de informeReunion, que es una adaptación del del ejemplo
anterior. En lugar de imprimir directamente los textos, devuelve un String para que
pueda ser metido en el diccionario. Utilizamos un StringBuilder para construir la
cadena de textos resultante.
En informeParticipantes también hay que reemplazar el uso de [Link]
por la construcción de un texto. En este caso ordenamos los participantes según su
número de empleado y, por cada uno, usando otra expresión lambda, construimos
la línea número empleado, apellidos en mayúsculas y nombre.
En el método informeActa solamente hemos cambiado la impresión por la construcción
del texto.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class InformeReuniones {
private static final DateTimeFormatter FORMATO_FECHA =
[Link]("'el' EEEE, dd 'de' MMMM 'de' yyyy 'a las' HH:mm");
private static final String ID_FORMAT = ",number,#";
private static final int MIN_PART = 3;
448 Proyecto «otra reunión más»
public static void main(String[] args) {
ActaDao actaDao = new ActaDao();
List<Acta> actas = [Link]();
Map<Reunion, String> informes = new HashMap<>([Link]());
[Link]().filter(
acta -> [Link]().getParticipantes().size() >= MIN_PART
).forEach(
acta -> [Link]([Link](), informeReunion([Link]()))
);
imprimirInformes(informes);
}
private static void imprimirInformes(Map<Reunion, String> informes) {
[Link]().stream().sorted(
[Link](informe -> [Link]().getFecha())
).forEach(
informe -> {
[Link]("== Reunión:\n" + [Link]().getAsunto());
[Link]("== Informe:\n" + [Link]());
}
);
}
private static String informeReunion(Reunion r) {
StringBuilder sb = new StringBuilder([Link](
"Informe de la reunión con asunto \"{0}\" (id {1" + ID_FORMAT + "})\n",
[Link](), [Link]()));
[Link]([Link](
"\tcelebrada {0}\n\ten la sala {1} (id {2})\n",
[Link]().format(FORMATO_FECHA), [Link]().getDescripcion(),
[Link]().getId()));
[Link](informeParticipantes(r));
[Link](informeActa(r));
return [Link]();
}
private static String informeParticipantes(Reunion r) {
StringBuilder sb = new StringBuilder("Participantes:\n");
Set<Persona> participantes = [Link]();
if ([Link]()) {
[Link]("No hay participantes\n");
}
[Link]().sorted(
[Link](Persona::getNumeroEmpleado)).forEach(
persona ->
[Link]([Link]("\t{2}\t{1}, {0}\n",
[Link](),
[Link]().toUpperCase(),
[Link]()))
);
return [Link]();
}
Informes de todas las reuniones 449
private static String informeActa(Reunion r) {
Acta a = [Link]();
StringBuilder sb = new StringBuilder("Acta:\n");
if (a == null) {
[Link]("No hay acta\n");
} else {
[Link]("\t" + [Link]() + "\n");
}
return [Link]();
}
}
Informe de reuniones sin límite de participantes
Si ejecutamos una vez bajando a cero el límite de participantes es posible comprobar el efecto
de ordenar por fecha las reuniones: sale primero la del lunes 19, luego la del martes 20:
== Reunión:
Reunión de ayer
== Informe:
Informe de la reunión con asunto "Reunión de ayer" (id 10)
celebrada el lunes, 19 de abril de 2021 a las 19:28
en la sala Sala grande (id S203)
Participantes:
E001 GARCÍA LÓPEZ, Marta
E002 GÓMEZ FERNÁNDEZ, Pedro
E003 PÉREZ PÉREZ, Santi
E004 GUTIÉRREZ GONZÁLEZ, Luisa
Acta:
Preparación del lanzamiento de la aplicación "Otra Reunión Más".
== Reunión:
Reunión de Test
== Informe:
Informe de la reunión con asunto "Reunión de Test" (id 6)
celebrada el martes, 20 de abril de 2021 a las 19:28
en la sala Trastero (id S099)
Participantes:
E001 GARCÍA LÓPEZ, Marta
Acta:
Marta se reúne sola, solo para descansar un rato
Informe de reuniones con límite de participantes
Ahora vemos que la reunión de Marta consigo misma ya no aparece:
== Reunión:
Reunión de ayer
== Informe:
Informe de la reunión con asunto "Reunión de ayer" (id 10)
celebrada el lunes, 19 de abril de 2021 a las 19:28
en la sala Sala grande (id S203)
Participantes:
E001 GARCÍA LÓPEZ, Marta
E002 GÓMEZ FERNÁNDEZ, Pedro
E003 PÉREZ PÉREZ, Santi
E004 GUTIÉRREZ GONZÁLEZ, Luisa
Acta:
Preparación del lanzamiento de la aplicación "Otra Reunión Más”.
Observa que los participantes ya salen ordenados por número de empleado.
450 Proyecto «otra reunión más»
Mejoras y evoluciones
A partir de estas tres funcionalidades, deja volar tu imaginación y extrae más datos,
aplica técnicas aprendidas a lo largo del libro: añade logs, crea test unitarios, internacionaliza
los mensajes…
Esto es solo el principio
Has llegado al final del libro, ¡enhorabuena! Espero que hayas aprendido mucho y
disfrutado aún más.
Sin embargo, esto es solamente el principio de tu camino en el mundo del desarrollo y la
programación. Si quieres seguir, son infinitos los caminos que se te ofrecen, te voy a listar
algunos temas en los que creo que podrías profundizar. Seguro que me dejo muchos…:
• SQL: Las bases de datos van intrínsecamente unidas a los programas. Saber hacer
consultas es imprescindible.
• HTML y CSS: Son lenguajes, pero no de programación. Se usan para escribir páginas
web. Tener ciertas nociones de ambos te vendrá bien.
• Control de versiones: Primero CVS, luego SVN, ahora git… Van saliendo nuevas
herramientas, pero conocer algún sistema de control de versiones es fundamental
para trabajar en equipo.
• Comunicación cliente servidor: Antiguamente JSP, Servlets, Struts… hoy en día,
microservicios SOAP. Java suele utilizarse como lenguaje de servidor, es importante
saber conectar adecuadamente ambas partes.
• Parte cliente: Si en Java puedes hacer la parte servidor, solo con HTML no podrás
crear la interfaz de usuario: necesitarás aprender otros lenguajes para lograrlo.
• Frameworks: Hibernate, Spring… hay muchos y muy variados.
• Android: Se puede utilizar Java para el desarrollo de aplicaciones para móviles
Android, pero tendrás que familiarizarte con sus librerías y su arquitectura.
Sin olvidar conceptos fundamentales para entender mejor la programación:
• Algoritmia.
• Estructuras de datos.
• Bases de datos.
• Refactorización y calidad de código.
• Arquitectura e ingeniería del software.
¡Feliz camino! ¡Sigue avanzando!
Mejoras y evoluciones 451
í
%, 54, 273
Índice
alfabético
|, 58-59
*, 54, 66 ||, 57, 59
*=, 109 ~, 58
+, 43, 51, 54, 119, 186, 300 ", 35, 44, 49, 69, 105, 326, 360, 384
++, 54 ', 49, 105, 360, 384, 430
+=, 109 …, 362
-, 54, 300 ::, 437, 449
--, 54 ; , 35-37, 51
->, 434, 438-439, 449 @, 185, 400, 260
/, 54 @Column, 400, 421
//, 47 @Deprecated, 370
<, 56 @GeneratedValue, 400
<<, 58 @Entity, 400
<=, 56 @exception, 287-288, 291-292
=, 53, 59 @ExtendWith, 324
==, 56, 59 @JoinColumn, 420
>, 56 @ManyToMany, 421-423
>>, 59 @ManyToOne, 416-417, 429, 431
>>>, 59 @OneToOne, 419, 423
>=, 56, 64 @OneToMany, 417
\n, 104-106, 360 @Override, 159, 184
^, 58, 83, 228, 435 @param, 287-288, 291-292
!, 57, 82, 92, 300 @return, 287-288, 291-292
!=, 56 @Table, 400, 419
&, 58-59 @Test, 309, 325-326
&&, 57, 59 @throws, 265, 292
&=, 190
452 Índice alfabético
A clase, 25-38, 79-80, 84, 86-87, 143, 147-149, 151-155,
abstract, 158, 161, 209, 405 158, 161-162, 165, 167, 172-173, 175-176, 188, 207-209,
ámbito, 28, 53, 266 212-213, 247, 254, 296, 306-314, 331-332, 366,
anomalía, 296, 310 373-374, 399-400, 402, 419
anotación, 159-160, 184, 309, 324, 370, 399-400, 417, clase abstracta, 152, 158-159, 161, 172-173, 179, 188,
419-420, 423, 429, 431 366, 402-403, 405
clase envolvente, 367, 388
apóstrofo, 49, 105-106, 360 clase hija/subclase, 183, 207-209, 252, 255, 369
argumento, 29-30, 37, 42-47, 59, 61-71, 74-75, 84-89, clase madre/padre, 148, 176, 181, 184, 192, 247
93-94, 96, 98-102, 108-109, 111-112, 114-115, 137, class, 27, 31, 154, 161, 208
163-164, 194, 200-201, 236, 254, 258, 260-261, 263,
270-273, 277-278, 296, 311, 355, 362, 374, 408, 436, cliente, 78, 108, 142-145, 148, 192, 216, 218-219, 227,
438-439, 443 236, 313, 333, 391, 395-397, 451
arquetipo, 333, 396 cobertura, 306, 313-315
aserción, 307, 310, 312, 325-327 comentario, 47, 91, 160, 177, 186, 202-203, 265-266,
282-283, 308
asignación, 53, 59, 86
comillas, 35, 43-44, 49, 69, 105-106, 110, 119, 326, 360,
asociación, 147-148, 384, 426, 430
atributo, 37, 43, 65, 84, 86-87, 116, 143, 146-147, 152, compilar/compilación/compilador, 16, 25, 31, 33,
160, 163-165, 169, 172-173, 176, 178, 191, 202, 35, 44, 48, 51-52, 79, 106, 158, 160, 246-247, 265-266,
207-208, 226, 275, 367
254, 400, 427 complejidad, 376-377, 381
autoboxing, 366-367 concepto, 17, 141-145, 148, 217
azúcar sintáctico, 434 condicional, 73-74, 82
consola, 35, 184, 235, 255, 282, 288, 291-292, 302-303,
B 330-332, 335-337, 343-344, 347, 350, 352, 355
boolean, 48-49, 55, 57-58, 74-75, 83, 86, 92, 160, 190, constantes, 73, 78-80, 82, 84-87, 89, 91, 99, 101, 111,
273, 300 114-115, 118, 123-125, 128, 131, 155, 165-166,
break, 81, 91, 134, 276, 279 209-213, 225, 343, 385-386, 394, 435, 448
breakpoint, 296, 298 constructor, 152, 163-165, 168-173, 178-179, 181, 187,
198-199, 210-211, 222, 224, 227, 229-234, 254-255,
bucle, 95-104, 108-112, 114-118, 134-135, 188-190, 196,
257-259, 284, 291, 298, 302, 313, 345, 407-408, 420,
345
422, 429-430
bucle infinito, 201, 227, 424
constructor por defecto, 408, 415, 429-430
byte, 48, 366, 369
convención, 27, 29, 38, 47, 49, 51-52, 79-80, 84, 86,
148, 156, 166, 227, 309
C criteria, 425-426, 443
camelCase, 47, 49, 51, 148
caracteres especiales, 27, 95, 104-105, 179, 333, 360 D
case, 73, 81 DAO, 14-15, 401-403, 405-409, 412, 418, 420, 422-423,
casting, 225, 251, 366, 386, 411, 435 427, 442-445
catch, 246, 261-262, 264-270, 275, 278-283, 285-286, datos, 17, 42, 49, 102, 201, 208, 216, 229, 252-253, 267,
320, 345, 353, 412 285, 322, 331, 333, 357, 359-360, 363, 368, 370, 376,
char, 48-49, 56, 362, 366, 385-386 378-379, 393-407, 409, 412, 414-421, 423, 425-428,
checked exception, 250-252, 256-257, 260, 262, 272, 285 430, 433, 436, 441-443, 445, 451
checksum, 378 debug, 282, 296, 330, 332, 335-339
Java. Curso de programación 453
decremento, 54 final, 79, 208-210, 212-213, 308
default, 73, 81, 91 finally, 246, 267-269, 278-279, 286, 349
deprecated, 369-370, float, 48, 56, 86, 93
diagrama, 10, 141, 146-147, 149, 153, 175, 195-196, 217, for, 17, 26, 96-100, 104, 107-114
247 for each, 96-98, 100, 102, 107, 109-112
diagrama conceptual, 141, 146-147, 153, 217 friendly, 207, 213
diagrama de clases, 217, 247
diagrama de secuencia, 175, 195
diagrama UML, 141, 146-147, 149 G
diccionario, 343-344, 373-374, 379, 383-384, 388-389, genérico, 374, 402
441, 447-448 getter, 222, 227, 400, 415, 418, 420-421
disco, 152, 282, 333, 369, 394
do, 100-102, 107, 109-110, 117
H
double, 48-49, 54, 225, 435
hash, 185, 359, 377-381, 383-384
driver, 395, 399, 404
herencia, 17, 148, 162, 175-177, 183, 208, 237
E
ejecución, 17-18, 28, 39, 44-45, 48, 74, 164, 198-199, 201, I
246-247, 263-264, 267, 275, 277, 286, 291, 296, 298-303, i18n, 364
313, 319, 323, 330, 332, 345, 394, 409, 412, 424, 426 if, 17, 26, 73-78, 80-93, 96, 114-115, 130, 192, 247, 263,
else, 73-75, 77-78, 80-81, 88-89, 92, 114, 263 273, 333, 336
encapsulación, 207 implements, 158, 162
encoding, 289, 293, 390, 398 import, 103
entidad, 393, 399-403, 405-408, 411, 415-419, 421, incremento, 54
425, 427-431, 442 info, 330, 331, 334-336, 338-339, 343, 404
enumerado, 33, 151, 152, 165-166, 188, 219-221, 224, inglés, 35, 47, 55, 74, 82, 108, 248, 296, 311, 332, 342,
226, 229, 231, 256-258, 271 360, 363, 365, 371, 376, 383, 390, 406
error, 31-35, 44, 47, 68, 87, 91, 102, 106, 128-129, instrucción, 17, 31, 49, 51, 53-54, 73-74, 76-77, 81, 83,
158-162, 164, 167, 169, 190, 196, 198, 206, 208, 96-97, 99-100, 448
246-249, 252-259, 262, 266, 271, 273-279, 282, int, 46, 50-51, 56, 97-98, 271
285-286, 291, 300, 310, 314, 319, 322, 330, 333, integración continua, 312
335-339, 345-346, 349-352, 355, 410, 412, 429-430
interfaz, 33, 152, 155-158, 160-162, 167-169, 172-173,
error de compilación, 106, 158, 246-247, 266
207, 222, 227, 253, 361, 376, 394, 402-403, 405, 407,
error de ejecución, 286, 291
434-435, 442
evaluación perezosa, 57
interfaz de usuario, 105, 342, 451
excepción, 17, 44, 90, 245-293, 320-321, 341-343, 345,
internacionalización, 363-365
353, 355, 406, 412
expresión, 53, 55, 57, 74, 86, 320, 434-435, 437-438
extends, 162, 176 J
javac, 16, 31
F java (comando), 16, 31
fatal, 330, 338-339 Java 5, 434
fichero, 18-19, 26-27, 31-33, 36-39, 68-69, 88-89, 103, Java 7, 268, 270
106, 124-125, 152, 154, 184, 207, 252, 255, 267, 282, Java 8, 371, 434, 436-437
331-335, 337, 342-347, 349-352, 355, 364-365, 378, javadoc, 189, 260, 265, 287, 291, 412
390-391, 396-399 JPA, 394, 397-398, 400, 423, 425
fichero de configuración, 398-399, 403, 427 JUnit, 305, 307-313, 320, 323-326, 334, 353
454 Índice alfabético
L operaciones CRUD, 402
l10n, 364 operador, 41-42, 53-59, 83, 368, 434
lambda, 17, 320, 406, 433-435, 437-438, 441, 448 operador ternario, 82-83
length, 43, 48, 65, 116, 361 Optional, 403, 405, 444-446
llaves, 27-30, 51-53, 76-78, 81, 91, 156, 160, 209, 262,
365, 434 P
Locale, 255, 362, 364-365, 368, 370, 390 paquete, 103, 152-154, 158, 165-166, 172-173, 177, 185,
localización, 363-365 207, 212-213, 217, 219-220, 308, 313, 327, 331, 368,
log4j, 329-331, 333-335, 343, 346, 349 400
logger, 331-339, 343-344, 346, 349-351 parámetro, 35, 50-51, 53, 63-64, 83, 102, 155, 160,
logs, 184, 253, 255, 282-283, 330, 334, 337, 451 163, 183-184, 208, 213, 226, 246, 299, 306, 413,
long, 48, 367, 369 429-430, 434, 436, 438
línea de comandos, 30-31, 35, 38-39, 44, 66 paréntesis, 29-30, 35, 51, 55, 65, 81, 87, 97, 99, 108,
110, 112, 115-116, 246, 269, 279, 374, 434, 438
patrones de diseño, 402
M persistencia, 394, 397-398, 400, 403-404, 427-428
main, 25-26, 28-39, 42-44, 46, 50-52, 63, 70, 79, 103, [Link], 323, 334, 396, 442
114, 123, 134, 142, 168, 275, 277, 306, 412 precedencia, 55
maven, 323, 333-334, 395-397, 407 preincremento, 54-55, 388
memoria, 56, 97, 170-171, 182, 190, 248, 369, 394 principio KISS, 307
Mockito, 305, 322-326 prioridad, 330
mocks, 322-323, 325 private, 50, 79, 207, 226, 308, 327
modificador, 50, 175, 207-208, 210, 213 producción, 241, 253, 255, 269, 279-280, 282, 330,
mySQL, 395-396, 398-399, 404 332, 395, 399, 404
mySQL Workbench, 397, 409, 416 programación, 16-17, 21, 26, 42, 46, 66, 199, 369,
método, 25-26, 28-31, 35-39, 41-43, 46, 48-53, 62, 70-71, 376-377, 426, 451
74, 80, 84, 86-87, 89, 104, 142-143, 147, 152, 155-169, programación concurrente, 209
176-177, 183-184, 188, 195-196, 207-209, 246, 253-256, programación estructurada, 17, 26, 123, 137, 155,
258-265, 267-268, 284-285, 296, 298-299, 306-313, 322, 176, 192, 207, 434
330, 332, 337, 353, 355, 361-363, 366, 368, 374, 376, 400, programación funcional, 433-434, 436
402, 405-406, 412, 426, 434, 436, 437 programación imperativa, 434
programación orientada a objetos, 137, 142, 155,
N 184, 196, 394, 399, 434
programación profesional, 82, 108
new, 56, 103, 167, 169-171, 182, 185, 226, 259, 261 protected, 79, 207, 212, 226-227, 308, 327, 353, 410
NullPointerException, 187, 251, 253, 255, 279-280, public, 27, 29, 79, 160, 207
282-285, 289, 292, 413
puntero, 56, 170-171, 182-183, 251
números mágicos, 78, 89, 114, 448
punto de ruptura, 296-298, 302-303
punto y coma, 35-36, 51, 110, 156, 160, 166, 209, 246
O puntos suspensivos, 362, 436
objeto, 26-27, 29, 56, 79, 116, 142, 146, 148, 151-152, 157, 163,
167, 169-171, 176, 182, 184-185, 190-191, 195-196, 201,
207-208, 218, 222, 224, 226, 251, 253, 269, 296, 360-362, Q
373-374, 402, 406, 408, 418-419, 426 query, 252, 376, 394, 405-406, 408, 410-414,
objetos inmutables, 363 425-430, 443
objetos maquetados, 322
Java. Curso de programación 455
R U
relación, UML, 141, 146-149, 175, 195-196, 217
relación 1:1, 416, 419, 429, 431 unboxing, 366-367
relación 1:N, 415-419, 429, 431
relación M:N, 416, 419-421, 423, 429
responsabilidades, 71, 176, 190-191, 193, 196, V
235-236, 269 variable, 17, 27, 41-42, 46-54, 56, 58-59, 63, 67, 70, 74,
return, 50-53, 57, 82, 291, 434 79-82, 84, 86-87, 96-97, 99-100, 102-104, 109, 114-115,
rollback, 406 190, 208-210, 226, 246, 266, 297-300, 311, 332
runtime exception, 250-252, 256-257, 260, 262, 266, void, 28-30, 33, 37, 50, 264, 353
272-274, 277-278, 280, 285, 319, 343, 413
W
S warn, 330, 334-339, 346, 350, 352, 404
Scanner, 95, 102-103, 117, 126, 131, 343, 349-350, 445 while, 98-102, 107-110, 114-117, 134, 344, 346, 350,
servidor, 26, 253, 322, 331, 395-396, 398-399, 428, 430, 451 385-386
setter, 227, 400, 415, 418, 420-421 wrapping classes, 366
short, 48, 366
sobrecarga, 17, 175, 183, 212-213, 381 X
sobrescritura, 17, 175-176, 183-184, 198-199, 201, 208, XML, 331, 398-399
212-213, 255, 258
static, 28-30, 36-37, 50, 79, 208-213, 362
strings, 28-29, 42-43, 46-49, 56, 63, 66-67, 103, 105,
108, 116, 119, 154, 160, 183, 185-186, 223, 251-252,
261, 278, 280-281, 285, 300, 302, 325-326, 333,
360-363, 370, 388, 448
super, 163-164, 179-181, 186, 198-199, 222-225,
229-233, 237, 257-258, 321, 348
switch, 73, 80-82, 90-91, 96, 133, 135, 275-277, 279, 293
T
TDD, 305-306, 325-326
test unitario, 17, 203, 206-207, 241, 290, 305-308, 310,
312-320, 322-324, 326, 341-342, 353, 451
this, 163-164, 189-191, 227, 232, 235-236, 258, 422, 424
throw, 246, 259-261, 263, 265-267, 278-279, 283-284,
286-293
Throwable, 247, 254-255, 257-258, 285-286, 291,
320-321
throws, 246, 260-261, 263, 265-267, 273-275, 278-279,
286-288, 290-293, 320
TODO, 46, 47, 128-129, 132, 136, 160-161, 177, 179,
186, 188, 191-192, 202-203, 223, 308
trace, 331-332, 335-336, 338
trazas, 17, 133, 184, 235, 238, 241, 255, 271, 275-277,
285, 296, 312-313, 329-339, 341-342
try, 246, 261-262, 264-270, 275, 277-280, 282-286, 288,
290, 292, 320, 345, 349, 412
456 Índice alfabético