Apuntes Java
Apuntes Java
Módulo: Programación
Apuntes
Índice
1. Conceptos Básicos ...................................................................................................................................... 4
Introducción......................................................................................................................................................................................... 4
Un programa sencillo.......................................................................................................................................................................... 5
Variables y Tipos de Datos ................................................................................................................................................................. 8
Operadores ........................................................................................................................................................................................ 15
Entrada y Salida por Consola ........................................................................................................................................................... 17
Primer Programa ............................................................................................................................................................................... 19
2. Estructuras de Control: Condiciones....................................................................................................... 20
If…Then…Else ..................................................................................................................................................................................... 20
Switch ................................................................................................................................................................................................. 22
Operador ? ......................................................................................................................................................................................... 24
Operadores Relacionales y Lógicos ................................................................................................................................................. 25
3. Estructuras de Control: Bucles ................................................................................................................ 27
For ....................................................................................................................................................................................................... 27
While ................................................................................................................................................................................................... 28
Do…While ........................................................................................................................................................................................... 30
Sentencias de Salto ........................................................................................................................................................................... 31
Ejemplos de estructuras repetitivas ................................................................................................................................................ 32
4. Cadenas ..................................................................................................................................................... 37
String .................................................................................................................................................................................................. 37
Format ................................................................................................................................................................................................ 41
StringBuilder y StringBuffer ............................................................................................................................................................. 42
5. Funciones .................................................................................................................................................. 45
Paso de Parámetros por Valor ......................................................................................................................................................... 49
6. Clases y Objetos........................................................................................................................................ 50
Conceptos Básicos ............................................................................................................................................................................ 50
Crear una Clase Paso a Paso ............................................................................................................................................................ 53
Modificadores de Acceso.................................................................................................................................................................. 58
Clases Interesantes ........................................................................................................................................................................... 60
Métodos Comunes a Todas las Clases ............................................................................................................................................ 65
7. Array y ArrayList ....................................................................................................................................... 70
Clase Array ......................................................................................................................................................................................... 70
Clase ArrayList ................................................................................................................................................................................... 79
Clase Collections................................................................................................................................................................................ 87
8. Clases y Herencia...................................................................................................................................... 88
Conceptos Básicos ............................................................................................................................................................................ 88
Herencia ............................................................................................................................................................................................. 90
9. Polimorfismo............................................................................................................................................. 98
Tipos de Polimorfismo ...................................................................................................................................................................... 98
Polimorfismo Puro ............................................................................................................................................................................ 98
Sobrecarga de Métodos.................................................................................................................................................................. 101
10. Clases Abstractas e Interfaces ............................................................................................................... 102
Clases Abstractas............................................................................................................................................................................. 102
Interfaces ......................................................................................................................................................................................... 105
11. Paquetes.................................................................................................................................................. 109
12. Interfaz Gráfica de Usuario .................................................................................................................... 112
Introducción..................................................................................................................................................................................... 112
Programación Guiada por Eventos................................................................................................................................................ 112
Creando Una Aplicación Gráfica .................................................................................................................................................... 113
Componentes Swing ....................................................................................................................................................................... 118
Gestión de Eventos ......................................................................................................................................................................... 130
Paneles y Layout Managers............................................................................................................................................................ 133
Gráficos y Animaciones .................................................................................................................................................................. 139
El modelo MVC................................................................................................................................................................................. 141
13. Excepciones............................................................................................................................................. 143
Gestión de Excepciones .................................................................................................................................................................. 143
Throw y Throws ............................................................................................................................................................................... 146
Tipos de Excepciones ...................................................................................................................................................................... 148
Excepciones Frecuentes ................................................................................................................................................................. 149
14. Ficheros ................................................................................................................................................... 150
Entrada/Salida de información. Flujos .......................................................................................................................................... 150
Clases para lectura / escritura de texto ........................................................................................................................................ 152
Clases para Lectura / Escritura de Bytes ....................................................................................................................................... 158
Clase Files ......................................................................................................................................................................................... 160
Acceso aleatorio a ficheros ............................................................................................................................................................ 161
Serialización de objetos .................................................................................................................................................................. 163
Entrada/Salida estándar ................................................................................................................................................................. 164
Clase Properties............................................................................................................................................................................... 165
15. Colecciones ............................................................................................................................................. 167
Interface List: Listas......................................................................................................................................................................... 168
Interface Set: Conjuntos ................................................................................................................................................................. 171
Interface Map: Mapas ..................................................................................................................................................................... 175
Recorrer Colecciones e Iteradores ................................................................................................................................................ 177
Comparable y Comparator............................................................................................................................................................. 180
Últimas Consideraciones ................................................................................................................................................................ 183
16. Orientación a Objetos Avanzada ........................................................................................................... 185
Enumeraciones ................................................................................................................................................................................ 185
Wrapper Classes (Clases Envoltorio) ............................................................................................................................................. 187
Expresiones Regulares .................................................................................................................................................................... 188
Genéricos ......................................................................................................................................................................................... 193
Tipos de Clases Especiales ............................................................................................................................................................. 197
17. Tratamiento XML .................................................................................................................................... 203
Introducción..................................................................................................................................................................................... 203
Procesar Archivos XML ................................................................................................................................................................... 204
Leer el árbol DOM ........................................................................................................................................................................... 206
Modificar el Árbol DOM .................................................................................................................................................................. 209
Pasar árbol DOM a documento XML ............................................................................................................................................. 212
18. Acceso a Base de Datos ......................................................................................................................... 213
Introducción..................................................................................................................................................................................... 213
Conexión - Desconexión ................................................................................................................................................................. 213
Operaciones ..................................................................................................................................................................................... 216
Patrón DAO / Repository ................................................................................................................................................................ 222
SQLite ............................................................................................................................................................................................... 225
Otros Temas Interesantes .............................................................................................................................................................. 226
Instalación y uso de MySQL............................................................................................................................................................ 228
19. Programación Funcional ........................................................................................................................ 235
Interfaces Funcionales .................................................................................................................................................................... 235
Funciones Lambda .......................................................................................................................................................................... 239
API Stream........................................................................................................................................................................................ 242
Tratar Ficheros como Streams ....................................................................................................................................................... 245
Últimas Consideraciones ................................................................................................................................ 246
Anexos ............................................................................................................................................................. 247
Instalación y Toma de Contacto con NetBeans............................................................................................................................ 247
Diferencias Tipos primitivos vs. Objetos ....................................................................................................................................... 251
Conversiones entre tipos/clases .................................................................................................................................................... 252
Proyecto Lombok ............................................................................................................................................................................ 253
Enlaces de Interés ........................................................................................................................................................................... 255
1. Conceptos Básicos
Introducción
Java es un lenguaje de programación de propósito general, concurrente, orientado a objetos, que fue
diseñado específicamente para tener tan pocas dependencias de implementación como fuera
posible. Su intención es permitir que los desarrolladores de aplicaciones escriban el programa una
vez y lo ejecuten en cualquier dispositivo (conocido en inglés como WORA, o "write once, run
anywhere"), lo que quiere decir que el código que es ejecutado en una plataforma no tiene que ser
recompilado para correr en otra.
Las aplicaciones de Java son compiladas a bytecode (clase Java, no ejecutable directamente), que
puede ejecutarse en cualquier máquina virtual Java (JVM) sin importar la arquitectura de la
computadora subyacente. Si el compilador encuentra algún error en nuestro código nos mostrará
un mensaje y si todo está bien el compilador nos creará un archivo con código byte .class, es este
archivo el que será ejecutado por la JVM. Lo que tiene que hacer el hardware (+ sistema operativo)
es disponer desarrollar una JVM, así podrá ejecutar el programa “semicompilado” .class. Esto hace
que el mismo código pueda ser ejecutado en un ordenador Linux, Windows, etc.
Java al ser un lenguaje derivado de C tiene una sintaxis muy similar a este. Existen tres versiones de
Java según la plataforma a la que va dirigido: Java SE (Standard Edition) ¸ Java EE (Enterprise Edition)
orientada a aplicaciones en red y web y Java ME (Micro Edition) orientada a dispositivos pequeños
como tabletas y móviles.
Existen múltiples razones por las que es bueno aprender a programar en Java, a continuación, te
mencionamos algunas.
• Está dentro de los lenguajes más usados en la actualidad y corre en casi todas las plataformas
que hay en el mercado. Es el más solicitado en las empresas de trabajo.
• Si quieres comenzar a desarrollar en Android, Java es una base importante que necesitas y
debes de aprender.
• Existe gran soporte, documentación y comunidades de Java a las cuales podrás acudir si
necesitas ayuda para entender mejor el lenguaje.
• Java también cuenta con una serie de librerías (nativas y de terceros) que amplían sus
funcionalidades, desde manipular archivos de Office hasta reconocer huellas digitales y
mucho más.
• Java no es un lenguaje complicado como se podría pensar, ya que es un tipo de programación
orientada a objetos, comprendiendo aspectos básicos de este tipo de programación el
aprendizaje de Java será de manera intuitiva.
• Es seguro, JVM nos ofrece mucha seguridad frente a infiltraciones de terceros o virus.
• Java está diseñado para crear software altamente robusto y fiable, para ello proporciona
numerosas comprobaciones durante la compilación y en tiempo de ejecución.
Un programa sencillo
Comencemos viendo un primer programa, aunque no sepamos muy bien el significado de sus
instrucciones ni cuál es su finalidad, nos servirá como primer contacto.
/* Este es un programa de ejemplo.
Nuestro primer contacto con Java.
Curso de Programación
*/
package ejemplo;
import java.util.Scanner;
Espacios en blanco
En Java no es necesario seguir reglas especiales de indentación. Por ejemplo, el programa anterior
se podría haber escrito en una línea o todas las líneas alineadas a la izquierda, siempre y cuando
hubiera un carácter de espacio en blanco entre cada elemento que no estuviera ya delimitado por
un operador o separador. En Java un espacio, tabulador o línea nueva son un espacio en blanco.
Las líneas de código se limitan con un punto y coma final salvo en las estructuras de bloque que
llevan llaves de apertura o cierre.
Identificadores
Los identificadores se utilizan para los nombres de las variables, clases y de los métodos. Un
identificador puede ser cualquier secuencia descriptiva de letras mayúsculas o minúsculas, números,
el carácter de subrayado, o el símbolo del dólar. Un identificador no debe empezar nunca con un
número, para evitar la confusión con un literal numérico.
Conviene recordar otra vez que Java distingue entre mayúsculas y minúsculas; así, el identificador
Resul no es lo mismo que el identificador RESUL o resul.
Cuando definimos un identificador le ponemos justo antes de su nombre el tipo de dato que tiene,
por ejemplo: int num representa una variable de tipo entero como veremos más adelante.
Comentarios
Podemos poner los comentarios que queramos en nuestros programas, los de varias líneas, que
comienzan por /* en la primera línea y terminan por */ en la última, o los de una sola línea, que se
representan por // de forma que todo lo que haya a partir de estos caracteres hasta el final de la
línea no se tendrá en cuenta.
//comentario de una sola línea
/* Comentario
de varias
líneas */
Ejecución de programas
Compilando el programa
En general, se utilizarán entornos integrados de desarrollo (IDE), como pueden ser Netbeans o
Eclipse, que ofrecen multitud de funcionalidades que facilitan el desarrollo y prueba de aplicaciones.
Sin embargo, siendo puristas, compilar el programa Ejemplo simplemente necesitamos el
compilador, javac, que invocaremos desde la consola especificando el nombre del archivo fuente, tal
y como se muestra a continuación.
C:\>javac Ejemplo.java
El compilador javac crea un archivo llamado Ejemplo.class, que contiene la versión del programa en
bytecode. El bytecode es la representación intermedia del programa que contiene las instrucciones
que el intérprete o máquina virtual de Java ejecutará. Por tanto, el resultado de la compilación con
javac no es un código ejecutable para una determinada plataforma, se podrá ejecutar en cualquiera
que disponga de la máquina virtual Java (con el comando java):
C:\>java Ejemplo
La máquina virtual ejecutará las instrucciones que se encuentre en el bloque main y desde ahí podrá
ejecutar instrucciones que estén en otros bloques de ese mismo archivo e o en otros archivos.
Enteros
Java define cuatro tipos de enteros: byte, short, int y long. En todos ellos se considera el signo, valores
positivos y negativos. Java no admite valores sin signo. Muchos otros lenguajes soportan enteros con
signo y enteros sin signo, pero Java no.
El tamaño y rango de estos tipos enteros puede variar mucho, según se muestra en la tabla siguiente:
Los valores literales se pueden representar en base decimal: 1, 7, -34 pero existen otras dos bases
que se pueden utilizar para literales enteros, la octal (base 8) y la hexadecimal (base 16). En Java se
indica que un valor es octal porque va precedido por un 0. Por lo tanto, el valor aparentemente válido
09 producirá un error de compilación, ya que 9 no pertenece al conjunto de dígitos utilizados en base
8 que van de 0 a 7.
Una base más utilizada por los programadores es la hexadecimal, que corresponde claramente con
las palabras de tamaño de módulo 8 tales como las de 8, 16, 32 y 64 bits. Una constante hexadecimal
se denota precediéndola por un cero-x (0x o 0X). Los dígitos que se utilizan en base hexadecimal son
del 0 al 9, y las letras de la ‘A’ a la ‘F’ (o de la ‘a’ a la ‘f’), que sustituyen a los números del 10 al 15.
¿Es posible asignar un literal entero a alguno de los otros tipos de enteros de Java byte o long, sin
que se produzca un error de incompatibilidad? La respuesta es sí: cuando se asigna una literal a una
variable del tipo byte o short, no se genera ningún error si el valor literal está dentro del rango del
tipo de la variable.
También se puede añadir una “L” mayúscula al final del literal para definir que es long, por ejemplo:
x = 234123333L.
-78 // tipo int, dígitos sin punto decimal
034 // en octal (equivale al 28 decimal)
0x1C // en hexadecimal (equivale al 28 decimal)
875L // de tipo long
Hay que tener cuidado de elegir la variable adecuada para no asignarle valores por fuera de su rango;
esta incidencia se denomina desbordamiento u overflow.
Los valores usan el punto como separador decimal, por ejemplo: 3.1416. También admite notación
científica, con un sufijo que especifica la potencia de 10 por la que hay que multiplicar el número. El
exponente se indica mediante una E o e seguida de un número decimal, que puede ser positivo o
negativo; por ejemplo, 6.022E23, 3.14159E-05, y 2E+100.
Los números de punto flotante utilizan por omisión la precisión double. Para especificar un literal
de tipo float se debe añadir una F o f a la constante. También se puede especificar explícitamente un
literal de tipo double añadiendo una D o d (aunque este último es redundante, ya que por defecto
los literales con decimales se crean como double.)
15.2 // de tipo double
1.52e1 // el mismo valor
15.8f // de tipo float = 15.8F
Caracteres
El tipo de datos que se utiliza en Java para almacenar un solo carácter es char. A diferencia de C/C++
que un char ocupa un solo byte, en Java ocupa 2 bytes ya que usa codificación Unicode para
representar caracteres.
Unicode define un conjunto completo e internacional de caracteres que permite la representación
de todos los caracteres que se pueden encontrar en todas las lenguas del mundo. Para ello son
necesarios 16 bits. Por este motivo, el tipo char de Java es un tipo de 16 bits. El rango de un char es
de 0 a 65,536. No existen valores de tipo char negativos.
El conjunto estándar de caracteres conocido como ASCII tiene un rango que va de 0 a 127 caracteres,
y el conjunto extendido de 8 bits, ISOLatin-l, va desde 0 a 255. Java utiliza Unicode para representar
caracteres, ya que está diseñado para crear aplicaciones que puedan ser utilizadas en todo el mundo.
Un literal de carácter se representa dentro de una pareja de comillas simples, como, por ejemplo, ‘a’,
‘z’, y ‘@’. Para los caracteres que resulta imposible introducir directamente, existen varias secuencias
de escape que permiten introducir al carácter deseado como ‘\’’ para el propio carácter de comilla
simple, y ‘\n’ para el carácter de línea nueva. También se puede introducir el valor de un carácter en
base hexadecimal (y octal) para ello se escribe la diagonal invertida seguida de una u (\u), y
exactamente cuatro dígitos hexadecimales. Por ejemplo, ‘\u0061’ es el carácter ISO-Latin-1 ‘a’, ya que
el bit superior es cero. ‘\ua432’ es un carácter japonés.
Secuencia de
Descripción
escape
\ddd Carácter escrito en base octal (ddd)
\uxxxx Carácter escrito utilizando su valor Unicode en hexadecimal (xxxx)
\’ Comilla simple
\” Comilla doble
\\ Barra invertida (backslash)
\r Retorno de carro
\n Nueva línea o salto de línea
\f Comienzo de página
\t Tabulador
\b Retroceso
Los textos los guardaremos en variables de tipo “String”, pero son un tipo de datos
diferentes. Los que estamos viendo hasta ahora son los tipos “primitivos”. String y
otros tipos de datos son ya referencias a “objetos”
Boolean
Una variable de tipo boolean solo puede tener dos valores, true o false. Éste es el tipo que devuelven
los operadores lógicos tales como edad > 18. Es el tipo requerido por las expresiones condicionales
que gobiernan las sentencias de control, como if y for, que veremos más adelante.
Variables
La variable es la unidad básica de almacenamiento en un programa en cualquier lenguaje de
programación. Una variable se define mediante la combinación de un identificador, un tipo y un
inicializador opcional.
En Java, se deben declarar todas las variables antes de utilizarlas. La forma básica de declaración de
una variable es la siguiente:
tipo identificador [ = valor][, identificador [= valor] ...];
• El tipo es uno de los tipos primitivos de Java, como, por ejemplo: int, float, double (o el nombre
de una clase o interfaz, pero eso lo veremos más adelante).
• El identificador es el nombre de la variable. Una convención, aunque no obligatoria, es
empezar la variable por una minúscula y luego emplear la mayúscula para indicar un cambio
de palabra en la variable, ejemplos: edadAlumno, dniPersona, fechaNacimiento, etc.
• Opcionalmente, se puede inicializar la variable mediante un signo igual seguido de un valor.
La expresión a la derecha del igual debe dar como resultado un valor del mismo tipo (o de un
tipo compatible) que el especificado para la variable. Para declarar más de una variable del
tipo especificado, se utiliza una lista con los elementos separados por comas.
int a, A, edad; // declara tres variables de enteros: a, A, y edad
int d = 3, e, f1 = 5; // tres enteros más, inicializando d y f1
byte z = 22; // inicializa z.
double pi = 3.14159; // declara una aproximación de PI
char x = 'x'; // la variable x tiene el valor ‘x’
También se puede inicializar mediante un cálculo:
int a = 3, b = 2;
int c = a + b;
Es importante destacar que el símbolo “=” se usará como asignación de variables no como la igualdad
matemática. Así pues: edad = edad +1 tiene sentido e indica que a la variable edad se le asigna lo
que hay a la derecha del igual, que es una suma. Si, por ejemplo, edad valiese 18, después de ejecutar
esa instrucción valdría 19. La igualdad matemática se representa, como veremos más adelante por
“==”, así pues, edad == edad +1 se evaluaría como false independientemente del valor de edad.
Desde Java 17 se pueden definir variables asignándoles un valor y sin especificar explícitamente el
tipo y Java lo infiere del valor asignado, pero aún así es un tipo fijo que no puede cambiar
posteriormente en el programa. Por eso se dice que Java es fuertemente tipado (a diferencia de
otros lenguajes débilmente tipados como JavaScript). Simplemente es una forma más sencilla de
escribirilo: var x = 10L; crearía x de tipo long.
Las variables se pueden declarar en cualquier punto el programa, pero sólo son válidas después de
ser declaradas. Obviamente, no se puede usar una variable en cualquier operación antes de ser
definida, ya que no sabría el tipo de datos que tiene.
euros = 15.37;
dolares = euros * tasaCambio;
System.out.println (dolares);
Constantes
Una constante es una variable cuyo valor no cambia durante la ejecución del programa, esto es, una
vez que a una constante se le asigna un valor, este no podrá ser modificado y permanecerá así
durante toda la ejecución del programa. Un ejemplo típico podría ser el número PI. Para convertir
una variable en constante, simplemente se le añade el modificador final antes de su tipo. Ejemplo:
final double PI = 3.141592;
Existe la convención de que las constantes o variables finales se escriban con todas sus
letras en mayúsculas.
En realidad, el número Pi ya lo tenemos definido en Java, como Math.PI. Por lo que no
haría falta crear esta variable.
origen, claro). Por ejemplo, siempre es posible asignar un valor del tipo int a una variable del tipo
long. Sin embargo, no todos los tipos son compatibles, y, por lo tanto, no cualquier conversión está
permitida implícitamente. Por ejemplo, la conversión de int a short no está definida.
Pero se puede obtener una conversión entre tipos incompatibles. Para ello, se debe usar un cast,
que realiza una conversión explícita entre tipos. Para ello, en la asignación se especifica entre
paréntesis, después del igual, el tipo destino. Puede que ocasione pérdidas de información, por
ejemplo, al pasar un double a int perdemos los decimales. En el ejemplo siguiente resul valdría 3.
Si la suma se calculase con tipo byte (máx: 127) se hubiese producido un overflow, pero no se
produce porque Java convierte al tipo destino el resultado de la operación. Aun así, es mejor prevenir
estos problemas. Si resul fuese de tipo byte si habría error.
Cadenas
En muchos lenguajes, incluyendo C/C++, las cadenas se implementan como listas (o Arrays) de
caracteres. Sin embargo, éste no es el caso en Java. Las cadenas son realmente un tipo de objetos
denominado String. Como se verá posteriormente, incluye un extensivo conjunto de facilidades para
manejo de cadenas que son, a la vez, potentes y fáciles de manejar. Podemos adelantar por ahora
que podríamos definirlas como un tipo primitivo:
String str2 = "Los Strings son objetos";
Los literales de cadena se encierran en comillas dobles y pueden incluir secuencias de escape como
las los char.
Operadores
Operadores aritméticos
Estos son los operadores aritméticos:
+ Suma - Resta (también es el menos unario)
* Multiplicación / División
++ Incremento –– Decremento
+= Suma y asignación –= Resta y asignación
*= Multiplicación y asignación /= División y asignación
% Módulo (resto) %= Módulo y asignación
Ejemplos, suponiendo que “a” es un entero con valor 13 y “b” un entero con valor 3 y “c” un float:
c = a + b; c tomaría el valor 16
a++; a tomaría el valor 14 (igual a: a=a+1; )
a=a+5; a tomaría el valor 18
a+=5; a tomaría el valor 18 (igual a la anterior)
c = a % b; (*) c tomaría el valor 1
c = a / b c tomaría el valor 4 (ojo: aunque c es float)
c = (float) a / b c tomaría el valor 4.3333335
El operador de asignación
Ya hemos hablado de él previamente “=”. No confundir con “==” que es la igualdad matemática. A la
izquierda del igual siempre tendremos una sola variable (definida en ese momento o previamente)
y a la derecha un valor o un cálculo evaluable (siempre de tipo compatible).
Existe una forma abreviada:
int a, b, c;
a = b = c = 10; //asigna a las tres variables el valor 10.
Veamos un ejemplo del uso del operador divisor y módulo. El siguiente ejemplo toma un
importe en euros y lo descompone en los billetes de 20 euros, 5 euros y monedas de 1 euro
correspondientes.
public class Ejemplo {
public static void main(String[] args) {
int billete5, billete20;
int importe, importeRestante;
importe = 133;
billete20 = importe / 20;
importeRestante = importe % 20;
billete5 = importeRestante / 5;
importeRestante = importeRestante % 5;
Si la variable importe e importeRestante las definimos con long, nos obligaría a hacer un casting en
las líneas 12 y 15. Compruébalo tú mismo.
billete20 = (int) (importe / 20);
En el caso de trabajar con variables, está última solución, el casting, es la única válida:
double res= (double) x / y; o bien
double res= x / (double) y; pero nunca: double res= (double) (x/y);
En cambio, x=z*5/2; comienza multiplicando ‘z’ por 5, y al ser ‘z’ de tipo double produce un
double y al dividirlo por 2 sigue siendo double. Esta segunda opción es la correcta, pero la primera
puede producir errores que pasan desapercibidos.
La diferencia entre print y println radica en que println hace un salto de línea al final del
mensaje y print no, por lo que los siguientes mensajes seguirían saliendo en la misma línea. Existe
un tercer método: printf que no veremos por ahora.
Para solicitar que el usuario introduzca un valor por teclado, necesitamos hacer dos pasos previos,
primero, importar la clase necesaria para emplear dichas funciones.
import java.util.Scanner; //antes del nombre del programa
A continuación, debemos crear un objeto de tipo Scanner:
Scanner teclado = new Scanner (System.in);
A partir de este momento podemos solicitar distintos tipos de datos:
String texto = teclado.nextLine();
int edad =teclado.nextInt(); // nextLong();nextFloat();nextDouble();
char resp = teclado.nextLine().charAt(0);
Para especificar juego de caracteres en Scanner, por ejemplo, para evitar problemas con las eñes
sería así:
new Scanner(System.in, "ISO-8859-1");
En temas posteriores se tratará en detalle la instrucción import , pero por ahora solo tenemos
que quedarnos con la idea de que Java tiene definidas multitud de clases útiles que podemos
emplear pero que pueden estar en distintos contenedores a los que llamamos paquetes. Para
usar clases de otros paquetes hay que importarlas; bien importar la clase sola:
import java.util.Scanner;
bien importar toda el paquete, con todas sus clases:
import java.util.*;
Aunque siempre será mejor la primera opción, importar solo las clases necesarias.
En Netbeans, sobre el código, botón derecho >> Fix Imports, nos incorpora todos
los imports necesarios.
Por último, hay ocasiones en las que queremos que el usuario pulse la tecla <ENTER> para que el
programa siga su ejecución. En este caso no valen las instrucciones anteriores, ya que esperarán que
se introduzca al menos un carácter y luego <ENTER>. La solución sería:
System.out.println("Pulse <ENTER> para continuar");
try {System.in.read();} catch( Exception e ) {}
Más adelante, explicaremos esa estructura “try…catch” pero está relacionada con las excepciones o
errores en tiempo de ejecución, para evitar que el programa “rompa” por meter datos incorrectos.
Aunque en el código de los programas el separador decimal es el punto, por ejemplo:
3.1416 cuando el usuario introduce un valor por consola con nextFloat, nextDouble debe
usar la coma, por ejemplo: 3,1416. Esto es debido a que por defecto se usa la
configuración local de España, introduciríamos punto si definiésemos Scanner de modo
“americano”: Scanner teclado = new Scanner(System.in).useLocale(java.util.Locale.US);
El inconveniente de esta situación es que tenemos que recordar esta situación particular, es
decir, no hay que hacerlo siempre, solo después de valores numéricos.
2) Leer siempre con nextLine() y convertir al tipo de dato que queramos (entero, doble, etc.)
mediante utilidades que nos ofrece el lenguaje.
float f = Float.parseFloat(teclado.nextLine());
int i = Integer.parseInt(teclado.nextLine());
String texto = teclado.nextLine();
Primer Programa
Ahora, sabiendo como pedir información al usuario, como hacer cálculos, y como mostrar datos por
pantalla, estamos en condiciones de hacer nuestros primeros programas.
El siguiente ejemplo pide al usuario que introduzca dos números enteros y los sume:
Es importante analizar el programa con calma, viendo el orden en el que se ejecutan las
instrucciones:
• Línea 1: son comentarios, no se tienen en cuenta
• Línea 2: los programas se agrupan en paquetes dentro de un proyecto, sería un concepto
similar al de las carpetas para agrupar archivos en nuestro ordenador.
• Línea 4: veremos más adelante que representa esta instrucción.
• Línea 5: nombre del programa. Tiene que coincidir con el nombre del archivo .java, es decir
el archivo se ha de llamar Ejemplo.java.
• Línea 7: Comienzo del bloque de código que se ejecutará, es el programa en sí.
• Línea 8: Se crean las variables que se van a necesitar, en este caso todas de tipo entero.
• Línea 10: Creamos un “objeto” teclado para pedir datos por la consola.
• Línea 12 a 15: Pedimos los dos valores al usuario y los almacenamos en sendas variables. Si
no introduce números enteros, se produce un error de ejecución y el programa se para
abruptamente.
• Línea 16: Calculamos la suma y lo almacenamos en otra variable.
• Línea 18: Mostramos el resultado por pantalla.
Para probar todos los fragmentos de código de estos primeros capítulos puedes utilizar el
programa anterior como plantilla y cambiar solo el contenido que hay entre la línea 8 y la 18.
If…Then…Else
Se utiliza para dirigir la ejecución del programa hacia dos caminos diferentes. El formato general de
la sentencia if es:
if (condición) sentencia1; else sentencia2;
Ejemplo:
if (temperatura > 25) System.out.println("A la playa!!!");
else System.out.println("A la montaña!!!");
La condición tiene que ser una expresión evaluable cuyo resultado final sea verdadero o falso, son
operadores típicos: <, <=, >, >=, ==,!= (menor, menor o igual, mayor, mayor o igual, igual, distinto,
respectivamente).
El operador ! representa la negación, por eso == representa igual a y != distinto de.
Tanto sentencia1 como sentencia2 puede ser cualquier instrucción del lenguaje y puede ser una
sentencia única o un conjunto de sentencias encerradas entre llaves, es decir, un bloque. La
condición es cualquier expresión que devuelva un valor booleano. La cláusula else (con la sentencia2)
es opcional.
La sentencia if funciona del siguiente modo: Si la condición es verdadera, se ejecuta la sentencia1.
En caso contrario se ejecuta la sentencia2 (si es que existe). En ningún caso se ejecutarán ambas
sentencias.
Es una buena práctica de programación utilizar las llaves de bloque en las sentencias del if aunque
solo haya una única instrucción y no sea obligatorio. Así, si a posteriori hay que añadir nuevas
instrucciones evitaremos que se nos olvide y produzca resultados inesperados. Ejemplo:
int a=1, b=0,c=3;
if (a > 0) b=1;
else b=2; c=4;
¿Qué valor tendrá c después de la ejecución de este fragmento de código?
Si en la condición, trabajamos con una variable boolean ya no es necesario incluir la igualdad a true:
boolean expr;
if (expr == true) //es lo mismo que: if (expr)
if (expr == false) //es lo mismo que: if (!expr)
if anidados
Un if anidado es una sentencia if que está contenida dentro de otro if o else. Cuando se anidan if lo
más importante es recordar que una sentencia else siempre corresponde a la sentencia if más
próxima dentro del mismo bloque y que no esté ya asociada con otro else y que la última llave de
bloque que abrimos, se corresponde con la primera que cerramos, la segunda con la penúltima, etc.
(tal y como ocurre en matemáticas con los paréntesis). Veamos un ejemplo:
Scanner teclado = new Scanner(System.in);
System.out.println("¿Cuántos años tienes? ");
int edad = teclado.nextInt();
if (edad >= 18) (*)
System.out.println ("Eres mayor de edad");
else {
if (edad >= 16) {
System.out.println("Eres menor de edad");
System.out.println("pero puedes trabajar");
}
else (*)
System.out.println("Sigue estudiando, aun eres un niño");
}
(*) Cuando solo hay una instrucción en el if (o en el else) no hacen falta llaves.
if-else-if múltiples
Una construcción muy habitual en programación es la de if-else-if múltiples. Esta construcción se
basa en una secuencia de if anidados. Su formato es el siguiente:
if (condición) sentencia;
else if (condición) sentencia;
else if (condición) sentencia;
else
sentencia;
La sentencia if se ejecuta de arriba abajo. Tan pronto como una de las condiciones que controlan el
if sea true, las sentencias asociadas con ese if serán ejecutadas, y el resto ignoradas. Si ninguna de
las condiciones es verdadera, entonces se ejecutará el else final. El else final actúa como una
condición por omisión, es decir, si todas las demás pruebas condicionales fallan, entonces se
ejecutará la sentencia del último else. Si no hubiera un else final y todas las demás condiciones fueran
false, entonces no se ejecutará inguna acción.
Este programa calcula un descuento en función del importe proporcionado.
float importe = 2390;
if (importe < 1000) System.out.println ("NO HAY DESCUENTO");
else if (importe < 3000) // (importe<3000 y importe>=1000)
System.out.println ("DESCUENTO: 3%: " + importe * 0.03);
else if (importe < 5000) // (importe<5000 y importe>=3000)
System.out.println ("DESCUENTO: 5%: " + importe * 0.05);
else // (importe >= 5000)
System.out.println ("DESCUENTO: 7%: " + importe * 0.07);
// no existen las palabras reservadas elseif, elif, elsif, ni equivalentes
Es frecuente ver operaciones de incremento en un if, por ejemplo, supón que i es una
variable de tipo entero que vale 10. ¿Qué diferencia habría entre if (++a>10) y if
(a++>10) ? Compruébalo.
Switch
La sentencia switch es una sentencia de bifurcación múltiple. En muchas ocasiones, es una mejor
alternativa que una larga serie de sentencias if-else-if. El formato general de una sentencia switch es:
switch (expresión) {
case valorl: // secuencia de sentencias; break;
case valor2: // secuencia de sentencias; break;
. . .
case valorN: // secuencia de sentencias; break;
default: // secuencia de sentencias por omisión
}
La expresión debe ser del tipo byte, short, int o char (desde Java7 también String); cada uno de los
valores especificados en las sentencias case debe ser de un tipo compatible con el de la expresión.
Cada uno de estos valores debe ser un literal único, es decir, una constante no una variable.
La sentencia switch funciona de la siguiente forma: se compara el valor de la expresión con cada uno
de los valores constantes que aparecen en las sentencias case. Si coincide con alguno, se ejecuta el
código que sigue a la sentencia case. Si ninguna de las constantes coincide con el valor de la
expresión, entonces se ejecuta la sentencia default. Sin embargo, la sentencia default es opcional.
Si ningún case coincide y no existe la sentencia default, no se ejecuta ninguna acción.
La sentencia break se utiliza dentro del switch para terminar una secuencia de sentencias. Cuando
aparece una sentencia break, la ejecución del código se desplaza hasta la siguiente instrucción
después del bloque switch.
Este ejemplo proporciona el nombre del mes en texto a partir del número de mes:
public class Ejemplo {
public static void main (String args[]) {
int numMes = 7;
switch (numMes) {
case 1 : System.out.println("Enero"); break;
case 2 : System.out.println("Febrero"); break;
case 3 : System.out.println("Marzo"); break;
case 4 : System.out.println("Abril"); break;
case 5 : System.out.println("Mayo"); break;
case 6 : System.out.println("Junio"); break;
case 7 : System.out.println("Julio"); break;
case 8 : System.out.println("Agosto"); break;
case 9 : System.out.println("Septiembre"); break;
case 10: System.out.println("Octubre"); break;
case 11: System.out.println("Noviembre"); break;
case 12: System.out.println("Diciembre"); break;
default: System.out.println("Mes erróneo");
}
}
}
Si omitimos el break en un case: la ejecución continuaría por los siguiente case. Olvidárnosla puede
provocar errores en el comportamiento del programa, pero también pue ser útil para agrupar
distintos case que tengan las mismas instrucciones asociadas.
Este programa calcula los días que tiene los meses del año, salvo para años bisiestos.
int numDias=0, numMes = 5;
switch (numMes) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12: numDias = 31; break;
case 2: numDias = 28; break;
case 4:
case 6:
case 9:
case 11: numDias = 30; break;
}
System.out.println(numMes + " -> " + numDias + " días");
Otra funcionalidad que nos ofrece switch es que devuelva implícitamente un valor y se lo asignemos
a una variable. Para lograr esto la sintaxis será variable = switch {. . .};. lDentro del switch,
para devolver el valor, cada case incluirá una instrucción: yield valor, y no será necesario el break.
Se ve más claro con un ejemplo. El código anterior podría haberse implementado así:
int numDias = switch (numMes) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12: yield 31;
case 2: yield 28;
case 4:
case 6:
case 9:
case 11: yield 30;
default: yield 0;
};
Hay que destacar que, con este formato, los ‘case’ tienen que cubrir todos los casos posibles, lo que
obligará a incluir la cláusula ‘default’ en la mayor parte de los casos.
Switch ->
Desde Java 14, existe una nueva versión de switch que sustituye los dos puntos por “->” y no necesita
break, de forma que solo se ejectuta el código de cada case y no de los siguientes (como ocurría en
el caso anterior si no incluíamos break) Queda el código más limpio.
switch (numMes) {
case 1 -> System.out.println("Enero");
case 2 -> System.out.println("Febrero");
case 3 -> System.out.println("Marzo");
case 4 -> System.out.println("Abril");
case 5 -> System.out.println("Mayo");
case 6 -> System.out.println("Junio");
case 7 -> System.out.println("Julio");
case 8 -> System.out.println("Agosto");
case 9 -> System.out.println("Septiembre");
case 10 -> System.out.println("Octubre");
case 11 -> System.out.println("Noviembre");
case 12 -> System.out.println("Diciembre");
}
En caso de tener varias sentencias a ejecutar para un case, debemos ponerlas entre llaves {}. Si
tenemos varios case con el mismo comportamiento, separamos sus valores por comas.
switch (numMes) {
case 1,3,5, 7,8,10,12 -> { numDias = 31;
mesLargo = true; }
case 2 -> numDias = 28;
case 4,6,9,11 -> numDias = 30;
}
Como en el caso del switch tradicional (case : ) podemos asignar el switch a una variable pero en este
caso no es necesario la instrucción yield si el case no tiene instrucciones adicionales. En el ejemplo
anterior, podríamos haber hecho:
int numDias = switch (numMes) {
case 1,3,5,7,8,10,12 -> { mesLargo = true; yield 31; }
case 2 -> 28;
case 4,6,9,11 -> 30;
default -> 0;
};
Al igual que en el formato tradicional, los ‘case’ tienen que cubrir todos los casos posibles, lo que
obligará a incluir la cláusula ‘default’ en la mayor parte de los casos.
Operador ?
Java incluye un operador ternario especial que puede sustituir a ciertos tipos de sentencias if- then-
else para la asignación de un valor a una variable. Este operador es ?. Puede resultar un tanto confuso
en principio, pero el operador ? resulta muy efectivo una vez que se ha practicado con él. El operador ?
tiene la siguiente forma general:
variable = condicion ? expresionTrue : expresionFalse;
Donde condicion puede ser cualquier expresión que dé como resultado un valor del tipo boolean. Si
esa condición genera como resultado true, entonces se le asigna a la variable la expresionTrue; en
caso contrario se le asigna expresionFalse. Es necesario que tanto la expresionTrue como la
expresionFalse devuelvan el mismo tipo que no puede ser void.
Como ejemplo, el código: if (x > y) mayor = x; else mayor = y;
Ahora que aún estamos aprendiendo a programar no recomendamos su uso, pero si tenemos que
saber interpretarlo si nos encontramos con él.
Los pasos a seguir serían:
1) Poner la variable a asignarle valor y el igual. mayor =
2) Poner la condición entre interrogaciones y el operador "?": mayor = (x > y ) ?
3) Poner el valor a asignar si la condición es true: mayor = (x > y ) ? x
4) Poner los dos puntos y el valor a asignar si la condición es false: mayor = (x > y ) ? x: y;
Otro ejemplo: Calcular la división de dos números siempre que el divisor sea distinto de cero. En caso
de que el divisor sea cero el resultado a mostrar será -999.
if (divisor !=0) resultado = dividendo/divisor; else resultado = -999;
lo haríamos así:
resultado = (divisor !=0) ? dividendo/divisor : -999;
Operador Resultado
== Igual a
!= Diferente de
> Mayor que
< Menor que
>= Mayor o igual que
<= Menor o igual que
El resultado de estas operaciones es un valor booleano: true o false. La aplicación más frecuente de
los operadores relacionales es en la obtención de expresiones que controlan la sentencia if y las
sentencias de ciclos.
Sólo se pueden comparar operandos enteros, de punto flotante y caracteres, para ver cuál es mayor
o menor. Para cadenas, como son objetos usaremos métodos propios de tal objeto, pero esto lo
veremos más adelante: if (cad1.equals(cad2))
Además de los operadores ya comentados: <, <=, >, >=, ==, != Java ofrece la posibilidad de
combinarlos mediante los operadores lógicos:
&&, es el AND lógico, en que ambas premisas deben ser true para que el resultado de la expresión
sea true.
||, es OR lógico, en la que con tal de que una de las expresiones sea true, el resultado también lo
será.
Al usar la forma en cortocircuito del operador AND (&&) no existe riesgo de que se produzca una
excepción en tiempo de ejecución, ya que, si denom es igual a cero, el resultado sería falso
independientemente del resultado de la expresión a la derecha del && por lo que ya no lo evaluaría
y no realizaría la división.
La forma sin “cortocircuito”, es decir, en la que se evalúan todas las expresiones de la
condición, aunque al evaluar la primera ya sepamos el resultado final, se hace mediante
los operadores & y | en vez de && y || respectivamente (no es muy frecuente).
El siguiente ejemplo calcula si un año es bisiesto o no (un año es bisiesto si es múltiplo de 4 pero no
múltiplo de 100. Excepcionalmente los múltiplos de 400 también lo son, a pesar de ser múltiplos de
100.
boolean bisiesto;
Scanner teclado = new Scanner (System.in);
System.out.println ("Introduce un año: ");
int año = teclado.nextInt();
El programa anterior funciona correctamente por que el operador AND && tiene más prioridad que
el operador || y así no hacen falta paréntesis adicionales. Si tenemos dudas, siempre podemos
añadir los paréntesis, aunque no sean necesarios.
Un último ejemplo, en el siguiente programa se muestra cómo resolver un problema mediante if con
anidamiento y sin anidamiento.
Scanner teclado = new Scanner (System.in);
System.out.println("Dime tu nota, entre 0 y 10 (con coma decimal)");
float nota = teclado.nextFloat();
// SIN ANIDAMIENTOS:
if (nota < 5) System.out.println (nota + " SUSPENSO");
if (nota >= 5 && nota < 7) System.out.println (nota + " APROBADO");
if (nota >= 7 && nota < 9) System.out.println (nota + " NOTABLE");
if (nota >= 9) System.out.println (nota + " SOBRESALIENTE");
En C/C++ el valor boolean falso está asimilado a cero y true como el resto de valores,
pero esto no es así en Java, así que siempre debemos usar variables y expresiones de
tipo boolean y no enteras para evaluar expresiones lógicas.
For
Esta estructura se utiliza para iteraciones o bucles que se va a ejecutar un número determinado de
veces, no depende de lo que ocurra en las instrucciones del interior del bloque.
La forma general de la sentencia for es la siguiente:
for (inicialización; condición; iteración) {
// operaciones que se van a ejecutar repetidamente
}
Si solamente se repite una sentencia, no es necesario el uso de las llaves, al igual que ocurría en el if,
o en su rama else o, como veremos a continuación, en el caso de while y do-while.
Al empezar, se ejecuta la parte de inicialización (generalmente, es una expresión que establece el valor
de la variable de control del ciclo) Esta expresión de inicialización se ejecuta una sola vez.
A continuación, se evalúa la condición, que debe ser una expresión booleana, si la expresión es
verdadera, entonces se ejecuta el cuerpo del ciclo. Si es falsa, el ciclo finaliza.
Cada vez que se recorre el ciclo, en primer lugar, se ejecuta la iteración y se vuelve a evaluar la
expresión condicional, si sigue siendo verdadera, se repite el proceso. Una vez que la condición sea
falsa, ya no se repite más el cuerpo y pasamos a la siguiente instrucción del programa.
Un bucle que queremos que se ejecute 10 veces podría tener una estructura como esta:
for (int i=1; i<=10; i++) {
// cuerpo
}
En ocasiones puede ser necesario incluir más de una sentencia en las secciones de inicialización e
iteración del ciclo for. Se puede hacer separando las expresiones de esas dos secciones mediante
una coma. for (int a=1,b=10;a<=10; a++,b--) System.out.println (a + " - " + b);
While
Con este ciclo se repite una sentencia o un bloque mientras la condición de control es verdadera. Su
forma general es:
while (condición) {
// cuerpo del ciclo
}
Ejemplo: Hacer un programa en el que usuario vaya metiendo números y los vaya acumulando, hasta
que el usuario introduzca -1. Finalmente mostrará el importe acumulado.
float num, total = 0;
De este ejemplo podemos inferir dos conceptos nuevos relacionados con los bucles: el primero se
denomina “lectura adelantada” y el de “acumulador”.
La lectura adelantada se refiere a que antes de evaluar el while debemos asegurarnos de que la
condición es evaluable, es decir, que sus variables tienen valor, por ello debemos hacer una primera
lectura de teclado antes de entrar en el bucle. La última instrucción del bloque del bucle debe ser
una nueva lectura para volver al principio del mismo.
Si quisiésemos evitar esta doble lectura, podríamos meterla al principio del bucle, pero
asegurándonos de que la primera vez la condición del while no va a representar ningún problema, y
que cuando el usuario introduzca -1 no vamos a restarle 1 a la suma acumulada.
Así pues, sin lectura adelantada, la lectura sería la primera instrucción dentro de la repetición, pero
necesitaría una condición justo a continuación para verificar que no es fin de ciclo. En el siguiente
ejemplo se ve el mismo programa que el anterior, pero sin lectura adelantada. Prueba a ejecutarlo,
y comprueba lo que ocurriría si se elimina el if.
import java.util.Scanner;
public class Ejemplo {
public static void main (String args[]) {
float num=0, total = 0;
Otro elemento importante que ha aparecido en este ejemplo es el concepto de acumulador, esto es,
una variable que va cambiando a medida que avanza el bucle, basándose en el valor previo y la
operación realizada. Su formato es, como se ha visto:
acumulador = acumulador + incremento;
y en notación abreviada:
acumulador += incremento;
Cuando el incremento del acumulador es siempre constante, se le denomina contador, y tiene el
formato (suponiendo la constante 2):
contador = contador + 2; //o contador +=2
En cada iteración del bucle la variable (suponiendo que se inicie en cero) tomará los valores: 2, 4, 6,
8, etc. Un caso muy habitual es en el que la constante es 1. Lo podríamos escribir de una tercera
forma:
contador++;
Do…While
Como se acaba de ver, si la expresión condicional que controla un ciclo while es inicialmente falsa, el
cuerpo del ciclo no se ejecutará ni una sola vez. Sin embargo, puede haber casos en los que se quiera
ejecutar el cuerpo del ciclo al menos una vez, incluso cuando la expresión condicional sea inicialmente
falsa. En otras palabras, puede que se desee evaluar la expresión condicional al final del ciclo, en lugar
de hacerlo al principio. El ciclo do-while ejecuta siempre, al menos una vez, el cuerpo, ya que la
expresión condicional se encuentra al final. Su forma general es:
do {
// operaciones que se ejecutan repetidamente
} while (condición);
En cada iteración del ciclo do-while se ejecuta en primer lugar el cuerpo del ciclo, y a continuación se
evalúa la expresión condicional. Si la expresión es verdadera, el ciclo se repetirá. En caso contrario, el
ciclo finalizará.
El ciclo do-while es muy útil cuando se procesa un menú de selección, ya que normalmente se desea
que el cuerpo del menú se ejecute al menos una vez. Ejemplo:
do {
System.out.println ("Elija:\na) Elevar al cuadrado");
System.out.println ("b)Raiz cuadrada\nOtra tecla para salir");
eleccion = teclado.nextLine().charAt(0);
Ciclos anidados
Al igual que vimos con la sentencia if se pueden anidar los bucles, tanto los while, do-while como for.
Estudia el siguiente ejemplo.
El primer bucle se enjutaría 10 veces, y para cada una de esas veces se ejecutaría el bucle interior. El
bucle interior, la primera vez se ejecutaría una sola vez (j va de 1 hasta i que vale 1), la segunda vez
se ejecutaría dos veces (j va desde 1 hasta i que ahora vale 2), y así sucesivamente. Al final de cada
iteración bucle interior dibuja un salto de línea con print \n.
Sentencias de Salto
Java incorpora tres sentencias de salto: break, continue y return. Estas sentencias transfieren el
control a otra parte del programa. Cada una es examinada aquí.
break
La sentencia break (además de su uso ya visto en un switch) fuerza la finalización inmediata de un
ciclo, evitando la expresión condicional y el resto de código dentro del cuerpo del ciclo. Cuando se
encuentra una sentencia break dentro de un ciclo, el ciclo termina y el control del programa se
transfiere a la sentencia que sigue al ciclo.
Cuando la sentencia break se utiliza dentro de un conjunto de ciclos anidados, solamente se saldrá del
ciclo en el que esté situado, si es el interno, no afecta al ciclo superior.
El siguiente ejemplo calcula si un número introducido es primo o no, recorriendo los números desde
el 2 hasta el anterior a dicho número. Si alguno de esos números es divisor del número introducido
(resto de su división es cero) ya no será primo, y podemos abandonar el bucle.
El uso de break en los bucles puede ser considerado un mal estilo de programación ya que, entre
muchas líneas de código, a veces es difícil encontrar la lógica de la condición de un bucle. Por ello,
suele ser recomendable controlar la condición que provoca el break desde la condición de ejecución
del bucle (en algunos casos convirtiendo un for en un while.
Esta sería una versión del ejemplo anterior, sin break, con un while en vez de for:
Este último es muy interesante, porque utiliza el concepto de flag, esto es, una variable (normalmente
de tipo boolean) que tiene un valor por defecto y en cuanto ocurre una determinada situación cambia
su valor y ya no vuelve al valor inicial. Ese nuevo valor es una marca para saber más adelante si esa
situación ha ocurrido o no. En el caso del ejemplo, por defecto un número dice que es primo, pero
en cuanto se encuentre un divisor esa situación cambia, ya no es primo, y nada hará que cambie esa
situación.
Este tipo de flags se suelen usar para enunciados del tipo: “saber si hay algún xxxx”, es decir una
situación, que, al darse una vez, ya nos marca la respuesta. En el caso del ejemplo: un número no es
primo si hay tiene algún divisor además de 1 y él mismo.
continue
Algunas veces es útil forzar una nueva iteración de un ciclo sin concluir completamente el
procesamiento de la iteración actual, es decir, un salto desde un punto del bloque hasta el final del
ciclo. Eso lo hacemos con la sentencia continue. Al igual que el caso del break no es muy aconsejable
abusar de su uso.
2.- Solicitar por teclado las edades de los alumnos de una clase y obtener la edad del mayor de la
clase. En el ejemplo anterior partíamos de un valor inicial ficticio para la variable ‘mayor’. En este no
lo hacemos así, tratamos la primera edad de forma especial, diciendo que es el mayor; luego el bucle
empieza en el segundo alumno.
Scanner teclado = new Scanner(System.in);
int edad;
final int TOTALALUMNOS = 10;
System.out.print("Introduce una edad: ");
edad = teclado.nextInt();
int mayor = edad;
for (int i = 2; i <= TOTALALUMNOS; i++) {
System.out.print("Introduce una edad: ");
edad = teclado.nextInt();
if (edad > mayor) mayor = edad;
}
System.out.println("el mayor tiene " + mayor + " años");
3.- Solicitar por teclado las edades de los alumnos de una clase y decir si hay algún mayor de edad,
solicitando las edades de todos los alumnos de la clase (no solo hasta que encontremos un menor
de edad).
Scanner teclado = new Scanner(System.in);
int edad;
final int TOTALALUMNOS = 10;
boolean hayMayorDeEdad = false;
for (int i = 1; i <= TOTALALUMNOS; i++) {
System.out.print("Introduce una edad: ");
edad = teclado.nextInt();
if (edad >= 18)
hayMayorDeEdad = true; //importante, no hay else
}
System.out.println("el mayor tiene " + mayor + " años");
if (hayMayorDeEdad) System.out.println("Sí hay algún mayor de edad");
else System.out.println("No hay ningún mayor de edad");
4.- Solicitar por teclado las edades de los alumnos de una clase y decir si hay algún mayor de edad,
solicitando las edades imprescindibles (en cuanto hay un mayor de edad ya paramos). Solución con
break, no recomendable.
Scanner teclado = new Scanner(System.in);
int edad;
final int TOTALALUMNOS = 10;
5.- Solicitar por teclado las edades de los alumnos de una clase y decir si hay algún mayor de edad,
solicitando las edades imprescindibles (en cuanto hay un mayor de edad ya paramos). Solución sin
break, recomendable.
6.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media de la clase
sabiendo previamente el total de alumnos.
7.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media solo de los
mayores de edad de la clase sabiendo previamente el total de alumnos.
8.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media de la clase
preguntando previamente al usuario el total de alumnos.
9.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media de la clase.
No sabemos la cantiad de alumnos, introducen 999 como edad para decir que no hay más alumnos.
Solución con lectura justo al empezar el bucle.
Scanner teclado = new Scanner(System.in);
int edad=0, suma=0;
int totalAlumnos=0;
float media;
while(edad !=999) {
System.out.println("Introduce una edad (999 para terminar): ");
edad = teclado.nextInt();
if (edad!=999) {
suma+=edad;
totalAlumnos++;
}
}
media= (float)suma/totalAlumnos;
System.out.println("La edad media es: " + media);
10.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media de la clase.
No sabemos la cantiad de alumnos, introducen 999 como edad para decir que no hay más alumnos.
Solución con lectura antes del bucle y como última instrucción dentro del bucle.
11.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media de la clase.
No sabemos la cantiad de alumnos, después de introducir cada alumno le preguntamos si quiere
introducir más y responde (S/N).
12.- Solicitar por teclado las edades de los alumnos de una clase y calcular la edad media de la clase.
No sabemos la cantiad exacta de alumnos; introducen 999 como edad para decir que no hay más
alumnos pero hay un límte máximo de 20.
4. Cadenas
Una cadena es una secuencia de caracteres, ya sean letras, números u otros símbolos. En Java, como
ya comentamos, no son un tipo de dato primitivos, sino que son objetos, por lo que además de
aprender a tratarlas nos servirán como primer contacto con el mundo de los objetos.
String
Cuando hablamos de objetos, el tipo de objeto se llama clase. La clase para tratar con cadenas se
llama String. Cuando escribimos:
int x=3; decimos que creamos una variable llamada x que es de tipo int o entero.
Análogamente cuando escribimos:
String cad="Hola mundo"; decimos que creamos un objeto u instancia de la clase String
llamada cad.
En realidad, la sintaxis pura para crear una instancia de un objeto, a nivel general, sería:
String Cadena = new String("Hola");
Es decir, la forma de crear nuevos objetos de forma general es mediante el operador new. En el caso
de las cadenas podemos emplear ambas sintaxis y otras clases tienen otros formatos para crearlas.
La gran diferencia que nos vamos a encontrar con una variable de un tipo primitivo es que tiene
definidas una serie de operaciones (que llamaremos métodos) que nos facilitarán el trabajo con ellas.
Por ejemplo, si queríamos saber si un número era primo o no, teníamos que hacer un bloque de
código que lo calculase y nos dijese si lo era o no. En el caso de los objetos muchas de estas
operaciones ya están definidas, y solo tenemos que llamarlas y recibir el valor que nos devuelven.
Como primer ejemplo, si queremos saber cuántos caracteres tiene una cadena, no tenemos que
recorrerla mediante un bucle e ir contando, simplemente invocaremos al método length(), que
devuelve precisamente la cantidad de caracteres y le asignaremos ese valor devuelto a una variable.
Lo haríamos mediante la siguiente sintaxis:
int longitud = cadena.length();
Vemos que un método se invoca con un punto y su nombre, después del nombre del objeto. Esto no
es nuevo, cuando hacíamos: System.out.println ("Hola"); estábamos invocando al método
println de la clase System.out.
Los métodos pueden recibir parámetros, como una fórmula de una hoja de cálculo, es decir, valores
que le hacen falta para calcular el resultado de la operación que realizan. En el caso de la longitud
no necesita ningún parámetro, por eso van los paréntesis sin nada en su interior, y en el caso del
println necesita la cadena que va a mostrar por pantalla.
El siguiente programa solicita al usuario que teclee una contraseña, que tiene que tener como
mínimo 8 caracteres.
Scanner teclado = new Scanner (System.in);
String password = new String ();
do {
System.out.println("Introduce una contraseña (min 8 caract)");
password = teclado.nextLine();
} while (password.length() < 8);
Otros métodos interesantes que tenemos en las cadenas son los siguientes:
• Convertir a mayúsculas:
String mayusculas = cadena.toUpperCase();
• Convertir a minúsculas:
String minusculas = cadena.toLowerCase();
• Concatenar cadenas (también podemos concatenar caracteres individuales):
String nombre = "José"; String apellido = "López"
String nombreCompleto = nombre + " " + apellido;
También se puede usar la función “concat()” para realizar la misma acción:
String nombreCompleto = nombre.concat(" ").concat(apellido);
• Obtener una subcadena de la cadena, esto es, recuperar un fragmento del texto que contiene
nuestra cadena principal. Para esto se usa un índice inicial y un índice final, tomando en
cuenta que el índice inicial de una cadena es “0” y la posición del índice final no se incluye:
String frase = "El perro y el gato juegan juntos";
String subcadena1 = frase.substring(3, 8);
Subcadena1 sería igual a = "perro"
• Obtener una cadena solo a partir de un índice inicial hasta el final de la cadena:
String subcadena2 = frase.substring(3);
Subcadena2 sería igual a = "perro y gato juegan juntos"
• Encontrar la posición en la que se obtiene un carácter o una subcadena. Devuelve un entero
que representa la posición de la primera ocurrencia del elemento buscado, o bien -1 si no lo
encuentra. Tenemos varios formatos, dependiendo si buscamos caracteres o cadenas y si
especificamos una posición de inicio de la búsqueda o no:
pos= frase.indexOf ('x');
pos= frase.indexOf ('x',posini);
pos= frase.indexOf ("texto");
pos= frase.indexOf ("texto",posini);
Ejemplo:
String frase = "Ellos juegan juntos";
int posic1 = frase.indexOf("ju"); // posic1 sería 6
int posic2 = frase.indexOf("ju",8); // posic2 sería 13
int posic = frase.indexOf("ju",14); // posic 3 sería -1
Ejemplo de programa que muestra cada letra de una cadena en una línea distinta, saltándose los
espacios en blanco:
String cad1 = "Este programa muestra cada letra en una línea";
for (int i = 0; i < cad1.length(); i++) {
char letra = cad1.charAt(i);
if (letra != ' ' ) System.out.println(letra);
}
Valor null
Como hemos comentado, un String es un objeto, y los objetos pueden no contener ningún valor (a
diferencia de los tipos primitivos). Para hacer que una cadena no almacene nada le asignamos el
valor null. String s = null;
También podemos preguntar si una cadena es null. De hecho, en algunos casos será aconsejable
hacerlo antes de llamar a algún método, ya que en caso de que la cadena sea null, se producirá una
excepción y nuestro programa terminará abruptamente.
if (s!=null) System.out.println(s.length());
String dispone de algún método que nos puede parecer que es lo mismo que preguntar por ==null
pero no producen el mismo resultado:
• isEmpty () devuelve true si la cadena vacía, esto es "", sin ningún carácter, pero no es lo mismo
que null.
• isBlank () devuelve true si la cadena contiene solo espacios en blanco, esto es: " ", o bien " ",
etc. (también si es "", sin nada dentro devuelve true).
Reemplazar caracteres
Para reemplazar caracteres en una cadena disponemos de varios métodos de la clase String. Como
ya sabemos, los String son inmutables, por lo que estos métodos no modifican la cadena si no que
devuelven una nueva cadena con los reemplazos realizados.
• replace (char charbuscado, char charnuevo) / replace (String textobuscado, String textonuevo)
reemplaza todas las ocurrencias del primer parámetro por el segundo. Ejemplo:
String original = "Este barco es el barco más grande";
String nueva = original.replace("barco","coche");
La cadena nueva contendría: Este coche es el coche más grande
Y lo contrario, podemos convertir cadenas a los siguientes tipos de datos: Byte, Integer, Double, Float,
Long y Short. Esto es útil, por ejemplo, si leemos un número, pero queremos tratar cada uno de sus
dígitos por separado. Podemos convertirlo en una cadena y acceder a cada digito individualmente.
int numeroEntero = Integer.parseInt("10");
int numeroEntero = Integer.parseInt("F7A3", 16);
float numeroFloat = Float.parseFloat("20");
double numeroDouble = Double.parseDouble("25.5");
long numeroLong = Long.parseLong("123456");
Recordemos que había un “bug” en la entrada de datos con Scanner cuando se hacía .nextLine()
después de un .nextFloat() ó .nextInt()… Con Float.parseFloat(teclado.nextLine()) se soluciona
este problema, pero ojo: en este caso el separador decimal es punto y no coma.
Clase Character
En muchas ocasiones necesitaremos tratar cada una de las posiciones de la cadena (o bien alguna
de ellas), para realizar alguna operación sobre la misma, por ejemplo, saber si el carácter de una
posición es un dígito o es una letra minúscula.
Para ello Java nos ofrece estas “funciones”:
• Character.isLetter(ch1) //devuelve true o false
• Character.isDigit(ch1) //devuelve true o false
• Character.isSpaceChar(ch1) //devuelve true o false
• Character.isUpperCase(ch1) //devuelve true o false
• Character.isLowerCase(ch1) //devuelve true o false
• Character.toUpperCase(ch1) //devuelve un char
• Character.toLowerCase(ch1) //devuelve un char
• Character.toString(ch1) //devuelve un String
• Character.getType(ch1) //devuelve la categoría del carácter.
Cuando avancemos en el tema de las clases y los objetos, veremos que tanto Integer como
Character son clases envoltorio (wrapper class), que “envuelven” al tipo primitivo correspondiente
(en este caso int y char respectivamente), con el mismo contenido que el tipo que envuelven, pero
en forma de objeto con propiedades y métodos muy útiles.
También veremos que los métodos que acabamos de mencionar, con el formato
Integer.toString, Character.isLetter son “de clase” o “estáticos”, es decir, que no hace falta
crear una instancia de la clase para poder utilizarlos. Otros métodos, como
cadena.toUpperCase(); requieren se llamados sobre una instancia de una clase (en este caso
cadena es una instancia de la clase String)
Format
Otra utilidad interesante es format para unir textos y variables con un formato definido por nosotros.
Para entenderlo basta este ejemplo sencillo:
int edad = 28;
String nombre = "David";
String resultado = String.format("Nombre:%s,edad:%d años",nombre,edad);
.precisión: (empieza por punto) indica el número máximo de dígitos después de la coma decimal.
type: (obligatorio) indica de qué tipo es el argumento. Los más usados son:
b: boolean
c: carácter Unicode
d: número entero
f: número decimal
s: String
t: fecha y hora
Ejemplos:
Instrucción Salida
int num = 1234;
String.format("El número es: %08d", num); 00001234
float num = 1.5;
String.format("El número es: %08.3f", num); 0001,500
float num = 1.5;
String.format("El número es: %+.2f", num); +1,50
Existen otras clases que podemos utilizar para formatear como pueden ser Formatter
o DecimalFormat, con funcionalidad similar y parecida forma de trabajo.
printf
Este método de la clase System.out tiene la misma funcionalidad que los vistos previamente: print y
println pero siguiendo gue el mismo patrón de funcionamiento que el método format que acabamos
de ver. Así podemos mostrar por consola salidas formateadas:
System.out.printf("cateto:%d > cateto:%d >> %.2f%n",
cat1,cat2,Math.hypot(cat1,cat2));
Con printf, en vez de emplear \n para el salto de línea, se suele usar %n ya que tiene mayor
compatibilidad en distintas plataformas.
StringBuilder y StringBuffer
Además de String, existen otras clases como StringBuffer y StringBuilder que resultan de interés
porque facilitan cierto tipo de trabajos y aportan mayor eficiencia en determinados contextos.
La clase StringBuilder es similar a la clase String pero presenta algunas diferencias relevantes:
setLength(int
void Modifica la longitud. La nueva longitud no puede ser menor
nuevaLongitud)
delete(int indiceIni,int Borra la cadena de caracteres incluidos entre los dos índices
StringBuilder
indiceFin) indicados en los argumentos
replace(int indiceIni, int Reemplaza los caracteres comprendidos entre los dos índices por
StringBuilder
indiceFin,String str) la cadena que se le pasa en el argumento
La clase StringBuffer es similar a la clase StringBuilder, con los mismos métodos y constructores,
pero sus métodos están sincronizados, permitiendo trabajar con múltiples hilos threads.
Esto no ocurre con los StringBuilder/StringBuffer, los métodos sí modifican el contenido del objeto,
así podríamos hacer directamente:
Es frecuente convertir String a StringBuider si necesitamos un método de una de las clases que
está disponible en la otra. Por ejemplo, si necesitamos eliminar en la tercera posición de una
cadena, podríamos hacer:
String cadena = "abcdef";
StringBuilder sb = new StringBuilder(cadena);
sb.deleteCharAt(3);
cadena = sb.toString();
abreviando:
cadena = new StringBuilder(cadena).deleteCharAt(3).toString();
Errores Frecuentes:
Hay que comparar los String con equals(), compareTo(), compareToIgnoreCase(),
pero nunca con ==.
Los String comienzan en la posición cero, no en la 1.
El último carácter de una cadena está en length()-1. Es erróneo
cadena.charAt(cadena.length()).
El método substring (x,y) no incluye la posición ‘y’, acaba en la anterior a ‘y’.
Una operación sobre un String crea un nuevo String, no modifica el actual.
5. Funciones
Las funciones en un lenguaje no orientado a objetos son bloques de código que pueden ser
invocados pasándoles ciertos parámetros y que pueden devolver un valor de cualquier tipo de datos.
Es frecuente que desde el cuerpo principal de un programa se llame a funciones para que realice
ciertas tareas y así estructurar mejor el código.
Un ejemplo típico de función podría ser una llamada esPrimo a la que se le pasase como parámetro
un número entero y devolviese verdadero o falso (la función sería un concepto parecido a una
fórmula de Excel).
La ventaja que tiene una función es que, una vez codificada y probada, la podremos usar en multitud
de programas, reutilizando código previo y quedando un código más claro. Vamos primero a ver su
estructura y luego veremos un ejemplo de un programa con y sin funciones para ver sus diferencias.
En Java no existe el concepto de función, es tratado como un método estático del programa que lo
usa (el programa es en realidad una clase), pero como aún no hemos llegado al tema de la
orientación a objetos, vamos a obviar esto por ahora. Retomaremos esto en el tema siguiente.
La definición de una función, con lo que sabemos por ahora, la podemos simplificar así:
static tipodevuelto nombreFuncion (tipo param1, tipo2 param, …) {
//cuerpo de la función
//return finaliza la función devolviendo un valor.
}
Siendo:
• tipodevuelto : el tipo de dato que devuelve la función, puede ser un tipo primitivo (int,
boolean, etc.) o bien una clase Java (por ejemplo, String) o una clase creada por nosotros
(Alumno, Teléfono, etc.)
• nombreFunción: es el nombre que le damos a la función: esPrimo, calcularFactura, etc… Por
convenio suele empezar por minúscula, aunque no es obligatorio.
• Tipo param1: entre paréntesis figurarán unos identificadores precedidos de su tipo, que
representan los parámetros que le pasamos a la función para que realice los cálculos
necesarios.
• En el cuerpo de la función irían todos los cálculos, incluyendo uno o varios return que finalizan
la función en ese momento, devolviendo un valor acorde a lo definido en la cabecera de la
función.
La función que verifica si un número pasado como parámetro es primo sería algo así:
Fíjate que usamos la característica del return, que termina abruptamente la ejecución del método,
sin finalizar el bucle, para reducir el número de líneas de código con respecto al cálculo de número
primo realizado al principio de este manual. Si la función llega a ejecutar todas las iteraciones del
bucle, llegaría a la siguiente instrucción, que devolvería true.
Ahora, desde el programa (main) o incluso desde otra función podríamos llamar a esa función:
int x=7;
boolean primo = esPrimo (x);
if (primo) System.out.println ("Sí es primo");
La llamada se podría hacer en el propio if, siendo el valor devuelto por la función lo que se evalúa:
if (esPrimo(7)==true) System.out.println ("Sí es primo" o bien
if (esPrimo(7)) System.out.println ("Sí es primo");
Los parámetros de un método son los valores que este recibe por parte del código que lo llama.
Pueden ser tipos primitivos u objetos y en la declaración del método se escriben después del
nombre del método entre paréntesis indicándose el tipo de cada uno. Ejemplo:
sumarImpuesto (float importe, float impuesto) {
importeTotal = importe + importe * impuesto/100f;
. . . }
y la llamada sería algo como sumarImpuesto (12.2 , x); //con x=21 p.ej.
Hay que destacar que el nombre que tienen los parámetros en la cabecera de la función no tienen
nada que ver con el nombre de las variables con las que son llamadas las funciones.
Es un concepto similar a los parámetros que le pasamos a las fórmulas de una hoja de cálculo, por
ejemplo =SUMA(A2,B3).
Es importante destacar que, cuando se ejecuta el return de un método finaliza la ejecución del mismo
y la expresión del return es lo que se devuelve a la sentencia que lo llamó. El tipo de dato devuelto
tiene que coincidir con el que se especifica antes del nombre del método. Ejemplo:
Función: double sumarIva (float euros) { return euros * 1.21; }
Llamada: importeTotal = sumarIva (12.50F);
En un programa estructurado en funciones, las variables definidas en una función solo tendrán
vigencia en esa función, concretamente en el bloque en el que se hayan definido (el bloque lo marcan
las llaves { bloque }. Si necesitamos una variable común a todas las funciones, una variable que se
pueda usar a lo largo de todo el programa, debemos definirla después del nombre del programa. A
este tipo de variables, visibles desde todo el programa, se le denominan variables globales.
En el siguiente ejemplo, la variable teclado (que es un objeto de la clase Scanner), se usará a lo largo
de todo el programa, por eso debe ser definida como global.
Las variables globales deben llevar el prefijo “static” antes de su tipo.
import java.util.Scanner;
public class Ejemplo {
static Scanner teclado;
Para terminar este apartado de funciones vamos a escribir un mismo programa sin funciones y con
funciones para comprobar lo claro que queda el código que queda en el segundo caso comparado
con el primero.
Se trata de un programa que calcula el día siguiente a partir de una fecha introducida previamente y
suponiendo que la fecha es válida. El programa debe distinguir 3 situaciones: que sea fin de año, que
sea fin de mes y no fin de año y resto de los casos.
Vemos en el primer ejemplo como el código está todo mezclado y es difícil de entender.
En el segundo caso, la estructura del programa, con funciones es mucho más claro.
public class diaSiguiente {
public static int dia,mes,año; //variables globales
public static void main(String[] args) {
Scanner teclado = new Scanner (System.in);
System.out.print("Introduce día: ");dia=teclado.nextInt();
System.out.print("Introduce mes: ");mes=teclado.nextInt();
System.out.print("Introduce año: ");año=teclado.nextInt();
sumaDia();
System.out.printf("dia siguiente:%d/%d/%d%n",dia,mes,año);
}
public static void sumaDia(){
if (dia == 31 && mes ==12 ){dia=1;mes=1;año++;}
else{
int diasMes = cantidadDiasMes (mes,año);
if (dia ==diasMes) {dia=1;mes++;}
else dia++;
}
}
public static int cantidadDiasMes (int mm, int aa){
if (mm==2){if (añoBisiesto(aa)) {return 29;}
else {return 28;} //else opcional
}
if (mm==4 || mm==6 || mm==9 || mm==11) return 30;
return 31;
}
public static boolean añoBisiesto (int a) {
if (a%4==0 && a%100!=0 || a%400==0) return true;
return false;
}
}
El programa anterior no es aún así una solución perfecta, ya que la función sumaDia () lee y escribe
las variables globales dia, mes y año. Lo ideal es que las funciones reciban como parámetros todo lo
que necesitan para funcionar y que devuelvan su resultado en en el return, sin emplear variables
globales. Así son más fáciles de reutilizar en otros programas.
Vamos a ver a continuación como las funciones añoBisiesto o CantidadDiasMes pueden ser
reutilizadas en otros programas, reduciendo nuestro trabajo de programación. Supongamos que
ahora necesitamos hacer un programa que calcule el día anterior a una fecha introducida.
Aprovechando las funciones creadas, el programa sería así:
public class diaAnterior {
public static int dia,mes,año; //variables globales
public static void main(String[] args) {
Scanner teclado = new Scanner (System.in);
System.out.print("Introduce día: ");dia=teclado.nextInt();
System.out.print("Introduce mes: ");mes=teclado.nextInt();
System.out.print("Introduce año: ");año=teclado.nextInt();
restaDia();
System.out.printf("dia anterior:%d/%d/%d%n",dia,mes,año);
}
public static void restaDia(){
if (dia == 1 && mes ==1 ){dia=31;mes=12;año--;}
else{
if (dia==1){
dia = cantidadDiasMes (mes,año);
mes--;
}
else dia--;
}
}
El programa mostaría “10”. Si los parámetros se pasasen por referencia, mostaría 20.
En el tema siguiente, veremos que en el caso de los objetos el comportamiento el
ligeramente diferente.
6. Clases y Objetos
Conceptos Básicos
La programación Orientada a objetos (POO) es una forma especial de programar, más cercana a
como expresaríamos las cosas en la vida real que otros tipos de programación. Por ejemplo, vamos
a pensar en un coche para tratar de modelizarlo en un esquema de POO. Diríamos que el coche es
el elemento principal que tiene una serie de características, como podrían ser la matrícula, el color,
el tamaño del depósito, cantidad actual de combustible o la velocidad máxima. Además, tiene una
serie de funcionalidades asociadas, como pueden ser ponerse en marcha, recorrer kilómetros, parar
o aparcar.
En programación esas características se llaman propiedades y son similares a las variables que
hemos visto previamente. Las funcionalidades se llaman métodos y son bloques de código que tiene
una finalidad concreta. Esos métodos pueden modificar las propiedades de un objeto, por ejemplo,
recorrer kilómetros reduciría la cantidad actual de combustible.
Si sobre ese coche invocamos al método recorrer_kilometros (100) se reducirá unos cuantos litros el
carburante disponible. Si una de las propiedades del objeto fuese su ubicación, dicho método
también la cambiaría.
A partir de una clase se pueden crear múltiples objetos, todos con el mismo comportamiento
(métodos) pero con valores propios asignados a sus atributos, lo que determinará el estado de
cada objeto. Durante la ejecución del programa los objetos se crean, se ejecutan métodos sobre
ellos y cuando ya no son necesarios, se destruyen.
Esto es solo lo más básico de la POO. Este paradigma contiene mecanismos como la herencia y el
polimorfismo que lo dotan de gran potencia. Por ahora podemos quedarnos con la idea del
abstracción y encapsulamiento, esto es, una vez definida y programada una clase podemos usarla
en miles de programas distintos, pero sin conocer “las tripas” de ese objeto. La abstracción hace
referencia a enfatizar las características y comportamiento de los objetos que son necesarios,
prescindiendo de los que no son. El encapsulamiento complementa a la abstracción y se refiere a
ocultar al exterior cómo se han programado las funcionalidades de la clase, se centra en mostrar el
“qué es/qué hace” y no en “cómo lo hace” haciendo que desde el exterior solo se pueda acceder a la
información relevante.
A veces se habla de “caja negra”, haciendo referencia a que el comportamiento y atributos del objeto
son conocidos, pero no así su trabajo interno, el cual continúa siendo un misterio. Veremos que
cuando definimos una clase, le podemos asignar diferentes modificadores de acceso, que
determinarán si ese atributo o método puede verse desde fuera o no.
Esto ocurría con los String vistos previamente, que como comentamos son objetos, una vez creada
una cadena nosotros podíamos invocar a un método, por ejemplo
micadena = micadena.toUpperCase();
y sabíamos que realizaba una determinada operación, pero sin necesitar saber cómo funciona
internamente.
Veamos otra forma de aproximarnos a los objetos: Imaginemos que hay creada una clase llamada
JuegoAjedrez, cuyas propiedades ni siquiera conocemos pero que dispone de los métodos:
• moverJugador, a la que le proporcionamos como parámetro una casilla origen y una casilla
destino y devuelve true si el movimiento es correcto o false si el movimiento es incorrecto.
• moverMaquina, el ordenador piensa y hace un movimiento.
• esJaqueMate, devuelve 0 si no hay jaque mate, 1 si gana blancas, 2 si gana negras
• pintarTablero, muestra la situación actual del tablero.
Como primer contacto, los métodos parece que son como las funciones vistas previamente,
pero aplicadas sobre una clase. En realidad, es todo lo contrario, una función es un método y
la clase a la que pertenece dicho método es el programa en el que se crea.
Esto nos lleva a otra reflexión: los programas en Java son clases: sus atributos son lo que
llamamos previamente variables globales del programa, y sus métodos son las funciones del
programa. Más adelante descubriremos porque tanto esas variables globales como a las
funciones les poníamos el modificador static.
Solo con esta información, y sin tener apenas ningún conocimiento sobre el ajedrez, podríamos
hacer un programa en el que el ordenador jugase al ajedrez contra el usuario.
Por ejemplo, así:
import java.util.Scanner;
public class Ejemplo {
public static void main (String args[]) {
se crea un objeto de tipo JuegoAjedrez. El método mediante el cual se crea se llama constructor y si
nosotros no lo creamos, se crea por defecto (puede interesarnos crearlo para inicializar el objeto con
unos valores determinados).
El constructor es la primera operación que debemos hacer con un objeto, y es el momento en el que
nace el objeto, a partir de ese momento podremos invocar a sus métodos o leer y modificar sus
propiedades. El nombre del constructor es siempre igual al de la clase y no devuelve ningún valor.
Su sintaxis es:
NombreClase nombreObjeto = new NombreClase();
Una vez creado el juego, el programa es un bucle de movimientos por parte de la máquina y el
jugador, comprobando si alguno hace algún movimiento ganador, y repintando el tablero cada vez
que hay un movimiento. El programa funciona perfectamente y todo el código referente al juego de
ajedrez ha quedado oculto en la clase juegoAjedrez.
El constructor es un método que se llama igual que su clase y que sólo puede recibir valores, pero
nunca retornar ningún valor. Gracias a ellos se inicializan los atributos de objeto, bien con valores
pasados como parámetro al constructor, bien con valores por defecto. Si no creamos un
constructor, Java construye uno por defecto, sin parámetros, que simplemente crea el objeto,
pero no inicializa ninguno de sus atributos ni hace ninguna otra tarea, solo crea el objeto.
Ahora podríamos crear un programa que crease instancias de Vehículos. Una vez creados podemos
asignar valores a cada uno de sus atributos: matrícula, cantidad de pasajeros y consumo:
El siguiente paso sería desarrollar los métodos que caracterizan el objeto o bien los métodos que
son necesarios para la funcionalidad que necesitamos. También vamos a crear un constructor que
se encargue de inicializar las propiedades de los objetos.
package ejemplo;
public class Vehiculo {
String matricula;
int cantPasajeros;
int tamDeposito;
float consumo;
float autonomia(){
return 100 * this.tamDeposito / this.consumo;
}
En el constructor vemos como se inicializan las tres propiedades del objeto. Cuando hacemos
referencia al propio objeto podemos utilizar opcionalmente la palabra this tanto en el constructor
como en los métodos. Así pues, el constructor también podría haber quedado así:
Vehiculo(String mat, int cantPas, int tamDep, float con) {
this.matricula = mat;
this.cantPasajeros = cantPas;
this.tamDeposito = tamDep;
this.consumo = con;
}
Una clase puede tener varios constructores, cada uno con un número diferente de parámetros o de
de distinto tipo. Ejemplo: el siguiente constructor fija el número de pasajeros a 5 y ya no es necesario
pasárselo como parámetro:
La pregunta que surge ahora es ¿Cómo invoco a uno u otro contructor? La respuesta es que el
constructor usado vendrá determinado por los parámetros que le pasemos al mismo. Así usaríamos
en un programa los constructores que acabamos de crear:
Vehiculo vehiculo1 = new Vehiculo ("1234ABC", 4, 50, 7.8f);
Vehiculo vehiculo2 = new Vehiculo ("7777ZZZ", 51, 8.9f);
Como ya comentamos, si no creamos ningún constructor, se crea uno por defecto, sin ningún
parámetro ni tampoco asigna ningún valor. Sería como si escribiésemos en la clase:
Vehiculo() { }
Obviamente habría que darle valores a los atributos posteriormente, por ejemplo:
vehiculo3.matricula = "AAAA222";
A un constructor, también podemos pasarle un objeto de esa misma clase creado previamente, para
que el objeto nuevo use esos valores, por ejemplo, para copiarlos. Añadimos a la clase:
Vehiculo(String mat, Vehiculo otroVehiculo) {
this.matricula = mat;
this.cantPasajeros = otroVehiculo.cantPasajeros;
this.tamDeposito = otroVehiculo.tamDeposito;
this.consumo = otroVehiculo.consumo;
}
A la izquierda del igual está la variable que referencia al objeto, que es la forma de
acceder a ese objeto. A la derecha, el constructor hace la reserva en memoria y
opcionalmente le da valores a sus atributos. Ambas operaciones son necesarias.
Desde un constructor se puede invocar a otro, para reducir el código. Así pues, este último
constructor de copia se podría haber escrito así:
Vehiculo(String mat, Vehiculo otroVehiculo) {
Vehiculo (mat, otroVehiculo.cantPasajeros,
otroVehiculo.tamDeposito, otroVehiculo.consumo);
}
El siguiente método, autonomía(), vemos que tiene el tipo float antes del nombre del método y, ya
en el bloque de código, una cláusula return. Lo que significa el primero es que el método devuelve
un valor de este tipo, float en este caso, que podrá ser recibido por una variable cuando sea llamado.
El valor que devuelve es el especificado en la cláusula return, en este caso 100 * deposito / consumo.
Cabe destacar que este método no necesita ningún parámetro adicional para ser ejecutado ya que
toda la información que necesita para calcular la autonomía está en la propia clase. Es por ello que
después del nombre aparecen los paréntesis sin nada en su interior ().
En la siguiente imagen podemos ver una clase sencilla llamada ‘Persona’ y como en un programa, se
crea una instancia de esa clase llamada ‘p’ invocando al constructor de la clase. A continuación, se
invoca al método ‘mayorDeEdad’ de dicha clase, sobre esa instancia de persona ‘p’.
Si no creamos ningún constructor, Java lo crea por defecto, pero sin ninguna funcionalidad
añadida, solo se preocupa de crear la instancia del objeto. Si nosotros creamos un constructor,
como ha sido el caso de este último ejemplo, Java no crea ese constructor por defecto, por lo tanto,
en este último programa, una instrucción de tipo:
Vehiculo monovolumen = new Vehiculo (); sería errónea.
Para solucionarlo, deberíamos crear ese constructor sin parámetros en la definición de la clase.
Vehiculo() {}
Ejemplo 1 :
- Vehiculo v1 = new Vehiculo (“1234ABC”, 4, 50, 6.2);
- v1 = new Vehiculo (“8888ZZZ”, 32, 5.1);
Hemos perdido el acceso al vehículo “1234ABC” y se borrará, porque v1 ahora
“apunta” al vehículo “8888ZZZ”.
Ejemplo 2:
- Vehiculo v1 = new Vehiculo (“1234ABC”, 4, 50, 6.2);
- Vehiculo v2 = v1;
No son dos vehículos. Es un único vehículo referenciado por dos variables. Si
hacemos un cambio en una variable se verá también en la otra.
Ejemplo 3:
- Vehiculo v1 = new Vehiculo (“1234ABC”, 4, 50, 6.2);
- Vehiculo v2 = new Vehiculo (null, 0,0,0);
- V2.matricula=v1.matricula;V2.cantPasajeros=v1.cantPasajeros;
- v2.consumo=v1.consumo; v2.tamDeposito=v1.tamDeposito;
Son dos vehículos distintos e independientes, aunque tienen los mismos valores por
lo que podríamos decir que son “iguales”.
Por último, si hacemos if (v1==v3) siempre devolverá false si las variables referencian distintos
objetos (distintas posiciones de memoria), aunque el contenido de los objetos sea idéntico, aunque
los dos vehículos tengan exactamente los mismos valores en sus atributos. Veremos más adelante
que usaremos equals(), hashCode() y compareTo() para las comparaciones.
Una última diferencia, que ya expusimos en el capítulo de cadenas, es que una variable de tipo
primitivo no puede tener valor null, pero un objeto sí, ya que al almacenar una referencia a memoria
y no un contenido, si que podemos decirle mediante null que no apunte a ningún contenido.
Podemos asignarle valor null mediante el operador de asignación = y también podemos preguntar
si un objeto es igual a null mediante ==.
(*) Si el método isDigit() no fuese static, habría que llamarlo de una forma similar a
esta: ch1 = new Character(); if ( ch1.isDigit() == true) {. . . };
Es decir, primero creando una instancia con un constructor de la clase y luego invocando
al método sobre ella.
Ahora ya tenemos la explicación de por qué las funciones y las variables globales de un programa
son static. No tiene sentido hablar de instancias del programa.
Modificadores de Acceso
Los modificadores de acceso nos introducen al concepto de encapsulamiento. El encapsulamiento
busca de alguna forma controlar el acceso a los datos que conforman un objeto, de este modo
podríamos decir que una clase (y sus objetos) que hacen uso de modificadores de acceso
(especialmente privados) son objetos encapsulados.
Los modificadores de acceso permiten dar un nivel de seguridad mayor a nuestras aplicaciones
restringiendo el acceso a diferentes atributos, métodos, constructores asegurándonos que el usuario
deba seguir una "ruta" especificada por nosotros para acceder a la información.
Siempre se recomienda que los atributos de una clase sean privados, para que no se acceda a ellos
directamente y se implementen métodos públicos get y set para para obtener y establecer
respectivamente el valor del atributo.
Algo a tener en cuenta es que siempre que se use una clase de otro paquete, esta se debe importar
usando import (esto ya lo vimos en el apartado de entrada/salida en la que importábamos la clase
Scanner). Cuando dos clases se encuentran en el mismo paquete no es necesario hacer el import
pero esto no significa que se pueda acceder a sus componentes directamente, dependerá de su
modificador de acceso. Import debe ir justo después de la sentencia package, antes de la definición
de clases.
private
El modificador private en Java es el más restrictivo de todos: cualquier elemento que sea privado
puede ser accedido únicamente por los métodos y constructores de dicha clase, pero no por otras
clases o programas del proyecto.
En general, se recomienda que los atributos de una clase sean privados, para que no se
acceda a ellos directamente y se implementen métodos públicos get y set para para
obtener y establecer respectivamente el valor del atributo. Se llaman getters y setters,
y los veremos con más detalle ma´s adelante.
Como ejemplo, vamos a poner la propiedad cantPasajeros como privada:
public class Vehiculo {
String matricula;
private int cantPasajeros;
int tamDeposito;
float consumo;
static float precioLitro;
Creando los métodos para acceder a su valor y para fijarle un nuevo valor. Podemos establecer reglas a
la hora de hacer el set, controlando mejor los valores que le permitimos tomar.
public void setCantPasajeros (int p) {
if (p > 0 && p < 10) this.cantPasajeros = p;
}
public int getCantPasajeros () {
return cantPasajeros;
}
Modificador protected
El modificador de acceso protected nos permite acceso a los elementos con dicho modificador desde
la misma clase, clases del mismo paquete y clases que hereden de ella (incluso en diferentes
paquetes). Como el tema de herencia aún no lo hemos tratado, no nos centraremos en este
modificador de acceso por ahora.
Modificador public
El modificador de acceso public es el más permisivo de todos, básicamente public si un componente
de una clase es public, tendremos acceso a él desde cualquier clase o instancia sin importar el
paquete o procedencia de ésta.
Una clase ha de ser siempre public o default. En el primer caso deberá estar en un archivo
.java con el mismo nombre. En el segundo caso no es necesario.
El tema de los paquetes se tratará en temas posteriores. Por ahora simplemente saber que un
paquete es un contenedor de clases que permite agrupar las distintas clases que por lo general
tiene una funcionalidad y elementos comunes, definiendo la ubicación de dichas clases en un
directorio de estructura jerárquica.
Para especificar el paquete al que pertenece una clase, se pondrá la instrucción package
nombrePaquete; y debe ser la primera línea del archivo, así pues, la estructura típica de un
programa/clase Java podría ser:
package nombrePaquete;
import librerias;
public class nombreClase {}
Clases Interesantes
Java nos ofrece una serie de clases para facilitarnos operaciones cotidianas como las siguientes:
Round
El método round no redondea con decimales, según la cantidad de decimales debemos multiplicar
por 10, 100, etc. y luego hacer la división decimal del resultado entre el mismo número.
Ejemplo, para dos decimales: Math.round(num*100)/100d;
Por otra parte, con datos de tipo float este redondeo funciona correctamente, pero con datos de tipo
double a veces veremos resultados del tipo 12,999999999 o bien 12,000000001. Para solucionar
estos problemas se podría trabajar con otras clases somo BigDecimal aunque nosotros no lo
veremos. Por ejemplo:
BigDecimal precioRound = new BigDecimal(precio).setScale(2, RoundingMode.HALF_UP);
También comentar que se pueden usar clases que no hacen redondeo, que no modifican el
contenido de la variable, pero que muestran al usuario solo los decimales deseados. Ejemplo:
DecimalFormat df = new DecimalFormat ("##.##");
System.out.print(df.format(precio));
Ejemplo:
Random random = new Random(); //solo se hace una vez en el programa
for (i=1; i<= 10; i++) {
int dado= random.nextInt (1,7); //generará números entre 1 y 6
System.out.println(dado);
}
(*) Los métodos que crean instancias de objetos pero no son constructores se llaman
métodos de factoría. Internamente, ellos llaman al constructor.
Métodos generales
• getYear(); getMonth(); // Y otros similares…
• getDayOfWeek(); // Devuelve enumeración: Monday, Tuesday…
• getDayOfWeek().getValue(); // Devuelve 1 para lunes… 7 para domingo.
• fecha1.toString(); // Obtiene una cadena a partir de la fecha
• isLeapYear(); // devuelve true si es bisiesto
• lengthOfMonth();
Podemos combinar los métodos de factoría que crean una fecha y métodos que operan sobre la
fecha. Ejemplos:
1) Obtener la fecha del 1 de enero del año actual.
LocalDate fecha = LocalDate.of(LocalDate.now().getYear(),1,1);
Comparar fechas
Disponemos de métodos que nos permiten saber si la fecha es anterior, igual o posterior a una fecha
pasada como parámetro.
• isEqual (fecha); // devuelve true/false
• isBefore (fecha); // devuelve true/false
• isAfter (fecha); // devuelve true/false
Ejemplo: Introducir por teclado dos fechas en formato AAAA-MM-DD y decir si la primera es anterior
a la segunda:
System.out.println("Introduce fecha 1 (formato:AAAA-MM-DD): ");
String fec1 = teclado.nextLine();
System.out.println("Introduce fecha 2 (formato:AAAA-MM-DD): ");
String fec2 = teclado.nextLine();
LocalDate fecha1 = LocalDate.parse(fec1);
LocalDate fecha2 = LocalDate.parse(fec2);
if (fecha1.isBefore(fecha2))
System.out.println("La primera es anterior");
else if (fecha1.isAfter(fecha2))
System.out.println("La primera es posterior");
else System.out.println("Las fechas son iguales");
Incrementos/decrementos en fechas:
• plus( cant, unidad);
• minus (cant, unidad);
donde:
• cant: cantidad que queremos sumar/restar
• unidad: unidad en la que está esta expresada la cantidad anterior, usando la clase
ChronoUnit. Ejemplos: ChronoUnit.HOURS, MINUTES, DAYS, WEEKS, MONTHS, YEARS
etc. Esta clase necesita import.java.time.temporal.*.
Ejemplos:
1) Introducir por teclado tres enteros que representen año, mes y día. Con ellos construir una fecha
y mostrar la fecha resultante al sumarle 100 días.
System.out.println("Introduce día");
int dia = Integer.parseInt (teclado.nextLine());
System.out.println("Introduce mes");
int mes = Integer.parseInt (teclado.nextLine());
System.out.println("Introduce año");
int año = Integer.parseInt (teclado.nextLine());
LocalDate fecha = LocalDate.of (año,mes,dia);
LocalDate fechaMas90 = fecha.plus(90, ChronoUnit.DAYS);
System.out.printf("Fecha resultante: %s", fechaMas90);
También hay métodos que no necesitan este último parámetro porque va implícito en el
propio nombre del método: plusYears(cant), plusMonths(cant), etc. Por ejemplo, la
última línea de código anterior podría ser:
LocalDate fechaMas90 = fecha.plusDays(90);
2) Introducir por teclado una fecha en formato AAAA-MM-DD y mostrar la fecha de los 10 días
siguientes:
System.out.println("Introduce fecha (formato:AAAA-MM-DD): ");
String fec1 = teclado.nextLine();
LocalDate fecha1 = LocalDate.parse(fec1);
for (int i=1;i<=7;i++) {
LocalDate siguiente=fecha1.plusDays(i);
System.out.println(siguiente); }
dias = fecha1.until(fecha2,ChronoUnit.DAYS);
O también con ChronoUnit.UNIDAD.between, como se muestra a continuación:
long tiempo;
tiempo = ChronoUnit.SECONDS.between(fecHora1,LocalDateTime.now());
En este caso resta la segunda fecha menos la primera, es decir la primera debe ser menor para que
el resultado devuelto no sea negativo.
Ejemplo: Calcular la edad en años de una persona de la que introducimos su fecha de nacimiento:
Formato de fechas
Disponemos de la clase DateTimeFormatter (import java.time.format.*) para dar formato a
las fechas. Por ejemplo, para solicitar al usuario una entrada por teclado en un formato concreto:
DateTimeFormatter formato = DateTimeFormatter.ofPattern("dd/MM/yyyy");
System.out.println("Introduce fecha DD/MM/AAAA: ");
LocalDate fecha = LocalDate.parse(teclado.nextLine(),formato);
Pudiendo incluir el patrón: yyyy, yy,MM, dd, H, m, s, a (am pm) z (zona), DD (dia del año), etc.
Así pues, para asignar fechas desde cadenas, por ejemplo, introducidas por el usuario, tenemos que
usar esta clase para indicarle el formato que el que está, como se ha visto en el ejemplo anterior. De
no indicar formato, debemos introducirlo en forma AAAA-MM-DD.
Representación en texto
Tenemos también enumeraciones que nos muestran en texto los nombres de los meses y días de la
semana, en distintos formatos: nombre completo, solo en 3 letras y solo la inicial (respectivamente:
TextStyle FULL, NARROW, SHORT) y en distintos idiomas (mediante Locale). Para ello debemos usar
las clases Month y DayOfWeek.
Primero debemos instanciar estas clases, podemos hacerlo desde una fecha, o empleado el método
of, al que le pasamos un entero entre 1 y 12 para los meses y un entero entre 1 y 7 para los días de
la semana, o bien mediante una constante. Ejemplos:
Month mes1 = LocalDate.now().getMonth();
Month mes2 = Month.of(3); //marzo
Month mes3 = Month.AUGUST;
DayOfWeek dia1 = LocalDate.parse("2020-01-01").getDayOfWeek();
DayOfWeek dia2 = DayOfWeek.of(3); //miércoles
DayOfWeek dia3 = DayOfWeek.MONDAY;
Luego, para mostrar su valor, usaremos el método getDisplayName, al que le pasamos el formato
deseado y el idioma. El formato puede tomar los valores: TextStyle.FULL, TextStyle.NARROW,
TextStyle.SHORT.
El idioma puede ser una constante Locale.* por ejemplo: Locale.FRENCH para los valores que ya
están definiros o bien crear una instancia de Locale para países/idiomas como español o gallego:
Locale locale = new Locale("es", "ES");
Locale locale = new Locale("gl", "ES");
Ejemplo: muestra el día de la semana en gallego que fue el 31 de diciembre del año 1999.
DayOfWeek dia = LocalDate.of(1999,12,31).getDayOfWeek();
Locale locale = new Locale("gl", "ES");
System.out.println(dia.getDisplayName(TextStyle.FULL, locale));
Mostrando:
venres
Otras clases
Disponemos asimismo de otras clases como:
• Year, YearMonth, etc… con métodos como lengthOfMonth(), isLeapYear(), etc.
• ZonedDateTime para trabajar con zonas horarias.
• Period para trabajar con periodos entre fechas
• Duration para trabajar con la duración entre dos horas.
Getters y Setters
Los getters y setters son métodos de acceso público y se usan para dar acceso a los atributos que
definimos de acceso private. Los getters devuelven el valor del atributo (en inglés, get) y los setters
asignan un valor al atributo (en inglés, set). Ejemplo:
A simple vista, podríamos decir que no son necesarios. Si los atributos los declaramos públicos,
podríamos acceder a ellos directamente. La misión de los getters/setters es ocultar la
implementación de nuestra clase, no permitiendo el acceso directo a nuestros atributos, sino que
somos nosotros los que gestionamos como se acceden y como se modifican. Podríamos incluso
hacer validaciones, formateo, modificaciones, desencadenar otras acciones, etc.
Los IDE más populares suelen generar estos métodos de forma automática. Por ejemplo, Netbeans
lo hace si, sobre la clase, pulsamos las teclas [Alt] + [Ins].
toString
El método toString (), como su propio nombre indica, se utiliza para convertir a String (es decir, a una
cadena de texto) cualquier objeto. Este método está definido para la clase Object, y como todos los
objetos heredan de dicha clase, todos ellos tienen acceso a este método.
Este método está redefinido para muchas clases (se dice “sobreescrito”) como por ejemplo
LocalDateTime o ArrayList mostrando el contenido del objeto de una forma “comprensible” para el
usuario. Así podemos hacer:
System.out.println(LocalDate.now().toString());
De hecho, System.out.println usa por defecto el método toString() para mostrar el contenido de su
parámetro, siempre que esté definido, así pues, el ejemplo anterior podría escribirse así:
System.out.println(LocalDate.now());
Deberíamos, entonces, redefinir el método toString () para las clases que creemos, así facilitaremos
su representación. Si no lo definimos, se mostraría una representación que incluye el nombre de su
clase y una referencia de su ubicación en memoria.
La salida sería:
equals
Es un caso similar al toString () ya que está definido para la clase padre de todos los objetos Object,
sobrescrito en otras clases como String y que debemos sobrescribir para nuestras clases.
Ya hemos utilizado el método equals () previamente, y sabemos que es la forma en que debemos
comparar objetos (los objetos no se pueden comparar utilizando el operador ==). El método
equals está sobrescrito para la mayoría de las clases. Por ello, podemos usarlo directamente para
comparar instancias de String o LocalDate, por ejemplo. Pero ¿qué ocurre en las clases que creamos
nosotros?
Si probamos empleado1.equals(empleado2), al no estar sobrescrito el método para la clase
Empleado, el resultado será incorrecto. Por tanto, para comparar objetos de tipo Empleado debemos
de sobrescribir el método en la clase:
@Override
public boolean equals (Object obj) {
if (this == obj) return true;
if (obj == null) return false;
//if (this.getClass()!= obj.getClass()) return false; mejor:
if (obj instanceof Empleado == false) return false;
Si no sobrescribimos equals () solo devolverá true en caso de que las dos referencias comparadas
compartan la misma ubicación de memoria, es decir apunten a la misma instancia de objeto.
Comparación de variables
Cuando hablamos de tipos primitivos, la comparación de variables es muy sencilla, empleando
operadores lógicos.
Como estamos comentando, en el caso de los objetos no es tan sencillo ya que el identificador
contiene la dirección de memoria del objeto que referencia, así que, tanto para comparar como
asignar estaríamos haciendo la operación con las direcciones de memoria y no con los contenidos
que referencian. A modo de resumen, esta sería la forma de comparar los tipos de datos en Java:
• Tipos primitivos: operadores lógicos, por ejemplo: ==.
• Cadenas (String) y otras clases predefinidas, como por ejemplo LocalDate: método equals
(==compararía direcciones de memoria).
• Clases definidas por nosotros: Hay que definir qué entendemos por dos objetos iguales,
depende del contexto, es decir, en qué consiste que dos instancias sean iguales ¿tienen que
coincidir todos los atributos o solo alguno de ellos? De acuerdo con nuestros criterios, debemos
sobrescribir el método equals heredado de la clase Object.
hashCode
Este método se utiliza en vez de equals en determinadas ocasiones para determinar si un elemento
de una colección de tipo hash (HashSet, HashMap= es igual a otra instancia, por lo que siempre que
sobrescribamos equals() para una determinada clase, deberíamos sobrescribir también hashCode().
Los dos métodos tienen que ir a la par, de forma que si para dos instancias, equals() devuelve true,
hashCode() debe devolver el mismo número, pero lo veremos en detalle en el capítulo dedicado a
Colecciones.
clone
Con la asignación de objetos ocurre algo similar a la comparación. Igual que no podíamos usar ==
para compararlos, no podemos usar el operador de asignación = para copiar un objeto a otro. Para
realizar esta operación debemos redefinir el método clone de la clase Object como se muestra a
continuación.
El código mostrado hace una copia “superficial”, es decir, copia los tipos primitivos, si el objeto tuviese
otras clases entre sus atributos, habría que llamar al método clone de esas sub-clases, antes de hacer
el return.
Las líneas try…catch sirven para capturar los posibles errores en tiempo de ejecución que se
pudiesen producir (ver capítulo “Excepciones” más adelante en este manual).
En el programa del ejemplo, el que hace la copia, hay que fijarse como la llamada a clone() lleva un
casting al objeto destino ya que el tipo devuelto por el método clone es Object.
7. Array y ArrayList
Los arrays (o vectores, o matrices, o “arreglos”) se usan para almacenar varios valores en una sola
variable, en vez de declarar varias variables con distintos nombres, una sola variable (con distintas
posiciones o índices) almacenará todos esos valores. Piensa que tienes que almacenar las edades de
todos los alumnos de una escuela, para calcular la edad media, o el mayor, o el menor…sería
inmanejable.
media = (edad1 + edad2 + edad3 + . . . + edad300 ) / 300
En Java hay dos aproximaciones: el array tradicional, común a prácticamente todos los lenguajes de
programación y la clase ArrayList, que implementa el array como un objeto con métodos
interesantes.
Clase Array
Para trabajar con un array tenemos que hacer los pasos de declaración de la variable, la construcción
(o reserva de memoria) y opcionalmente la inicialización de sus valores, estos tres pasos los podemos
hacer todos juntos o por separado.
Para declarar un array seguimos el formato tipo de dato [ ] nombreVariable, es decir, como una
variable cualquiera añadiendo los corchetes después del tipo.
int[] edad; //también está aceptado int edad[];
String [] meses;
No es legal incluir el tamaño de un array en nuestra declaración, como en otros lenguajes, así pues
esto: int[5] edad; no se puede hacer.
El siguiente paso es la “construcción”, la reserva de memoria en el heap (donde todos los objetos
viven). Para crear un objeto array, se debe saber cuantas posiciones va a tener, cuánto espacio debe
asignar, por lo que debemos especificar el tamaño del array en tiempo de creación. El tamaño de un
array es el número de elementos que el array almacenará. Para ello usamos el operador new.
int[] edad;
edad = new int[10];
y también es válido, y muy frecuente, hacerlo los dos pasos anteriores en una sola instrucción:
int [] edad = new int[10];
String [] meses = new String[12];
siendo int[] edad = new int[]; un error, ya que no sabe cuanta memoria reservar.
El tercer paso, opcional, sería la inicialización, es decir, darles valores a esas posiciones del array que
hemos reservado en una sola instrucción. Sería con la construcción:
int [] edad;
edad = new int[] {18, 20, 32};
Si hacemos los tres pasos en uno, admite también la sintaxis abreviada:
int [] edad = {18, 20, 32};
String [] coches = {"Volvo","BMW","Ford","Mazda"};
Para acceder a los elementos de un array, pondremos el índice de la posición a la que queremos
acceder, teniendo en cuenta que empieza en la posición cero y no uno. Así pues:
System.out.println(coches[1]); mostraría “BMW” por pantalla.
Los arrays tienen un comportamiento como objetos que son, y una propiedad interesante es length
que nos indica la cantidad de elementos del array. Podríamos recorrernos fácilmente un array
entonces con un bucle:
for (int i = 0; i < coches.length; i++)
System.out.println(coches[i]);
y también para inicializar cada una de sus posiciones:
for (int i = 0; i < coches.length; i++) {
System.out.println("Introduce nombre coche");
coches[i]= (new Scanner (System.in)).nextLine();
}
Ojo!! length es un atributo, no un método (sin paréntesis al final). La clase String dispone
de un método llamado length() para obtener la cantidad de caracteres de la cadena, no
confundir.
Si piensas en que hubiese 100 marcas de coches ya te das cuenta que los arrays son imprescindibles
en cualquier lenguaje de programación para almacenar en memoria datos similares, poder
recorrerlos y realizar operaciones sobre todos ellos. Otro ejemplo, sobre el array previo de edades,
podría ser calcular la edad media.
float media = 0;
for (int i = 0; i < edad.length; i++) media += edad[i];
media = media / edad.length;
Al ser una operación tan habitual, el recorrido de un array se puede hacer con otra sintaxis más
sencilla “for-each”, propia de Java, por ejemplo, para mostrar todas las marcas de coches del array
definido previamente:
for (String c : coches)
System.out.println(c);
El “for” se leería como: para cada cadena “c” del array coches haz…
Hay que tener presente que, recorriendo el array de esta forma, no se puede modificar el contenido
del array, no funcionaría:
for (String c : coches) c = "Opel";
Importante: La clase Array no permite cambiar el tamaño del array en tiempo de ejecución una vez
definido. Si se necesita que ese tamaño pueda ir variando a lo largo de la ejecución del programa,
debe dársele un tamaño suficientemente grande para todos los casos o bien emplear la clase
Arraylist.
Sin embargo, en el momento de crear un objeto Array el tamaño no tiene que estar definido por
un literal, puede ser el valor de una variable que toma valor en tiempo de ejecución. Ejemplo:
int[] miArray;
int tamañoArray;
tamañoArray = (new Scanner(System.in)).nextInt();
miArray = new int [tamañoArray];
2.- Añadirle al programa anterior el código para que sume los elementos del array. Versión con
contador que recorre el array:
int suma=0;
for (int i=0; i<lista.length;i++)
suma+=lista[i];
System.out.printf("La suma es %d%n", suma);
3.- Añadirle al programa anterior el código para que sume los elementos del array. Versión con
for…each:
int suma=0;
for (int elemento: lista)
suma+=elemento;
System.out.printf("La suma es %d%n", suma);
4.- Añadirle al programa anterior él código para que calcule la media de los valores en las posiciones
pares (como for…each recorre todo el array, empleamos mejor el recorrido con contador):
int suma=0, cont=0;
for (int i=0; i<lista.length;i+=2) {
suma+=lista[i];
cont++;
}
System.out.printf("La media es %.2f%n", (double)suma/cont);
5.- Partiendo de la clase simple (supongamos atributos públicos para que sea más sencillo):
Public class Persona {
public String nombre;
public int edad;
Búsqueda en Arrays
Existen dos formas de buscar elementos en un Array, la primera sería secuencial, y consistiría en ir
recorrer el Array de principio a fin hasta encontrar el elemento buscado: es lenta pero no necesita
que el array esté ordenado. A nivel de programación es muy sencilla, es un recorrido con un bucle
bien hasta el final si no lo encuentra, bien hasta que lo encuentre. Una primera versión podría ser
con un for + break:
int[] miArray = {10, 20, 12, 1, 2, 3 };
int num = (new Scanner(System.in)).nextInt();
boolean encontrado = false;
for (int i=0; i< miArray.length; i++){
if (num == miArray[i]) {
encontrado = true;
break;
}
}
System.out.println(encontrado?"encontrado":"no encontrado");
El segundo tipo de búsqueda sería dicotómica, necesita que el array está ordenado, pero es mucho
más eficiente. Comienza buscando el elemento en la mitad del array, y si el elemento buscado es
menor, pasa a buscar ahora en la mitad inferior del array, y si es mayor busca en la mitad superior,
repitiendo el proceso sucesivas veces. Cada vez tendremos intervalos de búsqueda más pequeños
hasta que no se pueda dividir más o lo encuentre. La siguiente figura explica el proceso:
Ordenación de Arrays
La clase ArrayList que veremos a continuación dispone de un método que ordena los elementos del
array por lo que el apartado que vamos a ver ahora no sería técnicamente necesario, simplemente
pasaríamos a trabajar con la clase ArrayList en vez de la clase Array, sin embargo, los algoritmos de
ordenación son un elemento básico en el conocimiento y maestría de la programación por lo que
vamos a estudiarlo.
Existen diferentes métodos de ordenación, algunos más rápidos, otros más intuitivos, etc. Puedes
consultarlos aquí: https://es.wikipedia.org/wiki/Algoritmo_de_ordenamiento
A modo de ejemplo solo vamos a ver uno de ellos, el algoritmo de selección, ya que es muy intuitivo
y sencillo de programar. Su funcionamiento es el siguiente:
• Buscar el mínimo elemento de la lista e intercambiarlo con el primero
• Buscar el siguiente mínimo en el resto de la lista e intercambiarlo con el segundo
• Repetir el proceso hasta el penúltimo elemento de la lista.
El programa tiene dos bucles, el primero para recorrer el Array y el segundo para buscar un elemento
menor que el que estamos tratando en el primer bucle. Cuando acaba la búsqueda (desde el
elemento siguiente al actual hasta el final) hace el intercambio entre el actual del primer bucle con
el mínimo encontrado.
public class Ejemplo {
public static void main (String args[]) {
int[] miArray = {61, 120, 32, 41, 52, 93 }; int posMin;
for(int i=0; i< miArray.length-1; i++){
//busqueda del menor
posMin = i;
for(int j=i+1 ; j< miArray.length; j++){
if(miArray[j] < miArray[posMin]) {posMin=j;}
}
//intercambio del actual i con el menor
int aux = miArray[i];
miArray[i]=miArray[posMin];
miArray[posMin]= aux;
}
//Mostrar resultado
for(int i = 0 ; i< miArray.length; i++)
System.out.println(miArray[i]);
}
}
En el ejemplo anterior, efectivamente copia ahora referencia a edad, sería como un alias de edad (se
habrán perdido los valores 1,2,3,4,5,6) y además, cualquier cambio en un elemento de edad, sería
también un cambio en copia, y viceversa. Prueba a hacer copia[0]=99; y luego muestra edad[0].
Para copiarlos deberíamos recorrer el primero y asignarlos uno a uno, o de una forma más rápida,
mediante el método System.arraycopy, que tiene la siguiente estructura:
System.arraycopy (array origen, posición inicial, array destino, posición inicial destino, cantidad de
elementos a copiar). En nuestro ejemplo:
System.arraycopy(edad,0,copia,0,edad.length);
Lo mismo nos ocurre cuando queremos comparar arrays, si hacemos algo del tipo if (edad == copia)
estaremos comparando las direcciones de memoria del comienzo de ambos arrays, no
compararemos cada uno de sus elementos. Para ello tenemos el método: Arrays.equals
(nombreArray1, nombreArray2), que devuelve true si son iguales y false en caso contrario.
if (Arrays.equals (edad,copia) == true) . . .
Arrays Bidimensionales
Los arrays pueden ser definidos de varias dimensiones (por ejemplo, de dos dimensiones, tendrían
forma de tabla, como una hoja de cálculo, con filas y columnas).
Y para recorrerlo haría falta un doble bucle, uno primero para recorrer las filas y dentro de cada fila
otro para recorrer las columnas de cada fila.
El siguiente ejemplo define un array de 4 filas y 3 columnas y lo recorre:
int[][] notas = { {10, 9, 10}, {0, 0, 1}, {6, 7, 6}, {5, 5, 4} };
for (int fila = 0; fila < 4; fila++) {
for(int col = 0; col < 3; col++) {
System.out.println(notas[fila][col]);
} }
Un array bidimensional es en realidad un array unidimensional, en el que cada elemento es una fila completa y
por tanto otro array. Así pues, en este ejemplo el límite del primer bucle que hemos puesto 4, podríamos haber
puesto: notas.length, esto es, la cantidad de elementos de ese array de filas, por lo tanto, la cantidad de filas.
Y ya que cada fila es considerada un array unidimensional, notas[0] sería el array de la primera fila, notas[1]
sería el array de la segunda fila, etc. y por ello podemos usar en el bucle interior la condición notas[i].length
en vez de 3. Piénsalo con calma…
También podríamos recórrelo usando la forma “for-each” pero quizás sea más confusa:
for(int[] arr: notas) {
for(int val: arr)
System.out.println(val);
}
• Mostrar el array anterior con todos los elementos de una fila en la misma línea:
for (int fila=0;fila<3;fila++){
for (int col=0;col<6;col++){
System.out.printf("%05.2f ",arr[fila][col]);
}
System.out.println(""); //al final de cada fila, salto de línea
}
Para ejecutar desde Netbeans, clicamos botón derecho sobre el proyecto y, en Propiedades del
Proyecto, en la sección Ejecutar: comprobamos que la entrada ’clase main’ contiene el nombre del
paquete+programa que queremos ejecutar, y en ’argumentos’ ponemos separados por espacios
en blanco los argumentos que necesita el programa. Para ejecutar pulsamos F6, o bien Ejecutar
proyecto, pero no May+F6 (Ejecutar archivo actual) como sí podemos hacer en otros casos.
Clase ArrayList
La clase ArrayList en Java, es una clase que permite almacenar datos en memoria de forma similar a
los Arrays, con la ventaja de que el número de elementos que almacena, lo hace de forma dinámica,
es decir, que no es necesario declarar su tamaño como pasa con los Arrays. Además, ofrece muchos
métodos que hacen el trabajo mucho más sencillo que con Arrays.
La forma de definir un ArrayList sería: ArrayList <Object> miarray
Y el constructor: new ArrayList <> ();
Pudiendo hacerse en una sola instrucción:
ArrayList <Object> miarray = new ArrayList <> ();
Debiendo ser Object una clase propia de Java o una clase definida por nosotros mismos. Esta clase
está incluida en la librería util por lo que debemos hace un import java.util.ArrayList; en
nuestro proyecto.En el constructor, se recomienda la sintaxis propuesta: new ArrayList <> ()
aunque puedas ver también: new ArrayList <Object>() o bien new ArrayList()
Métodos de ArrayList
Los ArrayList tienen métodos muy útiles que facilitan el trabajo respecto a los clásicos Arrays.
remove(X) (*) Si encuentra el objeto X, elimina su primera ocurrencia devolviendo true (si
no lo encuentra, false). También puede borrar por posición. Al borrar no deja
remove (posición)
“hueco” de forma que todas las posiciones superiores a la borrada se
desplazan a la izquierda (para recorrer un ArrayList y eliminar algunas de sus
posiciones será aconsajable usar un Iterator como veremos más adelante ya
que al borrar cambia el tamaño del ArrayList)
for (int i=0; i< arrData.size();i++)
if(arrData.get(i)=999) {arrData.remove(i);}
(*) Para que funcionen tanto remove (objeto) como contains(objeto) tiene que estar definido el método
equals() para la clase, ya que es lo que usa para comparar. String, LocalDate lo tienen definido, pero
para clases definidas por nosotros mismos, habrá que definirlo.
Puedes consultar todos los métodos en:
https://docs.oracle.com/javase/9/docs/api/java/util/ArrayList.html#method.summary
equals y ArrayList
Como acabamos de ver la clase ArrayList dispone del método indexOf (elemento_a_buscar) que
devuelve la posición de primera vez que se encuentra el elemento a buscar o -1 si no lo encuentra.
En caso de que el ArrayList sea de tipos primitivos, o clases que tienen implementado el método
equals () funciona perfectamente:
ArrayList <String> arr = new ArrayList<>();
arr.add("aa"); arr.add("cc");arr.add("bb");arr.add("gg");
int pos = arr.indexOf("bb");
if (pos!=-1)System.out.print("Encontrado!");
Pero cuando buscamos en una clase definida por nosotros esto no es así, por lo que si no
sobrescribimos equals () los métodos indexOf, contains o remove no funcionarán.
En el siguiente ejemplo, definimos lo que es la igualdad en la clase Alumno que, en este caso, se
corresponde con la igualdad en el nombre.
public class Alumno {
String nombre;
int edad;
public Alumno(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad; }
@Override
public int hashCode() {
int hash = 7;
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (obj instanceof Alumno == false) return false;
Alumno other = (Alumno) obj;
if (this.edad == other.edad && this.nombre.equals(other.nombre)) return true;
return false;
}
}
También funcionaría en una ArrayList de dos dimensiones, pero recordemos que en realidad un
ArrayList de 2 dimensiones es un ArrayList de ArrayList, por lo que la búsqueda hay que hacerla para
cada ArrayList (lo que sería para cada “fila” del array de dos dimensiones). Sería algo así:
Operaciones Un Arraylist
Añadir elementos a un ArrayList
• Se puede hacer mediante el método add(). En el siguiente ejemplo, en un bucle for leemos 10
cadenas y las añadimos a un arrayList. Podríar ser en un while, do…while, etc.
ArrayList <String> miLista = new ArrayList <> ();
for (int i=1; i<=10; i++){
System.out.prinln ("Introduzca una cadena a insertar");
String cadena = scanner.nextLine();
miLista.add(cadena);
}
Recorrer el ArrayList
• Usaremos el método get(). Podemos recorrerlo de forma clásica con un bucle for. Sobre el
arraylist anterior:
for(int i=0;i< miLista.size();i++)
System.out.println(miarray.get(i));
• O también, si lo vamos a recorrer completo en orden ascendente con un bucle for each y ya
no es necesario get().
for( String cadena: miLista)
System.out.println(cadena);
• Utilizando un objeto Iterator (esta técnica se verá en detalle, más adelante en el curso, en el
tema 15). Iterator tiene como métodos:
o hasNext(): devuelve true si hay más elementos en el array.
o next(): devuelve el siguiente objeto contenido en el array.
Ejemplo:
ArrayList<Integer> numeros = new ArrayList <>();
//una vez insertados elementos. . .
Iterator <Integer> iterator = numeros.iterator(); //se crea el iterador
while(iterator.hasNext()) //mientras haya elementos
if (condición) iterator.remove()); //se obtienen y se borran
Lo habitual será usar for each, para recorridos completos, for (int i… para recorridos específicos (solo
algunas posiciones, orden descendente, etc.) y los iteradores para borrados, ya que no se pueden
usar los dos métodos anteriores.
Insertar y modificar elementos de un Arraylist
Para insertar un elemento en un ArrayList disponemos de los métodos add (objeto) que añade el
objeto al final y devuelve true y add (posición, objeto) que añade el objeto en la posición indicada.
Para modificar un elemento utilizamos el método set (posición, Objeto).
La diferencia fundamental entre set y add (cuando le especificamos la posición) es que set sustituye
el valor de esa posición perdiendo el valor que hubiese previamente, y add desplaza a la derecha
todos los elementos desde esa posición, de forma que no se pierde el valor que hubiese en la
posición indicada.
Es importante recordar que como lo que se inserta o añade es un objeto, primero hay que llamar al
constructor de ese objeto, por ejemplo, sería incorrecto:
Persona p;
ArrayList<Persona> lista = new ArrayList <>();
lista.add (p);
O bien:
ArrayList<Persona> lista = new ArrayList <>();
lista.add (new Persona() ); //depende de los constructores de Persona
Ordenar un ArrayList
Arraylist de un objeto simple creado por Java como pueda ser String, LocalDate, etc. se puede
ordenar alfabéticamente de forma sencilla mediante la clase Collection (debemos hacer un: import
java.util.Collections; ):
ArrayList <Miclase> araylist1= new ArrayList<>();
. . .
Collections.sort(arraylist1);
Si es una clase con varios atributos, y queremos ordenar por uno concreto debemos añadirle un
segundo parámetro al método sort, como explicaremos más adelante, en el capítulo 15, cuando
hablemos de Comparable y Comparator. Aquí lo dejamos anotado simplemente por tener la
referencia.
ArrayList <Miclase> araylist1= new ArrayList<>();
. . .
Collections.sort(arraylist1, new Comparator<Miclase>() { (*)
@Override
public int compare(Miclase x1, Miclase x2) {
return x1.AtributoOrdenacion.compareTo(x2.AtributoOrdenacion);
}}
);
Ese segundo parámetro de sort es una clase que llamaremos clase anónima.También
podremos emplear una función Lambda o una referencia a métodos. Son todo conceptos
avanzados que veremos más adelante.
Copiar un Arraylist
El nombre de un ArrayList contiene la referencia al ArrayList, es decir, la dirección de memoria donde
comienza el ArrayList, igual que sucede con los arrays estáticos.
Si disponemos de un ArrayList de enteros llamado ventas:
La instrucción:
ArrayList<Integer> ventas1 = ventas;
No copia el array ventas en el nuevo array ventas1 sino que crea un alias:
De esta forma tenemos dos formas de acceder al mismo ArrayList: mediante la referencia ventas y
mediante la referencia ventas1.
Para hacer una copia podemos hacerlo de forma manual elemento a elemento o se puede pasar la
referencia del ArrayList original al constructor del nuevo:
ArrayList<Integer> ventas1 = new ArrayList <>(ventas);
En el siguiente ejemplo, combina las dos funcionalidades, se le pasa como parámetro un ArrayList y
devuelve un ArrayList. Lo que hace el programa es invertir el contenido de un ArrayList.
public static void main(String[] args) {
ArrayList <Integer> num = new ArrayList <>();
num.add(1); num.add(2); num.add(3);
num = invertir(num);
}
ArrayList bidimensionales
Un ArrayList es un array unidimensional, pero con ellos podemos simular arrays de dos o más
dimensiones anidando ArrayLists.
Para crear una matriz lo que creamos es un ArrayList cuyos elementos son a su vez ArrayList. Esto
se puede extender sucesivamente y obtener arrays de más dimensiones. Ejemplo:
ArrayList<ArrayList<Integer>> miArray = new ArrayList <>();
Una vez creado, hay que pensar que cada elemento de miArray es a su vez un array, por lo que para
añadir un elemento sería así: array.add(new ArrayList< >());
Ahora, para acceder a los elementos finales (fila, columna) sería así:
array.get(fila).add(valor);
array.get(fila).get(columna);
Clase Collections
Esta clase (no confundir con la interfaz Collection) es una clase con un conjunto de métodos estáticos
que permiten operar sobre distintas colecciones como pueden ser los ArrayList (y otras que veremos
en capítulos posteriores) que pueden ser muy útiles y que los usaremos frecuentemente.
• Collections.max(arraylist1) Devuelve el máximo elemento de arraylist1, de acuerdo al
orden natural de sus elementos. También disponemos del análogo min(arralist1).
• Collections.reverse(arraylist1) Invierte todos los elementos del ArrayList. No devuelve
nada.
• Collections.shuffle(arraylist1) Intercambia aleatoriamente todos los elementos del
ArrayList. No devuelve nada.
• Collections.frequency(arraylist1, obj) Devuelve el número de veces que aparece el
objeto obj en arraylist1.
Además de los ya vistos previamente:
• Collections.binarySearch(arralist1,obj)Busca el objeto obj en arraylist1 y devuelve
posición. Si no encuentra, devuelve < 0. El ArrayList debe estar ordenado.
• Collections.sort(arraylist1) ordena arraylist1 ascendentemente. También disponemos
del método reverseOrder () para ordenar descendentemente como ya mencionamos
previamente.
Puedes ver la lista completa en: https://docs.oracle.com/javase/7/docs/api/java/util/Collections.html
Nota:
ArrayList tiene definido el método toString () por lo que podemos hacer
System.out.println (miArraylist) pero no así en el caso de Array, que debeamos emplear
el método estático Arrays.toString para convertirlo previamente en en String y poder
hacer System.out.println(Arrays.toString(miArray) ).
.
8. Clases y Herencia
Conceptos Básicos
En el tema anterior vimos los fundamentos de las clases y objetos. Resumimos elementos básicos
de la orientación a objetos:
• Clase: es la descripción de una identidad que queremos modelar e incluye atributos y
métodos.
• Objeto: es la instancia de una clase. Se genera invocando a un constructor de la clase.
• Atributo: es cada una de las características que conforman una clase.
• Método: representa una acción o comportamiento de una clase. Se les pueden pasar
parámetros y pueden devolver un valor.
• Constructor: son unos métodos especiales que se encargan de crear los objetos. Toda clase
debe tener alguno. Si no lo creamos, Java lo hace por nosotros de forma implícita. No
devuelven ningún valor, no puede ser static, ni final ni abstract.
• Modificadores de acceso: definen quien puede acceder a la clase/método/atributo al que
califican: public, private, protected y por defecto. Las clases sólo pueden ser: public o por
defecto. El nombre de una clase pública tiene que coincidir con el nombre del fichero en el
que se guarda.
• Atributos y métodos estáticos: Se aplican a la clase, no a una instancia de la clase.
• Inicialización por defecto y valor null: cuando declaramos una variable, pero no la inicializamos,
Java la inicializa automáticamente. En el caso de los tipos primitivo escoge un valor por defecto,
por ejemplo 0 para los numéricos como int, false para boolean, etc. En el caso de los objetos, se
inicializa con un valor o referencia llamado null.
Principios de la POO
Existe un acuerdo acerca de qué características contempla la “orientación a objetos”. Las
características siguientes son las más importantes:
Abstracción
La abstracción consiste en aislar un elemento de su contexto o del resto de los elementos que lo
acompañan. Así expresaremos las características esenciales del objeto y su comportamiento
esencial, eliminando lo superfluo. Ejemplo: ¿Qué características podemos abstraer de un coche? o
¿Qué características y comportamientos semejantes tienen todos los coches? Características: Marca,
Modelo, Matrícula… Comportamiento: Acelerar, Frenar, Retroceder...
Encapsulamiento
Significa reunir todos los elementos que pueden considerarse pertenecientes a una misma entidad,
al mismo nivel de abstracción. También se suele asociar con el principio de ocultación,
principalmente porque se suelen emplear conjuntamente. Esto es, solo se puede acceder a las
características y comportamientos de un objeto mediante las operaciones permitidas, sin conocer
realmente su funcionamiento interno. Así el usuario puede centrarse en qué hace y no como lo hace.
Herencia
Las clases no se encuentran aisladas, sino que se relacionan entre sí, formando una jerarquía de
clasificación. Los objetos heredan las propiedades y el comportamiento de todas las clases a las que
pertenecen. La herencia organiza y facilita el polimorfismo y el encapsulamiento, permitiendo a los
objetos ser definidos y creados como tipos especializados de objetos preexistentes. Por ejemplo, en
el juego del ajedrez una clase Pieza tendrá unos atributos (como su color o posición en el tablero) y
unos métodos. Una clase “hija” como la clase “peón” heredará esos atributos y métodos, podrá tener
nuevos métodos o redefinir alguno de su clase padre.
Polimorfismo
Las definiciones habituales de polimorfismo son bastante difíciles de entender, por ejemplo:
“Comportamientos diferentes, asociados a objetos distintos, pueden compartir el mismo nombre; al
llamarlos por ese nombre se utilizará el comportamiento correspondiente al objeto que se esté
usando.”
“El polimorfismo se refiere a la propiedad por la que es posible enviar mensajes sintácticamente
iguales a objetos de tipos distintos. El único requisito que deben cumplir los objetos que se utilizan
de manera polimórfica es saber responder al mensaje que se les envía”.
Podríamos decir que el polimorfismo es una “relajación” del sistema de tipos, de tal manera que una
referencia a una clase (por ejemplo, una variable) acepta referencias de objetos de dicha clase y de
sus clases derivadas (hijas, nietas, etc.).
Después de ver los apartados de herencia y polimorfismo, ya con casos concretos, podremos volver
a esta definición y la entenderemos perfectamente.
Modularidad
Se denomina “modularidad” a la propiedad que permite subdividir una aplicación en partes más
pequeñas (llamadas módulos), cada una de las cuales debe ser tan independiente como sea posible
de la aplicación en sí y de las restantes partes. Estos módulos se pueden compilar por separado, pero
tienen conexiones con otros módulos. Al igual que la encapsulación, los lenguajes soportan el
modularidad de diversas formas.
Principio de ocultación
Cada objeto está aislado del exterior, es un módulo natural, y cada tipo de objeto expone una
“interfaz” a otros objetos que especifica cómo pueden interactuar con los objetos de la clase. El
aislamiento protege a las propiedades de un objeto contra su modificación por quien no tenga
derecho a acceder a ellas; solamente los propios métodos internos del objeto pueden acceder a su
estado. Esto asegura que otros objetos no puedan cambiar el estado interno de un objeto de manera
inesperada, eliminando efectos secundarios e interacciones inesperadas. Algunos lenguajes relajan
esto, permitiendo un acceso directo a los datos internos del objeto de una manera controlada y
limitando el grado de abstracción.
Recolección de basura
La recolección de basura (garbage collection) es la técnica por la cual el entorno de objetos se encarga
de destruir automáticamente, y por tanto desvincular la memoria asociada, los objetos que hayan
quedado sin ninguna referencia a ellos. Esto significa que el programador no debe preocuparse por
la asignación o liberación de memoria, ya que el entorno la asignará al crear un nuevo objeto y la
liberará cuando nadie lo esté usando.
Mensajes
Un mensaje es una comunicación o solicitud que le hacemos a un objeto para que actúe según su
comportamiento definido. En términos prácticos consiste en la invocación de un método de un
objeto.
Herencia
La herencia es uno de los 4 pilares de la programación orientada a objetos (junto con la Abstracción,
Encapsulación y Polimorfismo). Al principio cuesta un poco entender estos conceptos característicos
del paradigma de la POO porque solemos venir de otro paradigma de programación como el
paradigma de la programación estructurada, pero se ha de decir que la complejidad está en entender
este nuevo paradigma y no en otra cosa.
Una posible definición: “La herencia es un mecanismo que permite la definición de una clase a partir
de la definición de otra ya existente. La herencia permite compartir automáticamente los métodos y
atributos entre clases y sus clases sucesoras.
Ejemplo: vamos a modelar los distintos tipos de alumnos que hay en un instituto:
• Alumno ESO: nos interesa saber su nombre, DNI, faltas en el curso y teléfono de los padres para
llamar cuando hay una falta de asistencia. Se tendrá que hacer periódicamente un recuento de
faltas porque si falta más 30 sesiones, habrá que llamar a los servicios de Asuntos Sociales del
ayuntamiento.
• Alumno Ciclos: nos interesa saber su nombre, DNI, faltas en el año, empresa donde hará las
prácticas y email para notificarle cualquier aviso. Si falta más de 50 sesiones se le da de baja.
A nivel de código, tendremos lo siguiente:
Las flechas indican código idéntico en ambas clases., repetimos mucho código ya que las dos clases
tienen métodos y atributos comunes, de ahí decimos que la herencia consiste en “sacar factor
común” para no escribir código de más, por tanto lo que haremos será crear una clase con el “código
que es común a las tres clases” (a esta clase se le denomina en la herencia como “Clase Padre o
Superclase o Clase Base”) y el código que es especifico de cada clase, lo dejaremos en ella, siendo
denominadas estas clases como “Clases Hijas, Subclases o Clases Derivadas”, las cuales heredan de
la clase padre todos los atributos y métodos públicos o protegidos.
Es muy importante decir que las clases hijas no van a heredar nunca los atributos y métodos privados
de la clase padre, solo heredamos elementos public y protected (en realidad los atributos y métodos
privados si se heredan, pero no están accesibles). A nivel de código, las clases quedarían
implementadas de la siguiente forma:
Nota: vuelve al tema 6 a repasar los modificadores de acceso: public, protected, private y default.
Ahora queda un código mucho más limpio, estructurado y con menos líneas de código, lo que lo hace
más legible, y lo que todavía es más importante es que es un código reutilizable, lo que significa que
ahora si queremos añadir más clases a nuestra aplicación como por ejemplo una clase
AlumnoPrimaria, lo podemos hacer de forma muy sencilla ya que en la clase padre tenemos
implementado parte de sus datos y de su comportamiento y solo habrá que implementar los
atributos y métodos propios de esa clase.
Encontramos dos palabras reservadas nuevas:
• extends: Como ya imaginaremos, indica a la clase hija cual va a ser su clase padre, en el
ejemplo, la clase AlumnoESO tiene como padre a Alumno, de forma que la primera hereda todos
sus atributos y métodos públicos o protegidos.
• super: es una llamada al constructor de la clase padre. Si hacemos super () o llamaría al
constructor por defecto de la clase padre, esto es a: Alumno(), y en el caso del ejemplo, super
(nombre, dni) llama al constructor de dos parámetros Alumno (String nombre, String dni).
• Con super () también podremos llamar a métodos de la clase padre, por ejemplo
super.nombreMétodo (parámetros del método padre);
La creación de instancias de cada clase se hace como si no hubiese herencia, en nuestro caso
podríamos hacer un programa con el siguiente código:
Prueba a codificar todo esto a ver qué salida obtienes. Puedes juntar en un único archivo las clases
definidas y el programa que contenga un main() como el que acabamos de mostrar.
Aunque una clase pública tiene que ir en un archivo independiente, con el mismo nombre que la
clase y extensión .java, podemos crear varias clases sin modificador de acceso en un mismo archivo
e incluir el programa que las usa, el nombre del archivo será el del programa (.java). Estas clases
solo serán accesibles desde otras clases/programas del mismo paquete (modificador de acceso
por defecto) pero es una forma cómoda de hacer nuestros ejercicios.
Constructores y herencia
Viendo el ejemplo podemos establecer las siguientes consideraciones:
• Es posible que tanto las superclases como las subclases tengan sus propios constructores,
entonces ¿qué constructor es responsable de construir un objeto de la subclase, el de la
superclase, el de la subclase o ambos? El constructor de la superclase construye la porción de
la superclase del objeto, y el constructor para la subclase construye la parte de la subclase.
• Esto tiene sentido porque la superclase no tiene conocimiento ni acceso a ningún elemento en
una subclase. Por lo tanto, su construcción debe estar separada.
• Cuando solo la subclase define un constructor, el proceso es sencillo: simplemente construye el
objeto de la subclase. La porción de superclase del objeto se construye automáticamente
utilizando el constructor predeterminado de la superclase.
• this () se usa para llamar a uno de sus constructores (depende del número de parámetros que
le pasemos llamará a uno o a otro). Es decir, desde un constructor puedo llamar a otro de la
misma clase.
• super () se usa para llamar a uno de los constructores de la clase padre (depende del número
de parámetros que le pasemos llamará a uno o a otro). Es decir, desde un constructor de la
clase hija puedo llamar a cualquiera de los constructores de su padre, incluyendo el constructor
por defecto.
• El constructor de una clase hija siempre llama al constructor de la clase padre. Si no se indica
explícitamente, se llama al constructor sin argumentos de forma implícita.
• En un constructor solo puede haber un super () o un this () y obligatoriamente tiene que ir en la
primera línea del constructor.
• Los constructores no son heredados por subclases, pero el constructor de la superclase puede
invocarse desde la subclase con super ().
• Si queremos impedir la herencia en una clase, es decir, que no se puedan derivar clases hijas, le
asignaremos el modificador final. Por ejemplo, si no queremos permitir clases hijas de
AlumnoCiclos como podría ser AlumnoCicloMedio.
public final class AlumnoCiclos extends Alumno {......}
class Padre {
public String nombre;
El error consiste en que en la primera línea del constructor hijo no hay una llamada explícita al
constructor padre mediante super () por lo que se llama al constructor por defecto Padre(){}, pero
este constructor no existe. Para que Java cree ese constructor por defecto en el padre no debe haber
ningún otro constructor.
El error se podría solucionar de dos formas diferentes.
a) En la primera línea del constructor hijo, llamar al constructor padre que sí existe, por ejemplo,
así: super("noname");
b) Crear en la clase padre, el constructor por defecto: Padre (){}
No producirían el mismo resultado las dos soluciones, pero ambas evitarían el error de compilación.
La sobre escritura de un método consiste en escribir un método en la subclase que tenga la misma
“firma” que el de la superclase y también que devuelva el mismo tipo de dato de retorno (o al menos
un subtipo de este). En este caso, para la subclase, no se ejecutaría el método padre, se ejecutaría el
sobrescrito en ella misma.
La firma o signatura de un método es su nombre y los parámetros que recibe. Cuando decimos
que dos métodos tienen la misma firma nos referimos a que tiene el mismo nombre y el mismo
número de parámetros y éstos con el mismo tipo de datos.
En nuestro caso, vemos que una nueva falta tiene un tratamiento común para ambas clases hijas
(incrementar el atributo faltas) pero luego hay un tratamiento distinto del límite de faltas en cada
una de las subclases. Ese incremento lo podríamos hacer en la clase padre:
Y en las clases hijas podemos volver a definir el método, llamando primero al método del padre y
luego hacer las tareas propias del método hijo.
Así en la clase AlumnoESO tendríamos:
Las anotaciones son metadatos que se pueden asociar a clases, miembros, métodos o
parámetros. No cambian las acciones de un programa, pero dan información sobre el elemento
que tiene la anotación y permiten definir cómo queremos que sea tratado por distintas
herramientas (en la compilación, documentación, ejecución, etc.) Comienzan siempre por @.
Algunas de las más comunes son @Override, @SuprressWarnings, @Deprecated, etc.
getClass y instanceof
getClass es un método que devuelve la clase en tiempo de ejecución del objeto sobre el que se llama.
Si lo imprimimos, muestra una referencia que incluye el nombre de la clase y el paquete en el que se
encuentra, pero podemos obtener más información, como el nombre concreto de la clase (sin el
paquete):
obj.getClass().getSimpleName();
El operador instanceof es parecido al método anterior, y nos permite preguntar el tipo de la variable.
Ejemplo:
if (a3 instanceof AlumnoCiclos)
System.out.println ( ((AlumnoCiclos)a3).empresa );
instanceof devuelve un valor boolean indicando si la instancia pertenece a la clase o no. Hay
que resaltar que también devuelve true para todas las clases ancestro (padre, abuelo, etc.) de
la clase a la que pertenece.
En nuestro ejemplo, si la variable ‘a3’ es un ‘AlumnoCiclos’:
a2 instanceof AlumnoCiclos se evaluará como true, pero, además:
a2 instanceof Alumno también será true. Finalmente:
a2 instanceof AlumnoESO Obviamente será false.
Para saber la clase exacta de una instancia en vez de instanceof es mejor emplear
getClass().getSimpleName(). Puedes investigar en internet su uso.
Java permite estas asignaciones y ofrecen mucha flexibilidad. Por ejemplo, podríamos crear
un ArrayList de alumnos, y añadir tanto alumnos de la ESO como de ciclos. Lo veremos más
adelante. Por ahora quedarnos solo con la idea de que podemos encontrárnoslo.
• this.: En capítulos anteriores vimos cómo, en para los métodos de una clase, podíamos
preceder con this. a los atributos de la misma. Pues bien, seguiremos haciendo lo mismo,
incluso cuando esos atributos no hayan sido creados en esa clase si no que provengan de su
superclase, esto es, su clase padre.
• Getters/Setters: aunque una subclase no hereda los miembros privados de su clase principal,
si la superclase tiene métodos públicos o protegidos (como getters y setters) para acceder a
esos atributos privados, estos métodos sí pueden ser utilizados por la subclase y así, acceder
a los miembros privados. Esta es una situación muy habitual.
Como resumen:
9. Polimorfismo
Tipos de Polimorfismo
Como primera aproximación podríamos ver alguna definición comentada ya previamente, aunque
con ejemplos prácticos lo entenderemos mucho mejor.
“Comportamientos diferentes, asociados a objetos distintos, pueden compartir el mismo nombre; al
llamarlos por ese nombre se utilizará el comportamiento correspondiente al objeto que se esté
usando.”
El polimorfismo hace referencia a “tener varias formas” y en la programación orientada a objetos,
tiene varias vertientes:
- Polimorfismo puro.
- Polimorfismo ad hoc o sobrecarga.
- Polimorfismo de inclusión (redefinición o sobrescritura).
- Polimorfismo paramétrico (generalidad).
En realidad, no nos importa tanto la nomenclatura ni las definiciones, como su funcionalidad en la
programación y las ventajas que nos reporta. Vamos a ver a continuación las dos primeras, la tercera
ya la hemos visto en la sobrescritura de métodos del capítulo anterior, y la última la dejamos para el
siguiente trimestre, bajo el título de “Genéricos”.
Polimorfismo Puro
Ya sabemos que una variable de tipo objeto en realidad contiene una referencia a memoria donde
realmente está almacenado el objeto al que apunta. También sabemos que una vez que declaramos
una variable de un determinado tipo (de tipo objeto o tipo primitivo) no se puede cambiar ese tipo.
Por ejemplo, una variable llamada a1 de tipo Alumno no puede ser más delante de tipo
TelefonoMovil. Y, por último, sabemos también que el valor referenciado por una variable puede ir
cambiando en un programa (salvo que la declaremos como final), es decir la variable a1 puede
apuntar a un alumno, y más tarde en el programa a otro alumno.
Lo nuevo que vamos a ver ahora es que una variable de un determinado tipo puede referenciar
objetos de este tipo, pero también objetos de clases hijas de ese tipo, por eso decimos que es
polimórfica.
Estas variables polimórficas pueden ir cambiando de un tipo a otro en tiempo de ejecución (eso sí,
no a cualquier tipo, solo dentro de la jerarquía de herencia).
Partiendo del ejemplo previo de herencia (Alumno -> AlumnoESO, AlumnoCiclos), una variable
polimórfica sería la definida de tipo Alumno, y en tiempo de ejecución asignarle instancias tanto
AlumnoESO como de AlumnoCiclos. Java nos permite entonces instrucciones como esta:
Alumno a1 = new Alumno ();
Alumno a2 = new AlumnoESO ("Juan Pérez","32233N", 981900900);
Alumno a3 = new AlumnoCiclo ("Ana López","77700K","ABanca", "[email protected]");
Una vez instanciada la variable o referencia de la superclase o clase “padre” con una clase derivada
o clase “hija”, solo puedo acceder a los atributos y métodos definidos en la superclase. Si trato de
acceder a atributos/métodos definidos en la subclase pero no en la superclase, se producirá un error
de compilación.
El único caso en el que no es necesario el casting para acceder a métodos de la subclase, es si estos
métodos son una sobrescritura de un método de la superclase.
Vamos a ver con un ejemplo distintos casos:
class Padre {
public String nombre;
@Override
void cambiaNombre () { this.nombre += " (es Hijo)";}
void cumple () {edad++;}
}
Si creamos una variable: Padre p1 = new Hijo ("Juan") tendríamos los siguientes casos:
• p1.minusc(); llamaría al método de la clase padre, pasando el nombre a minúsculas.
ArrayList Polimórfico
Podemos crear un ArrayList de alumnos, definiéndolo de la clase padre, con lo que nos va a permitir
añadir elementos de esta clase, pero, lo que es más interesante, también de las clases hijas.
Podremos recorrer el ArrayList y ejecutamos sus métodos “comunes” como setNombre(), también
los redefinidos como nuevaFalta(). Dinámicamente él ejecuta el método correspondiente a su clase,
sea una clase hija o padre.
Este proceso, llamado Vinculación dinámica, es el mecanismo que utiliza la máquina virtual de Java
para averiguar, en tiempo de ejecución, a qué método debe llamar, a partir del tipo del objeto
asignado a una referencia.
En el siguiente ejemplo, cuando llamamos al método nuevaFalta() Java decide en tiempo de ejecución
qué método ejecutar, en función si se trata de un alumno de tipo Alumno, AlumnoESO o
AlumnoCiclos.
Caso diferente es el de ejecutar métodos que solo existen en algunas de las instancias del ArrayList.
Por ejemplo, no podemos ejecutar el método setEmpresa() para alumnos que no sean de la clase
hija AlumnoCiclos.
Para saber la clase a la que pertenece una determinada instancia de clase, usaremos instanceof del
que ya hablamos en el apartado anterior.
Sobrecarga de Métodos
La sobrecarga de un método (overload) se llama también polimorfismo estático y se produce si los
parámetros del método son:
• Distinto número de parámetros.
• Mismo número de parámetros, pero de distinto tipo.
• Mismo número, del mismo tipo, pero en distinto orden.
En cualquiera de estos tres casos siempre devuelve el mismo tipo de dato, no cambia. Si no, no lo
llamaríamos sobrecarga.
Por ejemplo: partiendo del método: int metodo (int a, int b){} el método: float metodo (int a, int b){}
no representa una sobrecarga del primero, ya que sólo se diferencian en el valor de retorno no
valdría la sobrecarga.
El método int metodo (float a, int b) {} si representa sobrecarga.
La sobrecarga de métodos es un mecanismo muy útil que permite definir en una clase varios
métodos con el mismo nombre (de hecho, la hemos utilizado previamente en los constructores).
public void sumar (int a, int b) {
int suma= a + b;
System.out.println("la suma es: "+suma);
}
Y en la clase hija:
public class Rey extends Pieza {
//atributos
...
//constructores y métodos
...
public mover () {
...
}
}
En el siguiente cuadro podemos ver un resumen de todas las características que definen una clase
como abstracta.
- Tiene algún método abstracto (firma, pero sin cuerpo) que las subclases lo
- Desde Java8, puede tener métodos de tipo static que, como ya sabemos, se
“plantillas” de los métodos que luego las clases hijas se verán obligadas a
En el siguiente ejemplo, la clase abstracta Empleado tiene el método abstracto calcularSalario(), pero
son las subclases las que lo implementen.
public abstract class Trabajador {
public String nombre;
public int bonus;
public abstract int calcularSalarioAnual ();
Podemos preguntarnos ¿Qué beneficios aporta una clase abstracta? En el ejemplo anterior
podríamos pensar que si implementamos calcularSalarioAnual() directamente en las clases hijas, sin
definirlo en la clase padre.
La respuesta a esta pregunta es que tenemos dos beneficios fundamentales:
Por una parte, si el día de mañana creamos una nueva clase hija, el compilador nos obligará a
implementar el método abstracto, mientras que si no hubiésemos definido ese método como
abstracto en la superclase, alguna subclase podría no implementarlo.
Por otra parte, como ya vimos en el capítulo de polimorfismo, solo puedo invocar a los métodos
definidos en la clase de la variable (clase padre) y no de la clase con la que hemos instancia (clase
hija) así que para invocar los métodos que están en las clases hijas pero no en la superclase debo
hacer un casting.
Por lo tanto, sin el método abstracto anterior, no funcionaría:
System.out.printf
("Salario %s -> %d%n", t1.nombre, t1.calcularSalarioAnual());
Deberíamos hacer:
if (t1 instanceof Empleado)
System.out.printf
("Salario %s -> %d%n", ((Empleado) t1).nombre,
((Empleado) t1)t1.calcularSalarioAnual());
if (t1 instanceof Consultor)
System.out.printf
("Salario %s -> %d%n", ((Consultor) t1).nombre,
((Consultor) t1)t1.calcularSalarioAnual());
Interfaces
Una interfaz es similar a una clase abstracta en el sentido que define métodos abstractos que las
clases vinculadas a ella deberán implementar. Podríamos definir una interfaz como una clase
abstracta pura, en el sentido de que todos sus métodos son abstractos, no implementando ninguno
de ellos, además no contendrá ningún atributo como las clases, ni otros métodos no abstractos.
Una clase que implementa una interfaz se compromete a implementar todos esos métodos
abstractos definidos en la interfaz.
Ejemplo: una interfaz llamada Avion enumeraría los siguientes métodos: despegar(), aterrizar(), etc.
public interface Avion {
int despegar(int metros);
void aterrizar ();
}
Luego, clases como AvionComercial, Avioneta, etc. desarrollarán esos métodos de acuerdo a la firma
especificada en la interfaz.
@Override
public int despegar (int metros) {
this.altura+=metros;
return this.altura;
}
@Override
public void aterrizar () {
this.altura=0;
}
}
Podemos considerar las intefaces como un “compromiso” o como un contrato que obliga a las clases
que la imlplementan a desarrollar el código de todos los métodos de la interfaz, o bien ser una clase
abstracta, siendo las clases hijas (o nietas…) las que los desarrollen.
Así pues, si AvionComercial no pudiese escribir el código del método de aterrizar, entonces
AvionComercial debería ser abstracta a la espera de que un descendiente no abstracto lo escribiese.
Consideraciones sobre las interfaces:
• Polimorfismo en interfaces: Una interfaz se comporta como una clase abstracta: una variable o
referencia puede ser de tipo interfaz y ser instanciada con un método de una clase que
implemente dicha interfaz.
Por ejemplo, List es una interfaz y ArrayList es una clase que la implementa, por lo que será muy
frecuente definir así un ArrayList:
List <Integer> miLista = new ArrayList <>();
Esto hace que el código sea más flexible ya que podríamos cambiar a otro tipo de clase que
implemente la interfaz de forma muy cómoda:
List <Integer> miLista = new LinkedList <>();
En el ejemplo anterior:
Avion miAvion = new AvionComercial();
• Las interfaces, al igual que las clases, sólo pueden tener dos tipos de modificador de acceso:
public o default y, para las públicas, el nombre tiene que coincidir con el nombre del fichero en
el que se guarda.
• Los métodos de las interfaces serán siempre public y abstract y no es obligatorio indicarlo. Hay
una excepción: los métodos default, private o static que no son abstract de los que
hablaremos a continuación.
• Cuando una clase implementa los métodos de una interfaz, tiene que hacerlos públicos
explícitamente.
• Una interfaz puede heredar de otra interfaz usando la palabra reservada extends.
• Si una clase implementa más de una interfaz, se escriben todos los nombres de las interfaces
separadas por comas. Por ejemplo: public persona implements Cantante, Nacionalidad {…
Las últimas versiones de Java, han incorporado tres tipos de métodos nuevos, que sí incluyen la
implementación de los mismos en la propia interfaz, a diferencia de los métodos abstractos puros.
En cierto modo, estos métodos desvirtúan la función de una interfaz, pero tienen su utilidad como
veremos más adelante.
- Métodos por defecto: (modificador default) Se le añade un código general común para todas
las clases. Aquellas clases que no implementen el método podrán usar esta implementación.
- Métodos estáticos (modificador static). Son métodos comunes a todas las clases que
implementan la interfaz y por ello no tiene sentido que estén desarrollados en las clases.
Además de las firmas de los métodos, en las interfaces también podemos definir esos atributos
estáticos y finales y podemos omitir public static final, ya son así implícitamente. Eso sí, tienen
que estar inicializadas con un valor en la misma línea.
public interface Cantante { //public abstract
String formatoCancion = "mp3"; //public static final
void cantar(); //public abstract
default double tarifa () {return 0;}
}
class Persona implements Cantante { //public abstract
//añadiríamos atributos y métodos de persona y ...
@Override
public void cantar() {
System.out.println("La laa la raa laaa!");
}
@Override
public double tarifa() {return 1000d;}
}
class Canario implements Cantante { //no implementa método tarifa
//añadiríamos atributos y métodos de canario y...
public void cantar() {
System.out.println("Pio Pio Pio");
}
Pregunta: Supongamos que tenemos una interfaz ya creada y varias clases no abstractas que
implementan sus métodos. ¿Qué ocurre si añadimos un nuevo método a la interfaz?
Respuesta: Se produciría un error de compilación en las clases, ya que no implementan el nuevo
método y es algo obligatorio (si una clase no implementa todos los métodos de la interfaz, debe
marcarse como abstracta).
Una solución para evitar estos errores es definir el método en la interfaz como default, con una
implementación por defecto, así no se produciría el error y “daríamos tiempo” a los
desarrolladores para que implementasen el nuevo método en las clases.
}
Luego usaremos los métodos de Avion, ya que la variable referencia a ese tipo, aunque haya sido
instanciado con un AvionComercial.
miAvion.aterrizar();
Sin interfaces, Avioneta podría haber llamado a ese método de otra forma: aterrizo(), landing(), etc…
lo que haría que el cambio de AvionComercial a Avioneta implicaría cambios en su utilización.
1. Una clase abstracta puede heredar de una sola clase (abstracta o no) mientras que una interfaz
puede heredar de varias interfaces a la vez.
2. Una clase abstracta puede tener métodos que sean abstractos y otros que no lo sean, mientras
que las interfaces sólo podían definir métodos abstractos. Desde Java 8 interfaces tienen
métodos estáticos, privados y por defecto, que incumplen esa afirmación.
3. En una clase abstracta es obligatoria la palabra abstract para definir un método abstracto (así
como la clase). En interfaces, esta palabra es opcional ya que se infiere en el concepto de interfaz.
4. En una clase abstracta, los métodos abstractos pueden ser public o protected. En una interfaz
solamente puede haber métodos públicos.
5. En una clase abstracta pueden existir variables de instancia y variables static, y ambas con
cualquier modificador de acceso (public, private, protected o default). En una interfaz sólo
puedes tener constantes (public static final).
6. Una clase abstracta puede heredar de cualquier clase (independientemente de que esta sea
abstracta o no) o implementar interfaces, mientras que una interfaz solamente puede heredar
de otras interfaces.
Una pregunta muy frecuente es ¿Qué debo usar interfaces o clases abstractas?
No hay una respuesta válida para todos los casos. Se usan clases abstractas cuando no necesitamos
herencia múltiple y cuando hay mucha compartición de código entre superclase y subclases. Las
interfaces van más orientadas a ese “compromiso” o normas de cómo se comportarán las clases
subyacentes, no centrándose tanto en compartir código.
11. Paquetes
Los paquetes son el mecanismo que usa Java para facilitar la modularidad del código. Un paquete
puede contener una o más definiciones de interfaces y clases, distribuyéndose habitualmente como
un archivo. Para utilizar los elementos de un paquete es necesario importar este en el módulo de
código en curso, usando para ello la sentencia import.
La funcionalidad de una aplicación Java se implementa habitualmente en múltiples clases, entre las
que suelen existir distintas relaciones. Las clases se agrupan en unidades de un nivel superior, los
paquetes, que actúan como ámbitos de contención de tipos. Cada módulo de código establece,
mediante la palabra clave package al inicio, a qué paquete pertenece, después, con la cláusula import
cualquier módulo de código puede hacer referencia a tipos definidos en otros paquetes.
Creación de Paquetes
Un paquete Java se genera incluyendo la palabra clave package al inicio de los módulos de código en
los que se definen las clases que formarán parte del mismo. Trabajando en un proyecto con
NetBeans, comprobaremos que en la ventana Projects los paquetes se representan con un icono
específico y actúan como nodos contenedores, alojando los módulos .java con el código fuente. El
menú contextual del proyecto nos ofrece la opción New>Java Package, que será el que usemos
habitualmente para crear un nuevo paquete.
Cada vez que se crea un nuevo proyecto con NetBeans se propone la definición de un nuevo
paquete, cuyo nombre sería el mismo del proyecto, donde se alojarían los módulos de código.
En proyectos complejos, no obstante, puede ser necesaria la creación de paquetes adicionales.
Un paquete puede contener, además de definiciones de tipos como las clases e interfaces, otros
paquetes, dando lugar a estructuras jerárquicas de contenedores. La denominación de
los “subpaquetes”, paquetes contenidos en otros, se compondrán del identificador del contenedor
seguido de un punto y el nombre del subpaquete. De existir niveles adicionales se agregarían los
distintos identificadores separados por puntos, formando así el nombre completo del paquete.
Así pues, cuando hacemos import java.time.format.*; nos referimos al paquete format que
es un subpaquete del paquete time.
Notas:
- El concepto de “subpaquete” se aplica solo a nivel organizativo de clases, pero para
Java no hay ninguna relación especial entre paquete y subpaquete, es como si fueran
dos paquetes totalmente independientes (a efectos de permisos, acceso…)
- Comentar finalmente que, si no se indica ningún paquete, las clases se crean en un
paquete por defecto, aunque esta situación no es recomendable.
Import
Como ya hemos visto, con import incorporamos al archivo actual las definiciones de otro paquete
para poder usarlas según el procedimiento habitual sin necesidad de especificar luego en el código,
el paquete origen del elemento.
La cláusula import puede utilizarse para importar un elemento concreto de un paquete, facilitando
el nombre de este seguido de un punto y el identificador de dicho elemento. Por ejemplo, para
importar la clase Math del paquete java.lang, pudiendo así acceder a la constante PI y las funciones
matemáticas que aporta, bastaría con la siguiente línea import java.lang.Math;
Es habitual que al importar un paquete nos interesen muchas de las clases definidas en el mismo.
En este caso podríamos importarlas individualmente, usando la sintaxis anterior, o bien podríamos
recurrir a la siguiente alternativa. Esto nos permitiría usar la clase Math, así como la clase System, la
clase Thread y muchas otras definidas en el paquete java.lang: import java.lang.*;
En concreto ‘java.lang’ se importa por defecto, por lo que no es necesario importarla.
En ocasiones, como ocurre con la clase Math, importamos una clase para acceder directamente a
sus miembros estáticos (constantes y métodos), no para crear instancias a partir de las clases del
paquete, podemos recurrir a la sintaxis import static paquete.clase.*; cuya finalidad es
incluir en el ámbito actual los miembros estáticos de la clase indicada y no sería necesario emplear
el prefijo de la clase en nuestro código, como se muestra en la figura.
Cada archivo de código de un proyecto Java únicamente puede contener una clase pública, cuyo
nombre, respetando mayúsculas y minúsculas, debe coincidir con el del archivo. Es decir, existe una
correspondencia directa entre los identificadores de clase y archivos en los que se alojan.
El nombre del paquete establece además el nombre de la carpeta donde se alojan los módulos de
código contenidos en el paquete. Los nombres de paquete pueden ser compuestos, usándose el
punto como separador de las diferentes porciones del identificador. Cada parte entre puntos
identificaría a una subcarpeta en la jerarquía.
Así pues, podemos decir que cualquier elemento, salvo que sea private¸ es accesible desde cualquier
miembro de su mismo paquete.
Por otra parte, si hablamos de distintos paquetes, solo los elementos public serán accesibles (y
también los protected si hablamos subclases).
Hay que tener en cuenta que la jerarquía de paquetes no afecta para la visibilidad de sus clases, a
todos los efectos, un paquete y un subpaquete del mismo son dos paquetes totalmente
independientes.
Estamos hablando de elementos, y en ese término estamos agrupando clases, interfaces, atributos
y métodos. Hay que recordar que una clase o una interface solo puede ser public o default.
Las clases que públicas y default de otros paquetes de nuestro proyecto podemos usarlas
directamente sin el prefijo del paquete siempre que hagamos su import. Si no hacemos import,
podemos usarlas igualmente, pero hay que poner el nombre con el prefijo del paquete al que
pertenece.
¿Qué es un evento?
Es todo hecho que ocurre mientras se ejecuta la aplicación. Normalmente, llamamos evento a
cualquier interacción que realiza el usuario con la aplicación, como puede ser:
• Pulsar un botón con el ratón,
• Hacer doble clic,
• Pulsar y arrastrar,
• Pulsar una combinación de teclas en el teclado,
• Pasar el ratón por encima de un componente,
• Salir el puntero de ratón de un componente,
• Abrir una ventana, etc.
El proceso sería algo así: cada vez que el usuario realiza una determinada acción sobre una
aplicación que estamos programando en Java: un clic sobre el ratón, presionar una tecla, etc, se
produce un evento que el sistema operativo transmite a Java, creando un objeto de una determinada
clase de evento, y este evento se transmite a un determinado método para que lo gestione. Ejemplos
de fuentes de eventos pueden ser:
• Botón sobre el que se pulsa o pincha con el ratón.
• Campo de texto que pierde el foco.
• Campo de texto sobre el que se presiona una tecla.
• Ventana que se cierra.
• Etc.
Un ejemplo típico será la selección de un elemento, bien haciendo click con el ratón sobre él o
pulsando [Enter] cuando tiene el foco. Suponiendo un botón llamado jButton1 sería así:
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
TareaAlPulsarBoton(evt);
}
});
private void TareaAlPulsarBoton (java.awt.event.ActionEvent evt) {
// Aqui nuestro código que se ejecuta al pulsar el botón
}
A nosotros lo que nos interesa es lo que está resaltado con fondo amarillo, el resto será
generado por el IDE. El parámetro del método ‘evt’ nos proporciona mucha información
útil sobre el evento, como se verá más adelante.
Otro evento típico será pulsar una tecla:
addKeyListener(new java.awt.event.KeyAdapter() {
public void keyPressed(java.awt.event.KeyEvent evt) {
formKeyPressed(evt);
}
});
}
}
Ojo!! el addKeyListener no lo aplicamos a un elemento, iría a toda la ventana (o
al contenedor)
También se podría hacer que nuestro proyecto sí tuviese una clase principal independiente,
como en los proyectos sin interface gráfica. Esta clase tendría una única misión, que sería
visualizar el JFrame. En tal caso, el main () de esa clase principal tendría un código similar al
método main () del JFrame generado por Netbeans.
Sobre el JFrame creado, ir a la pestaña Diseño. Veremos la nueva ventana, vacía por el
momento. Este sería el código generado:
Netbeans genera nuevo código que añade a la ventana, para los nuevos elementos y sus
propiedades:
6. El último paso será la generación de las acciones que queramos que realice ante los distintos
eventos, eso lo haremos con botón derecho sobre cada elemento > Events > Action > …
En nuestro caso, lo haremos sobre el botón [Convertir], concretamente Events > Action >
Action performed (que se corresponde con pulsar el botón) e introduciremos el siguiente
código:
Es fácil de entender lo que hace este código: obtiene lo que haya en la primera caja de texto
(jTextField1), que será de tipo String y lo convierte a número. Luego hace los cálculos necesarios e
introduce el resultado en la segunda caja de texto (jTextField2). Veremos un poco más adelante los
métodos interesantes de cada objeto gráfico.
Trataremos de separar, en la medida de los posible, la parte gráfica de la lógica de nuestro programa,
de forma que las acciones desencadenadas desde los eventos sean llamadas a métodos y funciones
situadas en otras clases. Así lograremos mayor portabilidad, por ejemplo, podremos hacer una
aplicación web o para otros dispositivos, y la lógica del programa podremos reutilizarla
completamente, solo cambiando el interfaz gráfico.
Si hubiésemos seguido esta filosofía en el ejemplo anterior, podríamos haber creado una clase
llamada Temperatura con un método estático que pasase de Celsius a Fahrenheit y del evento
quedaría algo así:
double celsius= Double.parseDouble(jTextField1.getText());
double fahrenheit = Temperatura.convertirCelsiusFar(celsius);
jTextField2.setText(Double.toString(fahrenheit));
7. Con esto ya podemos ejecutar nuestra aplicación. Al principio se ejecutará el código que está
en el constructor de la ventana (JFrame) y por por defecto es solo el método initComponents()
auque podríamos añadir más operaciones. Luego, al pulsar el botón se ejecutará el código
añadido en el paso anterior.
Por último, comentar que Netbeans genera archivos XML con extensión .form para almacenar
información sobre la parte gráfica de cada JFrame. No es necesario distribuirlos con la aplicación ya
que solo los emplea el IDE.
2.- Dentro de la clase declararíamos las variables de los elementos presentes en la ventana:
private JLabel etiq;
private JTextField campoTexto;
private JButton btnPulsame;
private JPanel PanelContenido;
//Componentes
etiq = new JLabel(“Texto de la etiqueta);
etiq.setBounds(369, 32, 229, 25);
PanelContenido.add(etiq);
Componentes Swing
Swing ofrece dos tipos de elementos clave: componente y contenedor. Sin
embargo, esta distinción es principalmente conceptual debido a que todos los contenedores
también son jerárquicamente componentes. La diferencia entre los dos se encuentra en su
propósito: como el término se emplea comúnmente, un componente es un control visual
independiente, como un botón o un deslizador. Un contenedor aúna a un grupo de componentes.
Por ello, un contenedor es un tipo especial de componente que está diseñado para poseer a otros
componentes.
Además, para que un componente sea desplegado debe ser colocado en un contenedor. Por ello,
todas las interfaces gráficas hechas con Swing contienen al menos un contenedor por defecto.
Debido a que los contenedores son componentes, un contenedor puede también poseer a otros
contendores. Esto le permite a Swing definir lo que se denomina una contención jerárquica, en cuyo
nivel más alto se encuentra lo que se denomina un contenedor raíz.
Componentes
En general los componentes de Swing se derivan de la clase JComponent. Las únicas excepciones a
esto son los cuatro contenedores raíz que se describen en la siguiente sección. Todos los
componentes de Swing están representados por clases definidas en el paquete javax.swing. Observe
que todas las clases comienzan con la letra J. Por ejemplo, la clase para crear una etiqueta es JLabel;
la clase para un botón es JButton; y la clase para un scrollbar es JScrollBar.
Contenedores
Swing define dos tipos de contenedores. El primero son los contenedores raíz: JFrame, JApplet,
JWindow y JDialog. A diferencia de los otros componentes de Swing, los contenedores raíz son
componentes pesados y son los que interactúan con el sistema operativo. Como el nombre lo indica
un contenedor raíz debe estar en lo alto de una jerarquía de contención y non está contenido ningún
otro componente. Además, todas las jerarquías de contención deben comenzar con un contenedor
raíz. El contenedor utilizado más comúnmente en aplicaciones es JFrame. El contenedor utilizado
para applets es JApplet.
El segundo tipo de contenedores soportados por Swing son los contenedores ligeros. Los
contenedores ligeros heredan de la clase JComponent. Por ejemplo, JPanel, el cual es un contenedor
de propósito general. Los contenedores ligeros se utilizan a menudo para organizar y administrar
grupos de componentes relacionados debido a que un contenedor ligero puede ser contenido por
otros contenedores. Así, el programador puede utilizar contenedores ligeros como JPanel para crear
subgrupos de controles relacionados que están contenidos en un contenedor exterior.
Ventana de Aplicación
JFrame: Es el contenedor de alto nivel más empleado, tiene las funcionalidades típicas de una
ventana (maximizar, cerrar, título, etc.). Algunos métodos importantes son:
• setSize (w,h); asigna las dimensiones de la ventana (ancho y alto).
• setTitle (str), getTitle ();
• setVisible (bool);
• setDefaultCloseOperation (constante); controla la acción al pulsar la “X” de cierre de ventana.
Su valor habitual suele ser JFrame.EXIT_ON_CLOSE.
• setLocationRelativeTo(null); centra la ventana en la pantalla.
Una vez creado el objeto de ventana, hay que establecer su tamaño, establecer la acción de cierre y
hacerla visible.
import javax.swing.*;
public class VentanaTest {
public static void main(String[] args) {
JFrame f = new JFrame("Titulo de ventana");
f.setSize(400, 300);
f..setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
}
Acciones de cierre:
Componentes básicos
Vamos a introducir aquí los componentes habituales de una ventana de aplicación, con su
descripción, apariencia y métodos interesantes. Estos componentes se añadirán a la ventana o,
mejor dicho, a un panel de la ventana. Si no hay ningún panel, se añaden al panel por defecto.
Esta es la jerarquía de componentes de Swing.
Si deseamos detectar el evento de clicar con el botón derecho, este sería el evento:
jButton1.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
TareaAlPulsarRaton(evt);}});
JCheckBox: Casilla de verificación. Se usa normalmente para indicar opciones que son
independientes, pudiéndose seleccionar algunas, todas o ninguna
El método que más nos interesa de esta clase es: isSelected() que devuelve true si el check-box está
seleccionado y false en caso contrario.
JFileChooser : Permite al usuario elegir un archivo para la lectura o escritura, mostrando una ventana
que nos permite navegar por los distintos discos y carpetas de nuestro ordenador
JLabel:Una etiqueta que puede contener un texto corto, o una imagen, o ambas cosas. En general
suelen ser estáticas, de forma que desde el asistente de Netbeans le asignaremos un valor y ya no
lo modificaremos más, pero siempre podremos emplear los métodos setText() y getText() para
modificar y obtener el texto mostrado, respectivamente.
jLabel1.setText("Temperatura Celsius");
Se suele utilizar esta etiqueta para incorporar imágenes (veremos más adelante cómo hacerlo).
JList:Un cuadro de lista para seleccionar uno o varios elementos de una lista
Los valores contenidos en la lista se gestionan desde una colección (similar a un array) llamada model
y que dispone de métodos para añadir / modificar / eliminar elementos y otras operaciones.
y luego podemos emplear los métodos siguientes para trabajar con la lista:
lista1modelo.addElement(str); //añadir a la lista, generalmente String
lista1modelo.clear();
lista1modelo.removeElementAt(position);
lista1modelo.setElementAt(string, position);
lista1modelo.getElementAt(posistion);
lista1modelo.size();
Para obtener el índice o valor de la lista seleccionado por el usuario, haremos respectivamente:
int index = jList1.getSelectedIndex();
String s = (String) jList1.getSelectedValue();
JPopUpMenu : Un menú emergente. La imagen del ejemplo está sacada de una aplicación de dibujo
JProgressBar : Una barra de progreso, que visualiza gráficamente un valor entero en un intervalo
JRadioButton : Crea un botón de radio. Se usa normalmente para seleccionar una opción de entre
varias excluyentes entre sí
Los botones de radio deben estar agrupados para que, cuando se seleccione uno, el resto del grupo
estén seleccionados (es lo que lo diferencia de un check buttonI. Para ello, necesitamos añadir un
nuevo componente de tipo buttonGroup (a nivel gráfico es invisible). Luego a cada radioButton, le
asignaremos el buttonGroup creado en la propiedad buttonGroup:
Como en el caso de los check-buttons, el método isSelected() nos informará si un botón está
seleccionado o no.
if (!jRadioButton1.isSelected() && !jRadioButton2.isSelected()) {
JOptionPane.showMessageDialog(null,"Seleccione una opción"); return;
}
Al igual que con JButton, el evento que ocurre cuando el usuario selecciona un botón de radio es:
ActionPerformed.
JSlider : Barra deslizante. Componente que permite seleccionar un valor en un intervalo deslizando
un botón
Para obtener el valor seleccionado tenemos el método getValue() y el evento para detectar
cambios en un Slider sería:
jSlider1.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
// Aquí el código
}
});
JSpinner : Un cuadro de entrada de una línea que permite seleccionar un número (o el valor de un
objeto) de una secuencia ordenada. Normalmente proporciona un par de botones de flecha, para
pasar al siguiente o anterior número, (o al siguiente o anterior valor de la secuencia).
En NetBeans, mediante la propiedad model podemos controlar sus valores máximos y mínimos.
Los dos métodos que más nos interesan son el que obtienen el texto que hay escrito en el
componente: getText() y el que escribe texto en el componente: setText().
El valor devuelto por getText(); es de tipo String por lo que, si queremos hacer operaciones
numéricas el mismo, debemos convertirlo con los métodos estáticos que ya conocemos, como
Integer.parseInt().
Métodos comunes
Existen una serie de métodos comunes a los elementos que acabamos de ver, de uso frecuente:
• setBounds (x,y,w,h); Este es común a todos los componentes e indica la posición y el tamaño
del “rectángulo” que forma la apariencia del componente. Los primeros dos parámetros son las
posiciones X e Y de la esquina superior izquierda (es la posición respecto a su contenedor). El
tercer parámetro es el ancho del elemento y el cuarto el alto del elemento.
• setLocation (x,y); y setSize (w,h); representan lo mismo que el anterior, por separado.
• setResizeable (bool); Indica si se permite cambiar el tamaño del componente o no.
• setVisible (bool);
• setEnable (bool); si acepta eventos o no (aparecerá en gris)
• setText(String n): para establecer el texto que aparece en el elemento (etiqueta, botón, caja
de texto, etc.)
• setName(String n); para asingnarle un nombre al elemento.
• getName(); para obtener el nombre del objeto.
Ventanas de Diálogo
Las ventanas de diálogo son ventanas sencillas, que se muestran en forma de “pop-up”, que
muestran información y pueden pedir información al usuario. Otra característica fundamental es
que son modales, es decir, paran la ejecución de nuestro programa hasta que el usuario cierre el
diálogo seleccionando alguna de las opciones.
Las ventanas de diálogo se gestionan en Swing con la clase JOptionPane que contiene los métodos
básicos para cada tipo de diálogo. Todos los métodos que vamos a ver tienen dos parámetros
comunes a todos ellos:
• parentComponent: Apuntador al padre (ventana sobre la que aparecerá el diálogo) o bien
null. En muchos casos podrá ser this, la ventana donde está definido.
• message: El mensaje a mostrar, habitualmente un String, aunque vale cualquier Object cuyo
método toString() esté definido.
JOptionPane.showMessageDialog()
Este es el diálogo más sencillo de todos, sólo muestra una ventana de aviso al usuario. La ejecución
se detiene hasta que el usuario cierra la ventana. El método está sobrecargado, con más o menos
parámetros, en función de si aceptamos las opciones por defecto o deseamos asignarle un título y
un icono. Ejemplo:
JOptionPane.showMessageDialog(this, "Un aviso informativo");
Mostrando:
No devolviendo ningún valor y continuando la ejecución del programa una vez se pulse Aceptar.
JOptionPane.showConfirmDialog()
Este método muestra una ventana pidiendo una confirmación al usuario, estilo "¿Estás seguro?" y da
al usuario opción de aceptar o cancelar esa operación. El método devuelve un entero indicando la
respuesta del usuario. Los valores de ese entero pueden ser alguna de las constantes definidas
en JOptionPane: YES_OPTION, NO_OPTION, CANCEL_OPTION, OK_OPTION, CLOSED_OPTION.
El siguiente ejemplo de código
int respuesta = JOptionPane.showConfirmDialog(this, "¿Estás seguro?");
if (respuesta == JOptionPane.OK_OPTION)
System.out.println("El usuario confirma la operación");
else System.out.println("El usuario cancela la operación");
Podríamos configurar las opciones de respuesta con nuestros propios valores, utilizando el método
showOptionDialog que veremos más adelante.
JOptionPane.showInputDialog()
A diferencia del anterior, este diálogo permite una respuesta del usuario. También está
sobrecargado, admitiendo más o menos parámetros, según queramos aceptar o no las opciones por
defecto. Los parámetros y sus significados son muy similares a los del método showOptionDialog(),
pero hay una diferencia.
Si usamos los métodos que no tienen array de opciones, la ventana mostrará una caja de texto para
que el usuario escriba la opción que desee (un texto libre). Si usamos un método que tenga un array
de opciones, entonces aparecerá en la ventana un JComboBox en vez de una caja de texto, donde
estarán las opciones que hemos pasado. Vemos las dos posibilidades en estos dos ejemplos:
String txt = JOptionPane.showInputDialog(this, "Introduce tu nombre");
System.out.println("El usuario ha escrito "+ txt);
JOptionPane.showOptionDialog()
Este método muestra la ventana más configurable de todas, en ella debemos definir todos los
botones que lleva. De hecho, las demás ventanas disponibles con JOptionPane se construyen a partir
de esta. Por ello, al método debemos pasarle muchos parámetros:
• parentComponent: Apuntador al padre (ventana sobre la que aparecerá el diálogo) o bien null
(ventana actual).
• message: El mensaje a mostrar, habitualmente un String, aunque vale cualquier Object cuyo
método toString() esté definido.
• title: El título para la ventana.
• optionType: Un entero indicando qué botones tendrá el diálogo. Los posibles valores son las
constantes definidas en JOptionPane: DEFAULT_OPTION, YES_NO_OPTION,
YES_NO_CANCEL_OPTION, o OK_CANCEL_OPTION.
• messageType: Un entero para indicar qué tipo de mensaje estamos mostrando. Este tipo
servirá para que se determine qué icono mostrar. Los posibles valores son constantes
definidas en JOptionPane: ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE,
QUESTION_MESSAGE, o PLAIN_MESSAGE
• icon: Un icono para mostrar. Si ponemos null, saldrá el icono adecuado según el
parámetro messageType.
• options: Un array de objects que determinan las posibles opciones. Si los objetos son
componentes visuales, aparecerán tal cual como opciones. Si son String,
el JOptionPane pondrá tantos botones como String. Si son cualquier otra cosa, se les tratará
como String llamando al método toString(). Si se pasa null, saldrán los botones por defecto
que se hayan indicado en optionType.
• initialValue: Selección por defecto. Debe ser uno de los Object que hayamos pasado en el
parámetro options. Se puede pasar null.
La llamada a JOptionPane.showOptionDialog() devuelve un entero que representa la opción que ha
seleccionado el usuario. La primera de las opciones del array es la posición cero. Si se cierra la
ventana con la cruz de la esquina superior derecha, el método devolverá -1.
Aquí un ejemplo de cómo llamar a este método:
int seleccion = JOptionPane.showOptionDialog(
null, "Seleccione opcion", "Selector de opciones", //ventana, mensaje, título
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE,
null, // icono: null para icono por defecto
new Object[]{"opcion 1","opcion 2","opcion 3" }, // opciones
//null para YES, NO y CANCEL
"opcion 2" //opción con el foco
);
y la ventana que se obtiene:
JDialog
Por último, comentar que disponemos de la clase JDialog, que es una clase base para hacer ventanas
de diálogo totalmente personalizadas. Son un elemento más que se puede añadir a nuestro JFrame
y podemos añadirle cualquier componente como cajas de texto, botones de opción, etc. Exactamente
igual que hacemos con una ventana o con un panel.
A nivel de código haremos lo siguiente:
• En el constructor del JDialog añadiremos:
setLocationRelativeTo(null);
this.setVisible(true);
• Al cerrar el diálogo tendremos disponibles los valores de los atributos para tratarlos en
nuestra aplicación: myD.atributo1, myD.atributo2, etc.
Menús
JMenuBar : Una barra de menú, que aparece generalmente situada debajo de la barra de título de la
ventana
JMenu : Un menú de lista desplegable, que incluye JMenuItems y que se visualiza dentro de la barra
de menú (JMenuBar)
Gestión de Eventos
Ya hemos visto en los apartados anteriores eventos típicos como seleccionar un botón o detectar los
cambios en un Slider. A continuación, vamos a profundizar un poco más en la gestión de eventos.
Cuando tenemos Arrays de elementos, esta es la forma habitual de trabajar (por ejemplo, en un
tablero, los dígitos de una calculadora, etc). No tendría sentido duplicar de varios elementos que
realizan la misma función (en la calculadora, la tarea a realizar si pulsas el 1 o el 2 o 3… es la misma,
solo necesitamos identificar el botón que se ha pulsado.
Analiza este ejemplo, que dibuja un tablero de damas y le asigna a cada casilla (que en realidad es
un JButton) un nombre compuesto por la fila y columna en la que está ubicado.
import javax.swing.*;import java.awt.Color;
public damas() {
boolean blanco=true;
tablero = new JButton [8][8];
for (int f=0;f<8;f++){
for (int c = 0;c<8; c++) {
}
if (blanco) blanco=false; else blanco=true;
} }
private void tareaSipulsa (java.awt.event.ActionEvent evt) {
evt.getSource();
//(JButton) evt.getSource() sería el botón pulsado
}
...}
En este ejemplo, hemos situado los botones “a mano” en la pantalla, mediante el método
setBounds(). Más adelante, cuando veamos los Layouts, veremos que hay formas más eficientes y
rápidas de colocar los objetos con la disposición que queramos. El caso de los tableros, la disposición
GridLayout o GridBagLayout será la más aconsejable.
Lista de Eventos
NOMBRE
DESCRIPCIÓN MÉTODOS EVENTOS
LISTENER
Se produce al hacer click JButton: click o pulsar Enter con el foco activado
en un componente, public void en él. JList: doble click en un elemento de la lista.
ActionListener también si se pulsa Enter actionPerformed JMenuItem: selecciona una opción del menú.
teniendo el foco en el (ActionEvent e) JTextField: al pulsar Enter con el foco activado.
componente.
public void
Según el Listener:
mouseClicked
mouseCliked: pinchar y soltar.
(MouseEvent e)
public void
mouseEntered: entrar en un componente con el
mouseEntered
puntero.
(MouseEvent e)
public void
mouseExited: salir de un componente con el
mouseExited(MouseE
puntero
vent e)
En Netbeans podemos crear un panel de varias formas: arrastrándolo desde la paleta como
cualquier otro componente o bien creando en el proyecto un nuevo elemento, con botón derecho
sobre el paquete deseado: New > JPanel Form, tal y como haríamos con un JFrame.
Una vez creados, trabajamos con ellos como un JFrame. Podemos mostrarlos u ocultarlos con el
método setVisible (boolean).
Para probarlo, crea un JFrame con:
import javax.swing.JPanel;
Podríamos añadir al JFrame dos botones que hiciesen que estuviese visible uno u otro panel:
La forma de trabajar con los JPanel es como con los JFrame, es decir,
añadiendo los componentes al mismo mediante el método add().
Layout
Los paneles nos ofrecen distintas formas de organizar, de manera automática, la posición y tamaño
de los componentes dentro de los contendedores, mediante lo que se conoce como Layout
Managers o gestores de aspecto.
FlowLayout: Es el más simple y el que se utiliza por defecto en todos los paneles si no se indica el
uso de alguno de los otros. Los componentes añadidos a un panel con FlowLayout se encadenan en
forma de lista. La cadena es horizontal, de izquierda a derecha, y se puede seleccionar el espaciado
entre cada componente.
Si el contenedor se cambia de tamaño en tiempo de ejecución, las posiciones de los componentes
se ajustarán automáticamente, para colocar el máximo número posible de componentes en la
primera línea.
Los componentes se alinean según se indique en el constructor. Si no se indica nada, se considera
que los componentes que pueden estar en una misma línea estarán centrados, pero también se
puede indicar que se alineen a izquierda o derecha en el contenedor.
BorderLayout: Esta composición (de borde) proporciona un esquema más complejo de colocación
de los componentes en un panel. Es el layout o composición que utilizan por defecto JFrame y JDialog.
La composición utiliza cinco zonas para colocar los componentes sobre ellas:
• Norte: parte superior del panel (NORTH o PAGE_START)
• Sur: parte inferior del panel (SOUTH o PAGE_END)
• Este: parte derecha del panel (EAST o LINE_END)
• Oeste: parte izquierda del panel (WEST o LINE_START)
• Centro: parte central del panel, una vez que se hayan rellenado las cuatro partes (CENTER)
En ella tendremos un JPanel para la parte superior y otro JPanel para la botonera. Este segundo panel
será tipo GridLayout de 4 filas x 4 columnas. Al añadir al panel los botones se irán colocando uno a
continuación de otro ocupando las 16 posiciones.
1.- Sobre el paquete de nuestro proyecto, botón derecho > New Frame. A continuación, desde la
ventana navegador, botón derecho sobre ella > Set Layout > Border Layout.
2. – Arrastramos un JPanel desde la paleta a la parte superior del JFrame, y lo soltamos cuando la
línea de puntos amarilla nos muestre la zona superior del frame. Esto indica que estamos situando
el panel en el área North. Desde la ventana Navigator, botón derecho sobre este panel: lo llamamos
Cabecera cambiándole el nombre de la variable y decimos que tiene BorderLayout. Luego veremos
que contendrá la caja de texto en la que se muestran operandos y resultados.
3.- Arrastramos otro JPanel desde la paleta a la parte central del frame, y le llamaremos Botonera.
Desde la ventana Navigator, botón derecho sobre este panel: Set Layout > GridLayout. Luego le
asignaremos todos los botones.
long num1 = 0;
long num2 = 0;
char oper = '=';
JButton[] tablero; //definimos el array con los 16 botones
public Calculadora() {
initComponents();
6.- Para finalizar, incorporamos el código que se ejecuta al pulsar un botón, esto es, el método
FActionPerformed (evt). Será un código común para los 10 botones que representan dígitos.
private void FActionPerformed(java.awt.event.ActionEvent evt) {
String nomBoton = ((JButton) evt.getSource()).getName();
int numBoton = Integer.parseInt(nomBoton.substring(7, nomBoton.length()));
if (numBoton <= 9) { //si es un botón numérico
if (jPantalla.getText().equals("0"))
jPantalla.setText(((Integer) numBoton).toString());
else
jPantalla.setText(jPantalla.getText() +
((Integer) numBoton).toString());
} else {
switch (numBoton) {
case 10://boton +
num1 = Long.parseLong(jPantalla.getText());
oper = '+';
jPantalla.setText("0");
tablero[15].setEnabled(true);
break;
Gráficos y Animaciones
La animación gráfica queda fuera del ámbito de este curso ya que es un tema muy especializado y
existen librerías específicas para optimizar tanto el desarrollo como la ejecución. Sin embargo, vamos
a ver utilidades sencillas que pueden resultarnos útiles en nuestras aplicaciones, para hacerlas más
vistosas o programar juegos sencillos.
Imágenes
La forma más sencilla de incluir imágenes en nuestras aplicaciones es añadiendo una etiqueta JLabel,
eliminando su texto y añadiéndole la imagen mediante el método setIcon (en Netbeans: propiedad
“Icon”). Podemos incluir imágenes de diferentes tipos incluidos gif animados.
Para que cuando generemos el .jar con el ejecutable no haya problemas con la ubicación de la
imagen, esta tiene que estar en la estructura de carpetas del proyecto. Para ello, la forma de
trabajar con imágenes será distinta según el tipo de proyecto que hayamos creado.
Para proyectos Ant:
- Primero crearemos una carpeta en el proyecto/src: Botón derecho sobre el proyecto/src >
New > Others > Folder, y le llamamos como queramos, por ejemplo: recursos.
- Cuando asignamos la imagen a la propiedad Icon de la etiqueta, pulsamos en Import image
to Project, incorporándola a la carpeta del paso anterior: recursos.
El código generado será:
jLabel1.setIcon(new
javax.swing.ImageIcon(getClass().getResource("/recursos/img.gif")));
En ambos casos, para ejecutar desde Netbeans, es necesario compilar y ejecutar todo el proyecto
(Project > Clear & Build y Project > Run, y no [May]+[F6] sobre el JFrame.
Swing.Timer
En nuestras aplicaciones con interfaz visual es muy frecuente que tengamos que ejecutar algo
periódicamente, por ejemplo, un contador, un reloj, o el movimiento de un objeto por la ventana,
etc. Para ello tenemos varias opciones: uso de hilos (Threads), la clase general Timer (del paquete
util) y la clase Timer (del paquete Swing). La primera es la más completa pero también la más
compleja.
La segunda y la tercera opción son más sencillas, y la ventaja principal de la última es que el código
se programa en la propia clase del contenedor, por lo que tenemos disponibles los objetos que lo
componen sin necesidad de tener que pasárselos por parámetro, que sería lo que habría que hacer
en caso de usar util.Timer.
Así pues, en nuestra aplicación gráfica, cuando necesitemos una tarea repetitiva, por ejemplo, definiríamos
una instancia de Timer como atributo global: Timer myTimer;, importando previamente: import
javax.swing.Timer;
Moviendo elementos
Un aspecto fundamental en las aplicaciones gráficas es el movimiento de los elementos que
aparecen en pantalla. Para ello, modificaremos su posición mediante el método setLocation(x,y)
representando ‘x’ e ‘y’ las coordenadas de la esquina superior izquierda del elemento.
Estas coordenadas no son absolutas respecto a la aplicación, sino que son relativas al contenedor en
el que esté ubicado el elemento, ya sea el JFrame o un JPanel concreto.
Si lo que necesitamos es un desplazamiento, tendremos que saber las coordenadas actuales para
incrementarlas o decrementarlas. Esa información la obtenemos con los métodos getX() y getY()
y finalmente necesitaremos saber los límites por los que nos podemos desplazar. Lo haremos con
los métodos getHeight() y getWidth() pero del contenedor del elemento, no del propio
elemento (en muchos casos será this ya que programamos eventos en el contenedor).
El siguiente ejemplo movería la etiqueta en diagonal hacia abajo.
jLabel1.setLocation(jLabel1.getX()+1, jLabel1.getY()+1);
Este movimiento puede ser automático, con Swing.Timer o provocado por acciones del usuario
mediante los eventos vistos previamente.
Dibujando figuras
Para dibujar figuras el proceso que seguiremos será el siguiente:
1.- Definir un panel (una nueva clase que extiende JPanel).
2.- Sobrescribir el método paintComponent () del panel
3.- Añadir el panel al JFrame
4.- Cada vez que haya un cambio, llamar a repaint() del panel, el cual llama internamente a
paintComponent ()
En la clase Graphics disponemos de métodos para dibujar figuras básicas. Por ejemplo:
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g); // siempre es la primera instrucción
g.drawString("Ejemplo de texto",30,20); // texto y posición
g.setColor(Color.RED); // color de primer plano
g.fillRect(40,50,60,70); // rectángulo sólido
g.setColor(Color.BLUE); // cambia el color de primer plano
g.drawRect(80,90,100,120); // dibuja un rectángulo sin relleno
g.drawLine(0, 0, 70, 70); // línea de esquina sup. izq. a inf. dcha
}
Ver esta serie de videos sobre cómo hacer el juego “Pong” con Swing en la que se ven los conceptos
vistos previamente: https://www.youtube.com/watch?v=fnJQLBPemcI
El modelo MVC
Modelo-vista-controlador (MVC) es un patrón desarrollo que separa los datos y la lógica de negocio
de la aplicación de su representación y el módulo encargado de gestionar los eventos. Con ello
optimiza la reutilización de código y la separación de conceptos, características que buscan facilitar
la tarea de desarrollo de aplicaciones y su posterior mantenimiento.
De manera genérica, los componentes de MVC se podrían definir como sigue:
Modelo: Es la representación de la información con la cual el sistema opera, por lo tanto, gestiona
todos los accesos a dicha información, tantas consultas como actualizaciones (lógica de negocio).
Envía a la ‘vista’ aquella parte de la información que en cada momento se le solicita para que sea
mostrada (típicamente a un usuario). Las peticiones de acceso o manipulación de información llegan
al ‘modelo’ a través del ‘controlador’.
Controlador: Responde a eventos (usualmente acciones del usuario) e invoca peticiones al ‘modelo’
cuando se hace alguna solicitud sobre la información (por ejemplo, realizar un cálculo o modificar
un registro en una base de datos). También puede enviar comandos a su ‘vista’ asociada si se solicita
un cambio en la forma en que se presenta el ‘modelo’ (por ejemplo, desplazamiento o scroll por un
documento o por los diferentes registros de una base de datos), por tanto, se podría decir que el
‘controlador’ hace de intermediario entre la ‘vista’ y el ‘modelo’.
Vista: Presenta el ‘modelo’ (información y lógica de negocio) en un formato adecuado para
interactuar (usualmente la interfaz de usuario), por tanto, requiere de dicho ‘modelo’ la información
que debe representar como salida.
Este sistema es bueno como primera aproximación, pero se queda “corto” en cuanto a la división de
responsabilidades, por ejemplo, no queda clara una división entre las entidades que forman el
sistema y las reglas de negocio. Una evolución del patrón MVC son las divisiones en capas
(Controlador, Servicio, Repositorio, etc…) que se emplean actualmente y que se denominan
“arquitecturas limpias”. Dentro de ellas, es común hablar de Arquitectura Hexagonal y Arquitectura
DDD.
13. Excepciones
Gestión de Excepciones
En Java los errores en tiempo de ejecución (cuando se está ejecutando el programa) se
denominan excepciones, y esto ocurre cuando se produce un error en alguna de las instrucciones
de nuestro programa, como por ejemplo cuando se hace una división entre cero, cuando un objeto
es ‘null’ y no puede serlo, cuando no se abre correctamente un fichero, etc. Cuando se produce
una excepción se muestra en la pantalla un mensaje de error y finaliza la ejecución del programa.
En Java (al igual que en otros lenguajes de programación), existen muchos tipos de excepciones y
enumerar cada uno de ellos sería casi una labor infinita.
Cuando en Java se produce una excepción se crea un objeto de una determina clase (dependiendo
del tipo de error que se haya producido), que mantendrá la información sobre el error producido y
nos proporcionará los métodos necesarios para obtener dicha información. Estas clases tienen como
clase padre la clase Throwable, por tanto, se mantiene una jerarquía en las excepciones. A
continuación, mostramos algunas de las clases para que nos hagamos una idea de la jerarquía que
siguen las excepciones, pero existen muchísimas más excepciones que las que mostramos:
La clase Error se utiliza para representar problemas graves fuera del control del programa
(OutOfMemoryError, por ejemplo) y no se suelen capturar ni tratar de forma alguna. Se ha creado
esta clase para diferenciarlas semánticamente del resto de las excepciones, consideradas menos
graves y que son con las que trabajaremos a continuación.
La estructura empleada por Java para la gestión de excepciones es el uso del bloque:
try/catch/finally
La técnica básica consiste en colocar las instrucciones que podrían provocar problemas dentro de
un bloque try, y colocar a continuación uno o más bloques catch, de tal forma que si se provoca
un error de un determinado tipo, lo que haremos será saltar al bloque catch capaz de gestionar ese
tipo de error específico. El bloque catch contendrá el código necesario para gestionar ese tipo
específico de error. Suponiendo que no se hubiesen provocado errores en el bloque try, nunca se
ejecutarían los bloques catch.
try {
division = num1 / num2; //instrucción que puede provocar excecpción.
}
catch (Exception e ) { division = 0; //si se produce excepción
}
En el caso anterior, si el denominador es igual a cero, se produciría una excepción y en vez de romper
el programa, le asignaríamos cero al resultado de la división, y el programa continuaría
normalmente. Se puede añadir una última parte al bloque, y que es opcional llamda:finally que
se ejecuta finalmente, haya o no haya habido excepciones.
El objeto excepción, como vemos en el ejemplo, dispone de métodos que nos proporcionan
información adicional sobre el error, como getMessage(), getClass().getName(), etc.
Decir también que hay muchísimas excepciones en Java por lo que se irán aprendiendo según nos
las vayamos encontrando.
Para saber el nombre de la excepción a tener en cuenta en cada caso, podemos provocarla
previamente, cuando desarrollamos el programa y aplicar luego el nombre obtenido.
Malas prácticas:
try {
// Código que declara lanzar excepciones
} catch(Exception ex) { }
El código anterior ignorará cualquier excepción que se lance dentro del bloque try, o mejor dicho,
capturará toda excepción lanzada dentro del bloque try pero la silenciará no haciendo nada
(frustrando así el principal propósito de la gestión de excepciones). Cualquier error de diseño, de
programación o de funcionamiento en esas líneas de código pasará inadvertido tanto para el
programador como para el usuario. Algo mejor sería así:
try {
// Código que declara lanzar excepciones
} catch(Excepcion ex) {
ex.printStackTrace();
}
printStackTrace () mostraría el código de error por consola, pero no pararía la ejecución del
código, es una buena fórmula para seguir con la ejecución, pero detectando posibles errores.
Throw y Throws
Hasta ahora hemos visto las excepciones que lanza el propio Java cuando se encuentra situaciones
de error en la ejecución de un programa, pero también nos puede interesar que nuestros programas,
bajo determinadas circunstancias, lancen también excepciones. La palabra reservada throw nos
permite hacer esto, nos permite lanzar una excepción de forma explícita. Por ejemplo:
static void rango(int edad)throws ExcepcionIntervalo{
if((edad>120)||(edad<0)){
throw new ExcepcionIntervalo("Edad inválida");}
import java.util.*;
public class gestionExcepciones {
public static void main(String[] args) {
System.out.println("Introduce dos enteros");
Scanner teclado =new Scanner(System.in);
String str1=teclado.nextLine();
String str2=teclado.nextLine();
String respuesta;
int numerador, denominador, cociente;
cociente=division(str1, str2);
respuesta=String.valueOf(cociente);
System.out.println(respuesta);
}
int num1=Integer.parseInt(str1);
int num2=Integer.parseInt(str2);
if(num1<0 || num1>100) throw new ExcepcionIntervalo();
return (num1/num2);
}
class ExcepcionIntervalo extends Exception{
//usamos constructor por defecto, no lo creamos
@Override
public String getMessage(){
return "Error: Número inválido.";
}
}
Vemos en este ejemplo como se generan las excepciones, tanto de sistema como la creada por
nosotros. Con lo que ya sabemos podríamos cambiar esto para capturar esas excepciones y que
nuestro programa no finalice de forma abrupta. Cambiaríamos el main() así:
import java.util.*;
public class gestionExcepciones {
public static void main(String[] args) {
System.out.println("Introduce dos enteros");
Scanner teclado =new Scanner(System.in);
String str1=teclado.nextLine();
String str2=teclado.nextLine();
String respuesta;
int numerador, denominador, cociente;
try{
cociente=division(str1, str2);
respuesta=String.valueOf(cociente);
}catch(NumberFormatException ex){
respuesta="Se han introducido caracteres no numéricos";
//o bien: respuesta=ex.getMessage();
}catch(ArithmeticException ex){
respuesta="División entre cero";
}catch(ExcepcionIntervalo ex){
respuesta=ex.getMessage();
}
System.out.println(respuesta);
}
Podríamos hacer nuestra excepción más compleja, pudiendo mostrar distintos mensajes en función
de los distintos parámetros a la hora de crearla. Haríamos un constructor que tomaría ese parámetro
y compondría el getMessage en función del mismo.
@Override
public String getMessage(){
String mensaje="";
switch (this.parametro) {
case 1: mensaje = "Error: Número menor que cero"; break;
case 2: mensaje = "Error: Número mayor que cero"; break;
}
return mensaje;
}
}
Tipos de Excepciones
En Java se distinguen dos tipos de excepciones: excepciones comprobadas (checked) y no
comprobadas (unchecked).
Una excepción checked representa un error del cual podemos recuperarnos. Por ejemplo, una
operación de lectura/escritura en disco puede fallar porque el fichero no exista, porque este se
encuentre bloqueado por otra aplicación, etc. Todas estas situaciones, además de ser inherentes al
propósito del código que las lanza (lectura/escritura en disco) son totalmente ajenas al propio código,
y deben ser (y de hecho son) declaradas y manejadas mediante excepciones de tipo checked y sus
mecanismos de control.
Por tanto, todas las excepciones de tipo checked deben ser capturadas (try…catch) o relanzadas
(throws) hacia “arriba”, hacia el que llamó al código que produce la excepción (ese “llamante” deberá
capturarla o volver a lanzarla hacia “arriba”).
Una excepción de tipo unchecked representa un error de programación, por ejemplo acceder a
elementos de un array en posiciones mayores que su tamaño. Estas excepciones no es necesario
capturarlas en el código.
Excepciones Frecuentes
14. Ficheros
Entrada/Salida de información. Flujos
Con las estructuras de almacenamiento vistas hasta ahora sólo se podían guardar los datos durante
la ejecución del programa. Para que los datos puedan perdurar de unas ejecuciones a otras, la
solución es lo que se denomina “persistencia” y consiste en almacenar esos datos en disco y no en
estructuras de memoria RAM como la ya vistas: variables, Arrays, etc. La forma habitual de lograr
esa persistencia es mediante ficheros o bien bases de datos. En este tema tratraremos los primeros.
Fichero: se define como una colección de información, que almacenamos en un soporte físico no
volatil, para poderla manipular en cualquier momento.
Atendiendo a la forma en la que se graban los registros podemos clasificarlos en ficheros
secuenciales y aleatorios. En los primeros se escriben y leen de forma secuencial, en orden de
principio a fin, y en los segundos se accede directamente a la información deseada, mediante un
índice.
Flujos: Determinan la comunicación entre un programa y el origen o destino de cierta información,
es decir, es un objeto que hace de intermediario entre el origen y destino de la información. Podemos
definir flujo como una 'abstracción' que proporciona Java y que identifica a una secuencia de bytes
desde un dispositivo de entrada hacia un dispositivo de salida.
A través de dichos flujos vamos a poder realizar lecturas y escrituras sobre diferentes dispositivos
sin que el programador necesita saber nada acerca de ellos, ya que será el núcleo de Java el que se
entenderá con el S.O. para realizar las operaciones correspondientes.
En Java, dichos flujos están identificados por una jerarquía de clases que se encuentran en el
paquete java.io, por lo que añadiremos siempre a nuestros programas y clases el import:
import java.io.*;
Quizás podemos entender más claramente el concepto de flujo si nos fijamos cuando escribimos
por teclado y desde nuestro programa guardamos dicha información o cuando mandamos escribir
por consola un texto determinado.
En Java, las letras (información) que estamos tecleando llegan a nuestro programa a través de un
'flujo' de entrada, denominado 'flujo de entrada estándar' y posteriormente enviamos información a
la consola a través de un 'flujo de salida estándar'. En caso de encontrar algún error también enviará
un mensaje a la consola (como la salida) por medio de otro flujo de datos. Estos flujos en Java los
tenemos implementados en los objetos System.in, System.out y System.err.
Para tratar un archivo en la forma tradicional siempre vamos a tener que realizar las siguientes
operaciones:
• Abrir el archivo para iniciar la lectura/escritura
• Leer /Escribir en el archivo (de forma repetitiva hasta llegar al final del mismo o aleatoria a un
punto concreto)
• Cerrar el archivo.
Todas estas operaciones pueden producir excepciones de tipo IOException por lo que los programas
y clases que trabajen con ficheros deberán realizar estas operaciones en bloques try-catch
capturando este tipo de excepciones y en la signatura incluiremos la cláusula throws:
public static void main(String[] args) throws IOException {
Esta figura muestra todas las clasese interfaces de las que dispone Java para trabajar con ficheros… ¡Un verdadero lío! Pero trataremos
de simplificarlo en los siguientes apartados.
Bufferes: En la jerarquía anterior vemos que hay ciertas clases que implementan un buffer
(BufferedReader, BufferedInputStream,...). Un buffer es una zona de almacenamiento, que se utiliza
de 'puente' entre un dispositivo de entrada y otro de salida, para aumentar la velocidad en las
operaciones de lectura y escritura, así como el rendimiento.
Si usamos sólo las clases básicas para trabajar con ficheros (FileInputStream, FileOuputStream,
FileReader o FileWriter, etc.) cada vez que hagamos una lectura o escritura, se hará físicamente en el
disco duro. Si escribimos o leemos pocos caracteres cada vez, el proceso se hace costoso y lento, con
muchos accesos a disco duro. Los BufferedReader, BufferedInputStream, BufferedWriter y
BufferedOutputStream añaden un buffer intermedio para aumentar el rendimiento.
Cuando leamos o escribamos, esta clase controlará los accesos a disco:
• Si vamos escribiendo, se guardarán los datos hasta que tenga bastantes datos como para
hacer la escritura eficiente.
• Si queremos leer, la clase leerá muchos datos de golpe, aunque sólo nos dé los que hayamos
pedido. En las siguientes lecturas nos dará lo que tiene almacenado, hasta que necesite leer
otra vez.
Esta forma de trabajar hace los accesos a disco más eficientes y el programa correrá más rápido. La
diferencia se notará más cuanto mayor sea el fichero que queremos leer o escribir.
Para estudiar este tema de forma estructurada vamos a dividirlo en dos grandes bloques: los ficheros
de texto y los ficheros de datos binarios. Dentro de los dos bloques distinguiremos la lectura por un
lado y la escritura por otro.
try-with-resources: Es una forma “especial” de usar try de modo que el cierre del flujo se realiza
automáticamente tanto si ha habido alguna excepción como si no. Y por lo tanto no se hace
necesaria la llamada al método close(). La sintaxis es la habitual de un try, pero añade la definición
de los recursos entre paréntesis justo después de la palabra reservada try y antes de la llave de
comienzo de bloque.
try (FileReader fr = new FileReader("fichero.txt") ) {
//tratamiento
}
catch (IOException ex) {
System.err.printf("%nError:%s",ex.getMessage());
}
Podemos ver como se llama a los constructores y se repite el proceso de lectura hasta el final del
archivo.
Clase Scanner
Esta clase ya la hemos utilizado durante el curso como forma de obtener información a través del
teclado. No es una clase que represente un flujo, sino que hace uso de las clases comentadas en el
gráfico inicial. Así podemos ver como utilizar otros flujos de entrada que no sea el teclado. Este sería
un ejemplo de su uso mediante el método hasNextLine() para saber cuando se ha acabado de leer
el fichero (existen múltiples hasXXXXX disponibles en esta clase).
String salida="";
try (FileReader fr = new FileReader("fichero.txt");
Scanner sc = new Scanner(fr)){
while(sc.hasNextLine()){
salida +=sc.nextLine() + '\n';
}
System.out.println(salida);
}
catch (IOException ex) {
System.err.printf("%nError:%s",ex.getMessage()); }
Archivos csv
Un archivo ‘.csv’ (comma-separated values) es un archivo de texto separado cada campo por un
símbolo (en muchos casos, un punto y coma) y cada línea termina por un salto de línea. Es un formato
habitual para convertir hojas de cálculo a archivos de texto. El siguiente ejemplo podría representar
una archivo csv con un nombre, edad y número de teléfono.
Angel:23:981111111
Pedro:45:234232314
La clase String dispone de un método llamado split () que obtiene de una cadena un array con las
sub-cadenas comprendidas entre el delimitado pasado como parámetro. Así que como lo que
leemos en el fichero de texto, son líneas, podríamos trtar cada línea así:
String[] partes = linea.split(";");
if (partes.length == 3)
System.out.printf("%s,%s,%s\n",partes[0],partes[1],partes[2]);
else System.out.println("Error de formato");
El parámetro que recibe split puede ser una expresión regular. Aunque las veremos en detalle en el
tercer trimestre, podremos decir que permite especificar patrones, por ejemplo si queremos obtener
las palabras que componen una frase, el separador será el espacio en blanco pero una o varias veces:
String[] partes = linea.split(" +"); Si queremos indicar que el punto o la coma también
son separadores: String[] partes =línea.split("[., ]+");
File
La clase File es una representación de un fichero o un directorio, permitiendo operaciones como:
• Borrar un archivo.
• Crear un archivo.
• Establecer fecha y hora de modificación del archivo.
• Crear un directorio.
• Listar el contenido de un directorio.
Por ejemplo, podemos hacer uso del método exists() para determinar si un fichero/directorio existe
antes de hacer uso del mismo (hasta ahora hacíamos uso de la excepción FileNotFoundException).
Dispone de diversos constructores:
File (String ruta_completa); File fichero=new File ("archivos\\texto.txt");
File (String ruta, String nombre); File fichero=new File ("archivos","texto.txt");
Y métodos como:
exists() Devuelve true si el fichero ya existe
length() Devuelve el tamaño del fichero
delete() Borra el fichero
renameTo() Renombra el fichero especificado por el objeto File
toString() Devuelve la ruta especificada cuando se creó el objeto File
createNewFile() Crea el fichero (devuelve false si ya existe el fichero)
Copiar un archivo
La clase File no ofrece ningún método para copiar archivos. Podríamos hacerlo “a mano”, creando
un archivo con un flujo de escritura e ir leyendo del origen y escribiendo en el archivo destino.
Una forma más cómoda de hacerlo es mediante la clase java.nio.file.Files que tiene método
copy, a la que le pasaremos ruta origen y ruta destino. Ejemplo:
import java.nio.file.Files;
. . .
File fo = new File("fichOrigen.txt");
File fd = new File("fichDestino.txt");
try {if (fd.exists()) fd.delete();
Files.copy(f.toPath(), b.toPath());
} catch (IOException ex) {System.err.printf("Error:%s",ex.getMessage());
Cuando distribuyamos nuestra aplicación crearemos un archivo .jar mediante la opción “Build” de
nuestro IDE. Ese archivo contendrá las clases compiladas a bytecode (archivos .class) y otros recursos
como imágenes o iconos, pero no los archivos de datos.
Los archivos deberán estar en el sistema de archivos de nuestro sistema, no en .jar, por ello, lo más
habitual es crear en el IDE una carpeta para los archivos en la raíz de nuestro proyecto y referenciarla
con posicionamiento relativo, por ejemplo: File fichero=new File ("data/facturas.csv");
Al distribuir la aplicación, podemos generar un archivo comprimido con el .jar y la carpeta con los
ficheros.
Otras clases
Existen muchas otras clases con características y métodos específicos que pueden ser útiles en
determinados contextos: ChareacterArrayReader, StringReader, PipedReader, PushbackReader,
LineNumberReader, etc.
• Deberíamos cerrar el fichero de forma explícita con el método close(), aunque desde la
versión JDK7 no es necesario si usamos try-with-resources.
Teniendo en cuenta estas consideraciones, la estructura que emplearemos habitualmente para la
escritura secuencial de ficheros de texto será esta:
PrintWriter
Permite enviar cadenas de caracteres con un formato a un flujo de salida (Writer, File, OutputStream
o Cadena con nombre del fichero) con métodos que ya utilizamos para la salida por consola: print(),
println(), printf() .
Tiene varios constructores en los que le podemos pasar como parámetro un File o un
OutputStream/FileWriter. En el primer caso se le puede pasar adicionalmente el tipo de codificación
y en segundo caso un boolean que representa si el ‘autoflush’ está activado o no. Ejemplos:
File fichero = new File("fichero.txt");
try(PrintWriter pw = new PrintWriter(fichero,"UTF-8")){ }
Los métodos de escritura de esta clase no lanzan excepciones IOException en caso de error. El
programador debe hacer uso de los métodos checkError() para comprobar si la llamada al método
produjo un error y clearError() para eliminar ese error ypoder chequear otro posterior.
Si está habilitado el 'autoflush', cada vez que se invoque a los métodos 'println', 'printf' o 'format' se
escribirá en el fichero. Sino será necesario llamar al método 'flush' o cerrar el flujo para que se envíen.
Como en casos anteriores, podemos hacer que un FileWriter sea 'envuelto' por un 'BufferedWriter'
para hacer uso de un buffer y aumentar el rendimiento y a su vez, el BufferedWriter será 'envuelto'
por un PrintWriter para poder enviar al flujo de salida diferentes tipos de datos, como flotantes,
booleano o cualquiera de los que soporta la clase PrintWriter.
De esta forma nos evitamos tener que estar convirtiendo los datos a enviar al flujo.
Fijarse que el orden en el que se envuelven las clases es importante, ya que podríamos haber hecho
que la clase BufferedWriter envolviera a la PrintWriter, pero de esa forma sólo, si quisiéramos utilizar
el buffer sólo lo podríamos conseguir llamando a los métodos de BufferedWriter, no a los de
PrintWriter.
File f = new File("fichero.txt"); double num=2.3232d;
try (
FileOutputStream fos =new FileOutputStream(f, true);
OutputStreamWriter osw =new OutputStreamWriter(fos,"UTF-8"); //"ISO-8859-1"
BufferedWriter bfw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bfw, true)) {
pw.printf("num=%06.1f\n",num);
pw.println("linea nueva");
} catch (IOException ex) {
System.err.printf("Error:%s",ex.getMessage()); }
Con el ejemplo anterior obtendíamos un fichero binario con los cuatro valores escritos. Como son
datos binarios necestaremos un visor de datos hexadecimales (por ejemplo, ICY Hexplorer en
Windows):
Analicemos el archivo:
• Por cada carácter está utilizando dos bytes. Además, al usar el método writeUTF() envía en
primer lugar la longitud de la cadena guardada.
o 00 0B => 11 caracteres/bytes que ocupa la cadena y luego la cadena
o x56 => V x41 => A, x4C=L, etc…
• Después va el valor de PI. Como es un double, ocupa 8 bytes.
o 40 09 21 FF 2E 48 E8 A7
• Y otra cadena
o 00 0A => 10 caracteres/bytes que ocupa la cadena y luego la cadena
o x76 => v x61 => a, x6C=l, etc…
• Y finalmente el valor de E:
o 40 05 BE 76 C8 B4 39 58
Clase Files
La clase Files, incorporada en la versión 7 de Java, incluye muchos métodos estáticos para la
manipulación de archivos (copiar, renombrar, consultar atributos, etc.) e incorpora también métodos
que permiten leer y escribir ficheros completos en una sola operación, a partir de una estructura en
memoria. Serían los siguientes:
• readAllLines (Path, CharSet): Lee todas las líneas y las devuelve en una List de String.
• readAllBytes (Path): Lee todos sus bytes y devuelve un array de byte con todo su contenido.
• write (Path, byte[] , options): Escribe el array de bytes pasado en el fichero p. Devuelve el
path. En las opciones se le puede especificar si añade o sobreescribe el fichero.
• write (Path, Colección de líneas, Charset): Escribe las líneas de texto al archivo.
En este curso nos centraremos solo en el primero:
List<String> lineas;
try {
Path path = Paths.get("fich.txt");
lineas = Files.readAllLines(path, StandardCharsets.ISO_8859_1);
}
catch (IOException ex) {System.err.printf("%nError:%s",ex.getMessage());}
En un solo paso, leemos todo el archivo y lo pasamos a memoria. Obviamente, si el fichero es muy
grande, la List en memoria podría ser poco eficiente. En el ejemplo expuesto, la colección líneas sería
una lista con un elemento por cada línea leída. Podríamos recorrerla con un for…each:
for (String linea : lineas)
System.out.println (linea);
(Opcional)
Hasta ahora hemos recorrido los ficheros de forma secuencial. Esto es, para posicionarnos en una
determinada posición del fichero debemos de leer previamente todas las posiciones anteriores.
Pero si tenemos un fichero en el que guardamos registros de datos, con un tamaño fijo determinado,
puede ser útil que podemos posicionarnos en un registro determinado sin necesidad de pasar por
los anteriores.
La clase que permite realizar estas operaciones es la clase RandomAccessFile.
Además de esta ventaja, esta clase permite realizar operaciones de lectura/escritura sobre un
archivo. No se necesitan dos clases como hasta ahora.
En el constructor enviaremos un objeto de la clase File o una cadena, que va a indicar el archivo a
abrir, y un segundo parámetro (mode) que es una cadena que indica el modo de acceso al fichero:
"r": Lectura
"rw": Lectura / Escritura
Como podemos comprobar en el gráfico del principio, la clase RandomAccessFile incorpora las
interfaces DataInput y DataOutput. Esto lleva consigo que esta clase va a disponer de los métodos
writeXXX y readXXX de los tipos primitivos de Java, así como writeUTF y readUTF, pasando dichos
datos a bytes.
Métodos más importantes:
• getFilePointer(): Devuelve la posición actual en el fichero.
• seek(long pos): Permite posicionarse en la posición indicada por pos.
• length(): Tamaño del fichero.
• skipBytes(int num): Desplaza la posición del puntero ‘num’ bytes.
Para añadir datos a un fichero debemos posicionarnos al final del mismo y llamar al método
writeXXXX que queramos. Si nos situamos en la posición de un registro existente, los datos serán
reemplazados al escribir.
Para ver un ejemplo, supongamos un registro con: String nombre (46 chars),int edad (4bytes) = 50
Bytes. El código para escribir añadir registros podría ser así:
En el ejemplo anterior, vemos como el nombre (String) se almacena como un conjunto constante
de 50B y no como vimos anteriormente, con dos Bytes previos que indicaban la longitud. Así
logramos que todos los registros sean del mismo tamaño.
Veamos ahora el ejemplo inverso, que lee una posición determinada del archivo, en este caso la
décima. Fíjate como tratamos la cadena sin los dos bytes iniciales que indicarían el tamaño (no
podemos usar readUFT):
Serialización de objetos
En Java disponemos de clases que permiten establecer un flujo de entrada (lectura) / salida (escritura)
de objetos. La serialización es el proceso por el que un objeto se convierte en una secuencia de bytes,
permitiendo guardar su contenido en un archivo:
• Cuando escribimos un objeto a disco lo que hace la clase ObjectOutputStream es convertir el
contenido de cada uno de los campos a binario y lo guarda en disco.
• Cuando leemos un objeto de disco, lo que hace la clase ObjectInputStream es leer un flujo de
bytes que después, mediante un cast, guardará en cada uno de los campos del objeto.
Para que un objeto pueda ser serializado debe de implementar la interface java.io.Serializable. Esta
interface no define ningún método, pero toda clase que la implemente, informará a la JVM que el
objeto será serializado.
Todos los tipos primitivos de datos en Java son serializables al igual que los arrays. Si una clase tiene
como 'dato' algún objeto de otra clase, esa otra clase debe implementar la interface Serializable.
Podemos indicarle a Java que un atributo de una clase no sea serializado (y por lo tanto no lo
guardará) con la palabra clave trascient de la forma:
private transient String ejemplAtributo;
Importante:
La serialización no permite añadir objetos a un archivo conservando los que
tuviera previamente. Esto es debido a que cada vez que se añade un objeto habiendo
cerrado el archivo, se añade una cabecera. Si añadiésemos, se crearía una nueva
cabecera cada vez, y los archivos tendrían un número indeterminado de cabeceras.
Un ObjectInputStream solo va a leer una cabecera.
Cuidado con el método readObject(). Este método no indica cuando se acaba el fichero,
por lo que si estamos haciendo un bucle while leyendo objetos, la condición del while
para salir podría ser llamando al método available() de un flujo de bytes asociado al
fichero (leeríamos mientras el método devuelva un valor mayor que 0). Otra forma
sería capturar la excepción EOFException.
Cuando leemos un objeto, tenemos que hacer un cast a la clase a la que pertenece.
Sin embargo, aplicando el polimorfismo, podemos hacer un cast a la clase 'común' pero
aún así, el objeto tendrá toda la información de su clase original, de tal forma que si
lo convertimos a su clase original (con otro cast) podremos acceder a todos sus
métodos y propiedades.
}
catch (IOException ex) {System.err.println("Error:"+ ex.getMessage()); }
La operación contraria, leer las instancias de Persona desde el disco sería como se muestra a
continuación: (vamos a suponer que el número de objetos leídos nunca será superior a 100)
Persona[] pers = new Persona[100];
boolean eof = false;
File fichero = new File("fichero.dat");
try( FileInputStream fis = new FileInputStream(fichero);
BufferedInputStream bufis = new BufferedInputStream(fis);
ObjectInputStream ois = new ObjectInputStream(bufis)){
int cont = 0;
while(!eof) { //while(bufis.available()>0
personas[cont] = (Persona)ois.readObject();
if (++cont> personas.length) break;
}
} catch (EOFException e) {eof = true;
} catch (IOException ex) { System.err.println("Error:"+ ex.getMessage()); }
} catch (ClassNotFoundException ex) { System.err.println("Err:"+ ex.getMessage());
Entrada/Salida estándar
A nivel de sistema operativo (sobre todo en Linux/Unix) se identifica la entrada estándar (stdin) con
el teclado y la salida estándar (stdout) como la pantalla. También se dispone de la salida de errores
(stderr) que también va a la pantalla.
Java tiene acceso a estos flujos estándar a través de la clase System. Así:
• Stdin: Es un objeto de la clase InputStream y es el flujo de entrada estándar (lectura por
teclado). Dicho flujo se puede redirigir con el método System.setIn(InputStream). Podemos
acceder a él de la forma: System.in.
• Stdout: Es un objeto de la clase PrintStream. Se pueden utilizar los métodos print y println. Se
puede redirigir llamando al método System.setOut(PrintStream). Podemos acceder a él de la
forma: System.out.
• Stderr: Es un objeto de la clase PrintStream. Se pueden utilizar los métodos print y println. Se
puede redirigir llamando al método System.setErr(PrintStream). Podemos acceder a él de la
forma: System.err.
Si cambiamos la entrada / salida / errores estándar y queremos volver a su 'valor original', tendremos
que emplear la clase FileDescriptor para tener una referencia a los flujos originales de entrada, salida
y errores de la forma: FileDescriptor.in, FileDescriptor.out y FileDescriptor.err.
Para redireccionar a la salida estándar por defecto, podríamos poner: System.setOut(new
PrintStream(new FileOutputStream(FileDescriptor.out)));
En el comienzo de este curso vimos que, para leer del teclado, hacíamos uso de la clase Scanner.
Utilizando los conceptos vistos de salida y entrada estándar podemos hacer una redirección de la
salida estándar (podríamos hacerlo con la salida de errores o la entrada estándar) a un fichero.
Para ello debemos hacer uso del método setOut() de la clase System.
FileReader f = null;int c;
PrintStream pStreamSalida=null;
try {pStreamSalida = new PrintStream("fichero.txt");
System.setOut(pStreamSalida);
f = new FileReader("fichero.txt");
while((c = f.read())!=-1) {
System.out.print((char)c);
}
} catch (IOException ex) {System.err.println("Error I/O ");}
finally {try {
if (f != null)
f.close();
} catch (IOException ex) {System.err.println("Error I/O"); }
}
Cualquier System.out que hagamos después de redirigir la salida estándar será enviado al fichero
"fichero.txt"
Clase Properties
Java dispone de librerías específicas para trabajar con ficheros de configuración, esto es ficheros que
se componen de parejas de variables y valores, típicos en casi cualquier aplicación, servicio, etc.
Como todos siguen patrón similar, es la librería la que se encarga de acceder al fichero a bajo nivel y
nosotros sólo tenemos que indicar la propiedad a leer/escribir.
Ejemplo:
# Fichero de configuración
# Thu Feb 13 10:49:39 CET 2020
user=usuario
password=mypassword
server=localhost
port=3306
Tanto para escribir como para leer este tipo de ficheros, hay que tener en cuenta que, al tratarse de
ficheros de texto, toda la información se almacena como si de un String se tratara. Por tanto, todos
aquellos tipos Date, boolean o incluso cualquier tipo numérico serán almacenados en formato texto.
Así, habrá que tener en cuenta las siguientes consideraciones:
• Para el caso de las fechas, deberán ser convertidas a texto cando se quieran escribir y
nuevamente reconvertidas a Date cuando se lea el fichero y queramos trabajar con ellas
• Para el caso de los tipos boolean, podemos usar el método String.valueOf(boolean) para
pasarlos a String cuando queramos escribirlos. En caso de que queramos leer el fichero y
pasar el valor a tipo boolean podremos usar el método Boolean.parseBoolean(String)
• Para el caso de los tipos numéricos (integer, float, double) es muy sencillo ya que Java los
convertirá a String cuando sea necesario al escribir el fichero. En el caso de que queramos
leerlo y convertirlos a su tipo concreto, podremos usar los métodos Integer.parseInt
(String), Float.parseFloat(String) y Double.parseDouble(), según proceda.
• Las líneas que comienzan por '#' o '!' se interpretan como comentarios.
• En las parejas de propiedad valor el seperador entre la clave y el valor puede ser '=' o
bien ':'.
15. Colecciones
Las colecciones representan grupos de objetos, denominados elementos que podemos tratar de una
forma conjunta, por ejemplo, recorriéndolos o accediendo a ellos individualmente. Un ejemplo de
colección, con la que hemos trabajado a lo largo de este curso son los ArrayList.
Podemos encontrar diversos tipos de colecciones, según si sus elementos tienen una posición
determinada o no, o si se permite repetición de elementos o no. En todos los casos, el tamaño de las
colecciones es dinámico, esto es, podremos añadir y eliminar los elementos que sea necesario.
Para usar estas colecciones haremos uso del Java Collections Framework (JCF), el cual contiene un
conjunto de clases e interfaces del paquete java.util para gestionar colecciones de objetos. Una
limitación de las colecciones es que solo se aplican a objetos, no a tipos primitivos.
Todas las colecciones implementan la interfaz Collection, en la que encontramos una serie de
métodos que nos servirán para acceder a los elementos de cualquier colección de datos, sea del tipo
que sea. Estos métodos generales son:
• boolean add (Object o): Añade un elemento (objeto) a la colección. Nos devuelve true si se ha
añadido el elemento correctamente, o false en caso contrario.
• void clear () Elimina todos los elementos de la colección.
• boolean contains (Object o) Indica si la colección contiene el elemento (objeto) indicado.
• boolean isEmpty() Indica si la colección está vacía (no tiene ningún elemento).
• Iterator iterator() Proporciona un iterador para acceder a los elementos de la colección (lo
veremos más en detalle)
• boolean remove (Object o) Elimina un elemento (objeto) de la colección, devolviendo true si
dicho elemento estaba contenido en la colección, y false en caso contrario.
• int size() Nos devuelve el número de elementos que contiene la colección.
• Object [] toArray() devuelve la colección como un array de objetos. Para llamarlo, crearemos e
instanciaremos previamente el array destino.
String [] cadenas = new String[MiColeccion.size()];
MiColeccion.toArray(cadenas);
Esta interfaz es muy genérica, y por lo tanto no hay ningún tipo de datos que la implemente
directamente, sino que implementarán subtipos de ellas.
En Java las principales interfaces de las que disponemos para trabajar con colecciones son: Set, List,
y Map y las clases más interesantes y que vamos a utilizar son las siguientes:
List ArrayList, Vector y LinkedList
Set HashSet, TreeSet y LinkedHashSet
Map HashMap, TreeMap y LinkedHashMap
Queue Priority Queue, ArrayDeque
A grandes rasgos, las clases que implementan List almacenan los elementos en cierto orden, admiten
duplicados y permiten acceder a ellos por su posición. Las clases de Set tienen como principal
característica que no admiten duplicados y, por último, los Map permite acceder por claves y valores
en vez de por posiciones.
Como ya hablamos en el capítulo de ArrayList, hay métodos que necesitan comparan objetos para
ver si son iguales (por ejemplo: contains(Object) compara el elemento pasado con cada elemento de
la colección y devuelve true si lo encuentra, remove(Object) borra el objeto si lo encuentra, etc.). y
decíamos que esos métodos requerían redefinir equals(Object). Eso ocurre con el resto de
colecciones y, adicionalmente, deberemos redefinir hashCode() como explicaremos más adelante.
Sería inabarcable hablar de todas las colecciones, con sus particularidades y métodos, vamos a ver
las más importantes, dividiéndolas en las cuatro interfaces principales: List, Set y Map.
Incorpora métodos nuevos, no disponibles en otras colecciones, que hacen referencia a estos
índices:
void add (int índice, Object obj): Inserta un elemento (objeto) en la posición de la lista dada por el
índice indicado.
Object get (int índice): Obtiene el elemento (objeto) de la posición de la lista dada por el índice
indicado.
int indexOf (Object obj): Nos dice cuál es el índice de dicho elemento (objeto) dentro de la lista. Nos
devuelve -1 si el objeto no se encuentra en la lista.
Object remove (int índice): Elimina el elemento que se encuentre en la posición de la lista indicada
mediante dicho índice, devolviéndonos el objeto eliminado.
Object set (int índice, Object obj): Establece el elemento de la lista en la posición dada por el índice al
objeto indicado, sobrescribiendo el objeto que hubiera anteriormente en dicha posición. Nos
devolverá el elemento que había previamente en dicha posición.
Podemos encontrar diferentes clases que implementan esta interfaz: ArrayList ya vista en capítulos
anteriores, LinkedList y las obsoletas Vector y su hija Stack.
Clase ArrayList
Esta clase, ya vista en capítulos previos, Implementa una lista de elementos mediante un array de
tamaño variable. Conforme se añaden elementos el tamaño del array irá creciendo si es necesario.
El array tendrá una capacidad inicial, y en el momento en el que se rebase dicha capacidad, se
aumentará el tamaño del array.
Las operaciones de añadir un elemento al final del array (add), y de establecer u obtener el elemento
en una determinada posición (get/set) tienen un coste temporal constante. Las inserciones y
borrados tienen un coste lineal dependiente del número de elementos del array.
Clase LinkedList
Es como un ArrayList, pero los elementos están conectados con el anterior y el posterior permitiendo
gestión tanto por el principio como el final de la lista. Cuando realicemos inserciones, borrados o
lecturas en los extremos inicial o final de la lista el tiempo será constante, mientras que para
cualquier operación en la que necesitemos localizar un determinado índice dentro de la lista
deberemos recorrer la lista de inicio a fin, por lo que el coste será lineal con el tamaño de la lista.
Para aprovechar las ventajas que tenemos en el coste temporal al trabajar con los extremos de la
lista, se proporcionan métodos propios para acceder a ellos en tiempo constante:
• void addFirst (Object o) / void addLast (Object o): Añade el objeto indicado al principio / final de
la lista respectivamente.
• Object getFirst() / Object getLast(): Obtiene el primer / último objeto de la lista respectivamente.
Hemos de destacar que estos métodos nos permitirán trabajar con la lista como si se tratase de una
pila o de una cola. En el caso de la pila realizaremos la inserción y la extracción de elementos por el
mismo extremo, mientras que para la cola insertaremos por un extremo y extraeremos por el otro.
• poll() /pop(): elimina y devuelve la cabeza de la pila (el primer elemento) Sería similar a
removeFirst().
• push():añade un elemento en la cabeza de la pila (el primer elemento(). Sería similar a addFirst().
• peek(): devuelve la cabeza de la pila - como poll()/pop() – pero sin eliminarlos.
Las colecciones desde Java9 tienen un método estático de factoría (hace la función de constructor)
llamado ‘of’, y se usa así:
Implementa los métodos vistos para la interfaz Collection. El método add añadirá el elemento si no
Interfaces y clases:
Como ya comentamos en el capítulo de polimorfismo e interfaces, se suelen crear variables o
referencias del tipo de la interfaz y se instancian con una clase que implemente la interfaz. La
limitación de esta técnica es que los atributos/métodos que podremos usar serán los
definidos a la interfaz o nos veremos obligados a hacer castings.
Así pues, será frecuente ver definidas colecciones de esta forma:
List <String> lista1 = new LinkedList<>();
List <String> lista2 = new ArrayList<>();
Un conjunto podrá contener a lo sumo un elemento null y podremos recorrerlo con un for-each o
mediante un Iterator. Esta última forma la veremos en apartados posteriores.
Esta interfaz es implementada por distintas clases, de las que destacaremos: HashSet,
LinkedHashSet y TreeSet.
Clase HashSet
Los objetos de esta clase (como todas las que llevan la partícula hash en su nombre) se almacenan
en una tabla de dispersión (hash).
En una estructura hash se almacena cada dato en una posición calculada a partir de una fórmula,
una operación sobre sus datos. Así, los datos se dispersan y su acceso es más eficiente que en una
estructura no ordenada.
Llevado a la vida real, imagina que tienes que guardar 30 DNI físicamente en una caja. Para acceder
a ellos de forma rápida podrías guardarlos ordenadamente pero cada vez que te diesen uno nuevo,
insertarlo ordenado sería laborioso (costoso en tiempo). Sabiendo que la letra de los DNI españoles
puede tener 23 valores distintos, lo que podrías hacer, es tener 23 cajas, cada una etiquetada con
una letra, y meter en cada una los DNI cuya letra sea la de la caja. Los DNI se “dispersarían” por las
cajas. La búsqueda sería rápida y añadir nuevos DNI también. En este caso, obtener la letra es la
función hash. Si tuvieras 1000 DNI cada caja tendría muchos DNI, lo que te ralentizaría las
búsquedas, por lo que te interesaría tener más cajas, necesitarías una función hash distinta. Las
“cajas” en Java se llaman “buckets” y que dos DNI vayan a la misma caja se llama “colisión”. Lo
óptimo es reducir el número de colisiones.
En el siguiente ejemplo, vemos como a partir del nombre se calcula una posición en la estructura,
y si hay una colisión (como Jack Williams y Andrew Wilson), se enlazan unos con otros.
Como todas las clases que implementan la interface Set, no admite duplicados. Para identificar los
duplicados utilizará los método equals() y hashCode(), que habrá de redefinir como ya comentamos
previamente. Recordemos que la definición por defecto de estos métodos en la clase Object,
compara la referencia de cada objeto, y dos objetos distintos aun con todos los atributos iguales,
produciría un false en el equals() y distinto valor de hashcode().
El método hashcode() deberá devolver un valor para cada objeto, de forma que dos objetos que
consideremos que son iguales, tengan el mismo hashcode(), yendo a la par con equals(). Los
generadores de código de los IDE nos ayudan en esta labor.
Siempre que redefinamos equals() hay que redefinir hashcode(), ya que si dos objetos
son iguales según equals(), sus métodos hashcode() deben devolver lo mismo.
Ejemplo de equals() y hashCode() generado por Netbeans para una clase Producto, a partir de su
atributo nombre. Las operaciones que se pueden ver en el método buscan la mayor “dispersión” de
los elementos.
Netbeans usa una forma con números primos para lograr más dispersión, pero podríamos generarlo
de forma más sencilla, así:
public int hashCode() { return Objects.hash(nombre);}
Por último, comentar que esta implementación no trabaja con índices y no garantiza el orden de los
elementos a través del tiempo. Como ejemplo, podríamos pensar en una lista de la compra ya que
no hay elementos repetidos y no nos importa el orden en el cual encontremos los elementos en la
lista.
Ejemplo: Lista de la compra (no nos importa el orden, pero no hay repetidos).
HashSet<Producto> listaCompra = new HashSet<>();
if (listaCompra.add(new Producto("Platanos", 2.5)))
System.out.println("Añadido correctamente");
else System.out.println("No se puede añadir. Repetido");
double total=0;
for (Producto p : listaCompra) total +=p.precio;
Clase LinkedHashSet
Es similar al HashSet, pero los elementos, además del almacenamiento de tipo hash, están enlazados
entre sí según el orden de inserción, lo que representa una mejora en rendimiento en caso de querer
recorrer todo el conjunto, mientras que las operaciones básicas seguirán teniendo coste constante
similar a HashSet (salvo la carga adicional que supone tener que gestionar los enlaces).
Si hacemos un recorrido for…each garantizamos que los recorrerá en el orden de inserción, algo que
no podríamos garantizar en un HashSet.
En el gráfico, las flechas de la derecha representan los enlaces entre los elementos, por orden de inserción.
Clase TreeSet
Similar a un HashSet, no admite duplicados y está ordenado ascendentemente. Para ambas tareas
(duplicados y ordenación) emplea el método compareTo() de la clase contenida en el Set o bien el
Comparator indicado en el constructor. En un apartado posterior detallaremos estas dos interfaces:
Comparable y Comparator y explicaremos ese método en detalle.
A diferencia de TreeSet, HashSet usaba equals() y hashCode() para evitar duplicados.
En secciones posteriores de este capítulo veremos en detalle Comparable y Comparator. Para su
almacenamiento usa un árbol en vez de una tabla hash. Por lo tanto, el coste para realizar las
operaciones básicas será logarítmico con el número de elementos que tenga el conjunto.
HashMap
Utiliza una tabla de dispersión para almacenar la información del mapa con la técnica de hashing
que describimos previamente. Las operaciones básicas (get y put) se harán en tiempo constante
siempre que se dispersen adecuadamente los elementos. Es coste de la iteración dependerá del
número de entradas de la tabla y del número de elementos del mapa.
Características:
- No se garantiza que se respete el orden de las claves y permite una sola clave igual a null y
múltiples valores iguales a null.
Existe otra clase llamada ‘HashTable’ similar a HashMap, que a diferencia de éste
no admite ningún valor nulo, ni en la clave ni en el valor y además está sincronizada,
lo que la hace aconsejable para aplicaciones multihilo.
- La clave y el valor tienes que ser clases, no tipos primitivos.
- La clase de la clave del mapa tiene que tener definido el método hashcode() ya que a la hora
de introducir una nueva clave, es la función que determina si ya existe (y por tanto la
sustituirá) o no existe (y la añadirá). Las clases típicas como Integer, String, etc. ya lo tienen
definido.
Ejemplo: País con cantidad de habitantes.
HashMap<String, Integer> mapaPaises = new HashMap<>();
mapaPaises.put("España", 47000000);
if (mapaPaises.containsKey("Portugal"))
mapaPaises.put ("Portugal", mapaPaises.get("Portugal ")+100000);
else mapaPaises.put ("Portugal", 0);
Para recorrerlos podremos utilizar un for…each (o un Iterador como veremos más adelante).
for (String k : mapaPaises.keySet())
System.out.println(k + " tiene " + mapaPaises.get(k) + " habitantes.");
TreeMap
Utiliza un árbol para implementar el mapa de forma que los elementos se encontrarán ordenados
por orden ascendente de clave.
Ya que está ordenado incorpora métodos que no tienen sentido para un HashMap pero sí para un
mapa ordenado:
firstKey(), firstEntry(), lastKey(), lastEntry() y otros similares.
Ver: https://docs.oracle.com/javase/7/docs/api/java/util/TreeMap.html#method_summary
Entry <String,String> ent = miTreeMap.firstEntry();
System.out.println(ent.getKey()+" ==> "+ent.getValue());
Análogamente a lo que ocurre con TreeSet, la clase contenida en la clave del mapa debe implementar
Comparable y desarrollar el método compareTo(), en cambio no necesita equals() ni hashcode(). Más
adelante detallamos esta interfaz y el método.
El funcionamiento será igual HashMap, con un rendimiento peor en las operaciones básicas que el
HashMap en inserción ya que tiene que mantener adicionalmente el árbol de ordenación.
Una clase interna es una clase no estática que se define en el cuerpo de otra clase. La gran
funcionalidad de estas clases internas es que pueden acceder a los atributos de la clase que las
envuelve. Ejemplo:
public class Externa {
// atributos clase externa
// constructor y métodos
Para crear una instancia de la clase interna hay que emplear esta fórmula (suponiendo
constructores sin parámetros):
ClaseExterna e = new ClaseExterna();
ClaseExterna.ClaseInterna i = e.new ClaseInterna();
Viendo la definición de clase interna del cuadro azul previo, crearemos una instancia del iterador de
una colección así:
ArrayList<String> lista= new ArrayList<>();
Iterator<String> iterator = lista.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if(condicion_borrado(item)) iterator.remove();
}
Como ya mencionamos estas operaciones las podemos hacer con una sintaxis más sencilla
empleando for…each. Hay que destacar que el for…each no permite el borrado que sí lo
podemos hacer con el método remove() del iterador.
Tenemos también el método descendingIterator() para instanciar el iterador.
para utilizarlo:
List<Integer> lista = Arrays.asList(1,2,3,4,5,6);
Iterator it = new IteradorPosPares(lista);
while (it.hasNext()) System.out.println(it.next());
- La clase sobre la que queremos iterar debe definir el método iterator() de la interfaz Iterable.
Ese método Iterator() tiene las siguientes características:
o Define una clase interna que implementa la interfaz Iterator.
o Esa clase interna debe escribir los métodos hasNext y next() de la interfaz Iterator.
o El método devolverá una instancia de esa clase interna.
- Una vez hecho esto, podremos usar un for…each para recorrer los elementos de nuestra clase.
Este es un ejemplo que define una colección llamada MiColección que tiene un array de 20
posiciones, y un iterador para recorrer solo los pares.
class MiColeccion implements Iterable <Integer> {
MiColeccion() {
for (int i = 0; i < TAM; i++) arr[i] = i * 10;
}
@Override
public Iterator<Integer> iterator() {
Iterator<Integer> it = new MiIterador();
return it;
}
class MiIterador implements Iterator <Integer> {
@Override
public boolean hasNext() {
if (sig < TAM) return true;
return false;
}
@Override
public Integer next() {
int val = arr[sig];
sig += 2;
return val;
}
}
}
Y lo usaríamos así:
MiColeccion m = new MiColeccion();
for (Integer i : m) System.out.println(i);
La clase MiIterador se puede definir como anónima, pero aún no sabemos cómo hacer
eso, lo veremos en capítulos posteriores.
Recorrer mapas
Vamos a ver las 2 formas de recorrer un HashMap, usando entrySet() o usando keySet().
Con keySet() lo que se obtiene como indica el nombre de la función son las claves y mediante un
iterador se recorre la lista de claves. De esta forma si queremos saber también el valor de cada
elemento tenemos que usar la función get(clave).
HashMap<String,Float> listaProductos;
Iterator<String> productos = listaProductos.keySet().iterator();
while(productos.hasNext()){
String clave = productos.next();
System.out.println(clave + " - " + listaProductos.get(clave));
}
La otra opción es entrySet() con la que se obtienen los valores y al igual que en el caso anterior con
un iterador se recorre el HashMap, pero de esta forma hay que crear una variable de tipo Map.Entry
para almacenar el elemento y con los métodos getKey() y getValue() de Map.Entry se obtienen los
valores.
Se puede usar un iterador del tipo que vamos a coger en este caso Map.Entry de la misma forma que
se hizo en el método anterior, o sino usar un iterador genérico y luego hacer el casting a Map.Entry.
HashMap<String, Float> listaProductos;
Iterator iterador = listaProductos.entrySet().iterator();
Map.Entry producto;
while (iterador.hasNext()) {
producto = (Map.Entry) iterador.next();
System.out.println(producto.getKey() + " - " + producto.getValue());
}
}
Con esta segunda forma es necesario usar una variable Map.Entry. El resultado es el mismo, aunque
esta segunda forma es más eficiente puesto que mientras que en la primera solo obtenemos la clave
y luego hay que buscar el contenido asociado con la función get(clave) con esta segunda forma ya
tenemos ambos valores y no hay que realizar esa búsqueda adicional, aunque quizás la primera
forma sea más sencilla.
Comparable y Comparator
Interfaz Comparable
Comparable es una interfaz que nos permite ordenar los elementos de una colección según los
criterios que queramos. Esta interfaz debe ser implementada por la clase que forma la colección. La
forma de ordenar (que atributos son los que definen el orden, orden ascendente o descendente,
etc.) la marca el método abstracto compareTo(Object o).
La clase debe redefinir este método de forma que devuelva un entero negativo si el objeto es menor
que el pasado como parámetro, un entero positivo si el objeto es mayor que el pasado como
parámetro, 0 si son iguales. Y ese será el criterio de ordenación.
Este método es usado por TreeSet y TreeMap (implementando Comparable) para mantener su orden
interno y evitar duplicados, y también será el que se emplee para ordenar listas cuando hacemos
Collection.sort (milista);
Este ejemplo ordena una lista de películas ascendentemente por su año.
public class Main {
public static void main(String[] args) {
List<Peli> lista = new ArrayList<>();
lista.add(new Peli("Episode 7: The Force Awakens", 2015));
lista.add(new Peli("Episode 4: A New Hope", 1977));
lista.add(new Peli("Episode 1: The Phantom Menace", 1999));
Collections.sort(lista);
for (Peli p: lista) System.out.println(p);
}
}
@Override
public int compareTo(Object o) {
Peli m = (Peli) o;
return this.año - m.año;
}
@Override
public String toString (){
return "("+this.año +") " + this.nombre;
}
}
¡Ojo! Un TreeSet de Peli estaría ordenado también por año, pero por otra parte no habría
duplicados por año…algo un poco extraño, así que compareTo() debería ser análogo a
equals() y hashCode() y por ejemplo, en este caso, trabajar sobre ‘nombre’ y no ‘año’.
El problema de la interfaz comparable es si queremos ordenar la colección por distintos criterios, es
decir imaginemos que queremos que la lista de películas en algún momento esté ordenada por el
año, pero en otros casos, por el nombre. La solución la proporciona la interfaz Comparator.
Interfaz Comparator
Aunque la misión de Comparator es igual a la de Comparable, lo hace mediante una clase externa,
no implementada mediante la clase objeto de ordenación. Por lo tanto, podemos crear tantas clases
que la implementen, cada una con sus criterios.
Esa es la gran diferencia: con Comparable solo podemos tener un criterio de ordenación para una
clase (criterio definido en la propia clase), y con Comparator podemos ordenar por distintos criterios,
(criterios definidos en clases independientes) y usaremos el que queramos en cada ocasión.
class Peli {
public String nombre;
public int año;
@Override
public String toString (){
return "("+this.año+") " + this.nombre;
}
}
Fíjate que para ordenar por Comparable hay que modificar la clase que queremos que sufra la
ordenación: hay que decir que implementa Comparable y hay que sobreescribir el método
compareTo(). Esto no ocurre con Comparator, no hace falta modificar la clase, está todo en las clases
que implementan Comparator. Este puede ser otro motivo para emplear Comparator en vez de
Comparable, si no podemos o no queremos modificar la clase que va a sufrir la ordenación.
Se podrá ver la clase de que implementa Comparator de forma anónima, sin crear la clase como
tal, escribiendo su código como parámetro de Collections.sort(), con una sintaxis más compacta:
Collections.sort(lista, new Comparator<Peli>(){
@Override
public int compare(Peli p1, Peli p2) {
return p1.año-p2.año;
}
});
Explicaremos esta estructura más adelante, en el apartado de “Clases anónimas”.
Últimas Consideraciones
Polimorfismo en Colecciones
Como vimos en temas anteriores, podemos crear instanciar una clase sobre una variable de una
superclase. Lo mismo ocurre con las interfaces y por lo tanto con las colecciones. Por ejemplo, todas
las clases de listas implementan la interfaz List. Por lo tanto, en un método que acepte como
parámetro un objeto de tipo List podremos utilizar cualquier tipo que implemente esta interfaz,
independientemente del tipo concreto del que se trate.
Es por lo tanto recomendable hacer referencia siempre a estos objetos mediante la interfaz que
implementa, y no por su clase concreta. De esta forma posteriormente podríamos cambiar la
implementación del tipo de datos sin que afecte al resto del programa. Lo único que tendremos que
cambiar es el momento en el que se instancia.
List<Peli> lista = new ArrayList<>();
//List<Peli> lista = new LinkedList<>();
lista.add(new Peli ("Episode 4: A New Hope", 1977));
• El proceso inverso lo podremos hacer también, pero con Streams y una función Lambda como
veremos al final de este manual.
• Crear una lista (interfaz List) a partir de un array con:
List<String> lista = Arrays.asList(array);
Esto es una forma rápida de ver si una lista contiene duplicados. Hacemos un conjunto con una
instrucción como la anterior y como un Set no puede tener duplicados, la comparación de size() de
las dos colecciones nos dirá si hay duplicados o no.
Elección de Colección
A la hora de elegir la colección adecuada para resolver un problema emplearemos diferentes
criterios. El primero será la estructura necesaria: admite duplicados o no, queremos que tengan un
índice, está ordenado, es de tipo par clave/valor, etc.
El siguiente gráfico puede ser de ayuda para la elección.
Con respecto al rendimiento en las distintas operaciones, este podría ser un resumen:
• Las operaciones mediante hash son muy eficientes en operaciones básicas (insertar, borrar y
buscar) sobre todo si la función de dispersión es adecuada. La iteración a través de sus
elementos es más costosa ya que no mantiene ningún enlace entre los elementos.
• Las colecciones que incorporan link penalizan un poco las operaciones básicas ya que tienen
que gestionar los enlaces entre los elementos para mantener el orden de inserción, pero
mejoran las iteraciones respecto al hash.
• Los Tree incorporan un sistema de enlaces más complejo y solo son adecuadas cuando
necesitamos ordenación.
Enumeraciones
Es un tipo de datos especial y sirve para definir un conjunto cerrado de valores constantes. Podremos
crear variables que tengan como tipo la enumeración, asignarle valores, comparar, etc.
Un ejemplo podrían ser los días de la semana, o el estado civil de una persona:
public enum DiaSemana {LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO};
public enum EstadoCivil {SOLTERTO, CASADO, DIVORCIADO, VIUDO};
Como recomendación, los valores suelen escribirse en mayúsculas. Implícitamente son public, final
y static.
DiaSemana dia1 = DiaSemana.LUNES;
if (dia1 == DiaSemana.DOMINGO) {. . . }
Pueden usarse como expresiones en los ‘case’ de un ‘switch’ pero no llevan no van
precedidos del nombre de la enumeración. Ejemplo: case LUNES: y no case
DiaSemana.LUNES.
En cuanto a la ubicación, se pueden definir como una clase:
• Como públicas en un archivo .java. Desde Netbeans: New > File > Java > Java Enum.
• Con el modificador por acceso por defecto, en el mismo archivo que otra clase.
• Como atributos de otra clase / variables globales de un programa.
El método values () devuelve un array con todos los elementos de la enumeración. El siguiente
ejemplo mostraría todos los días de la semana:
for (DiaSemana ds : DiaSemana.values()) {
System.out.println(ds);
}
El orden es importante en las enumeraciones. El método ordinal () devuelve la posición del elemento
dentro de la enumeración empezando en 0, así pue, es importante el orden el que se escriben los
elementos de la enumeración. En el primer ejemplo, DiaSemana.LUNES.ordinal() devolvería 0.
También incorpora el método final int compareTo (tipo-enum e) en el que tipo-enum es el tipo de
enumeración y e es el elemento que se compara con el elemento que invoca el método. Si la
constante de invocación tiene un valor ordinal menor que e devolverá un valor negativo, si los dos
valores ordinales son iguales, se devuelve cero y si la constante de invocación tiene un valor ordinal
mayor que e, se devuelve un valor positivo (análogo al compareTo() de otras clases).
Ejemplo: Se solicita al usuario que introduzca una cadena y, si el valor introducido coincide con un
elemento de la enumeración, crea una variable con ese elemento.
System.out.println("Introduce dia de la semana");
String dia = new Scanner (System.in).nextLine().toUpperCase();
try {
DiaSemana dia = DiaSemana.valueOf(dia);
System.out.printf("Es el día %d de la semana",dia.ordinal()+1);
}
catch (IllegalArgumentException e) {
System.out.println("Valor incorrecto");
}
Autoboxing: Es una característica que evita al programador tener que establecer correspondencias
manuales entre los tipos simples (int, double, etc) y sus correspondientes wrappers o tipos
complejos (Integer, Double, etc). Podremos utilizar un int donde se espere un objeto complejo
(Integer), y viceversa.
Por ejemplo, si tenemos un objeto Integer llamado io, la operación io1++; es válida.
Estas clases, además de servirnos para encapsular estos datos básicos en forma de objetos, nos
proporcionan una serie de constantes y métodos e información útiles para trabajar con estos datos.
Nos proporcionarán métodos por ejemplo para convertir cadenas a datos numéricos de distintos
tipos y viceversa, así como información acerca del valor mínimo y máximo que se puede representar
con cada tipo numérico.
Ejemplos:
Integer.MIN_VALUE, Integer.MAX_VALUE, Float.MIN_VALUE, Float.MAX_VALUE
Float.SIZE (tamaño en bits, no en Bytes)
Float.NEGATIVE_INFINITY,
Métodos
Estas clases disponen de diversos métodos muy útiles:
valueOf(): Para crear un objeto de tipo wrapper no se usan constructores, se recomienda que use el
método valueOf(), que es un miembro estático de todas las clases wrappers y todas las clases
numéricas admiten formas que convierten un valor numérico o una cadena en un objeto. Por
ejemplo, aquí hay dos formas compatibles con Integer:
Integer oI1 = Integer.valueOf(23);
Integer oI2 = Integer.valueOf("123"); //throws NumberFormatException si error.
Y tomando como ejemplo Integer, pero los hay análogos para todos los wrappers:
parseInt (String s): devuelve un entero obtenido a partir de la cadena ‘s’ (es estático). Acepta como
segundo parámetro la base, pudiendo así convertir de números hexadecimales a enteros.
byteValue(): devuelve el valor del entero como byte.
doubleValue() : devuelve el valor del entero como double (también floatValue(), loongValue()…).
int compareTo(int i) Compara el entero con ‘i’ devolviendo 0 si ambos son iguales, valor
negativo si el Integer que invoca al método es menor que ‘i’, y positivo si es mayor.
int compare(int num1, int num2) (estático). Compara los dos enteros pasados como parámetros
devolviendo valores análogos a compareTo()
toHexString(int i): (estático) Convierte a cadena hexadecimal el entero pasado como parámetro. Hay
también toOctalString() and toBinaryString().
Expresiones Regulares
Una expresión regular es un patrón que describe a una cadena de caracteres. Cuando usamos la
expresión *.doc para buscar archivos en el sistema operativo, estamos empleando un patrón que
representa a todos los archivos con extensión doc, en cierto modo esto sería una expresión regular
sencilla.
Una expresión regular nos servirá para buscar patrones en una cadena de texto, por ejemplo,
comprobar que una fecha tiene la estructura correcta (dos dígitos, luego un punto, después otros
dos dígitos para el mes, otro punto y cuatro dígitos para el año), también para comprobar que un
email está bien escrito (letra y números, luego una arroba, más texto, un punto final y otro texto
más), etc. Para cada uno de estos casos existe una expresión regular que los representa.
Hay que destacar que la expresión regular solo va a chequear la sintaxis, es decir el formato, pero
no va a validar la lógica semántica, por ejemplo, en una fecha no va a verificar si un 31 de abril es
correcto o no.
Uso
Las expresiones regulares se aplican a distintas tareas, aunque la más habitual será chequear si una
cadena cumple con un determinado patrón representado por la expresión regular. En Java
utilizaremos las clases Matcher y Pattern del paquete java.util.regex (incluyendo la
excepción PatternSyntaxException que se producirá si la sintaxis de la expresión no es válida).
Veamos con un ejemplo las tres formas de comprobar si una cadena cumple una expresión regular.
En este caso comprobaríamos si la cadena txt cumple con el formato especificado por regexp.
boolean resultado1, resultado2, resultado3;
String txt="12345678A";
String regexp="\\d{8}[A-Z]";
//Opción 1
resultado1 = txt.matches (regexp);
//Opción 2
resultado2 = Pattern.matches(regexp, txt);
//Opción 3
Pattern p = Pattern.compile (regexp);
Matcher m = p.matcher (txt);
resultado3 = m.matches();
En el ejemplo se está verificando si la cadena tiene 8 dígitos numéricos y a continuación una letra
mayúscula (como debería ser un DNI). En los tres casos devuelve ‘true’ si se cumple y false en otro
caso. Con cualquiera de las tres formas descritas, en caso de que se cumpla la expresión, el resultado
será true y si no se cumple será false, por eso está asignándose el resultado a variables boolean. El
último de los formatos tiene funcionalidades adicionales, que veremos en próximos apartados.
• Verificar un email: no existe una expresión regular para email que sea 100% fiable, puesto
que hay muchos formatos válidos de email y muy complejos. Aquí vamos a usar una expresión
regular más o menos sencilla: [^@]+@[^@]+\.[a-zA-Z]{2,}.
El símbolo ^ representa la negación, por lo tanto ^@ es cualquier carácter menos @. El +
indica una o más veces así:[^@]+ uno o más caracteres que no sean @.
Luego iría la @ del email y otra vez uno o más caracteres que no sean arroba. Finalmente un
punto \.
- [^@]+ cualquier carácter que no sea @ una o más veces seguido de
- \. un punto seguido de
- [a-zA-Z]{2,} dos o más letras minúsculas o mayúsculas
String emailRegexp = "[^@]+@[^@]+\\.[a-zA-Z]{2,}";
Podemos, a partir de los ejemplos anteriores, hacer un resumen de los elementos más frecuentes:
• \w una palabra (estaría delimitada por espacios en blanco, punto, coma, etc)
• \d \s un dígito, un espacio en blanco
• [abc] o ‘a’ o ‘b’ o ‘c’ (un solo carácter).
• [^abc] ni ‘a’ ni ‘b’ ni ‘c’.
• [a-g] un carácter entre ‘a’ y ‘g’.
• [A-Dh-k] un carácter entre ‘A’ y ‘D’ o bien entre ‘h’ y ‘k’.
• \. \* \\ caracteres especiales: el punto, el asterisco y la barra.
• a* a+ a? *: cero o más veces, +: una o más veces, ?: cero o una vez.
• a{5} a{5,8} a{5, } {5} cinco veces exactas, {5,8}entre 5 y 8, {5,}5 ó mas.
• ab | cd o bien ‘ab’ o bien ‘cd’
Además de estas disponemos de los paréntesis para agrupar elementos, por ejemplo:
([a-f]\*){3} representa una letra entre la ‘a’ y la ‘f’ seguida de un asterisco, todo ello tres veces,
de forma que a*a*c* cumpliría la expresión, pero ab**a* no.
Por último, a veces nos encontraremos los caracteres de comienzo ^ y final de expresión $ para
delimitarla y que coincida la expresión con la totalidad de la cadena y no solo con un subconjunto de
la misma, por ejemplo: ^([a-f]\*){3}$ aunque en los métodos que hemos empleado no tiene
relevancia ya que no responde ante subconjuntos, digamos que ambos caracteres van implícitos.
En https://docs.oracle.com/javase/10/docs/api/java/util/regex/Pattern.html disponemos de una lista
completa de los parámetros disponibles para expresiones regulares y también adjuntamos una
cheatsheet resumen al final de este apartado.
En http://regexr.com podemos hacer pruebas y practicar con distintos ejemplos de una forma
rápida e independiente del lenguaje. La página ofrece una explicación de cada paso que sigue en
la interpretación de las expresiones que escribimos.
Mostrando: Estoesunapruebadeexpresionesregulares
Genéricos
Los tipos genéricos (introducidos en Java 5) permiten forzar la seguridad de los tipos, en tiempo de
compilación y eliminan el uso de casting necesarios para utilizar las colecciones, que hasta este
momento eran de tipo Object. Permite que podamos definir una clase y la podamos instanciar de
tipos totalmente diferentes.
ArrayList, por ejemplo, está definida de forma que podemos crearla de Integer, String,
clases creadas por nosotros, etc.
ArrayList <Integer> aL1 = new ArrayList<>();
ArrayList <Alumno> aL2 = new ArrayList<>();
void verTipo() {
System.out.print("Tipo: " + dato.getClass().getName());
}
}
O también de String:
MiClase <String> m2 = new MiClase <>("Hola");
M2.verTipo();
public Caja () {
valor1 = 0;
valor2 = 0;
}
Pero qué ocurre si necesitamos que esta clase funcione con cualquier tipo de objetos: String,
LocalDate, Alumno, Coche, etc. Pues sin genéricos deberíamos crear los atributos de tipo Object y
trabajar con castings donde fuese necesario. Con genéricos simplemente debemos proporcionarle
el parámetro y trabajar sobre ese tipo ficticio:
public class CajaGenerica <T> {
private T valor1;
private T valor2;
public CajaGenerica () {
valor1 = null;
valor2 = null;
}
Los genéricos nos ofrecen también la posibilidad de limitar los tipos de los que se permite crear
instancias. Lo logra mediante el uso de herencia:
class Numerico<T extends Number> { // El argumento tiene que ser un Number o descendiente
T num;
En este ejemplo, solo podremos instanciar la clase con clases de tipo Number o descendientes de
Number, como puede ser Integer, Double, etc. pero no con otro tipo de clases.
La forma en que hemos instanciado la clase anterior nos recuerda mucho a la creación de instancias
de colecciones. Las colecciones están definidas como genéricos y por eso podemos crear ArrayList
de cualquier tipo:
public class ArrayList <T> { . . .
El problema que resuelven es que si al crear una colección (u otra clase) de un tipo determinado,
pongamos String, la instanciamos con un elemento de tipo entero, entonces producirá una
excepción. Si crease la colección de tipo Object, necesitaría castings y podrían dar error.
Según las convenciones los nombres de los parámetros de tipo usados comúnmente son los
siguientes:
• E: elemento de una colección.
• K: clave.
• N: número.
• T: tipo.
• V: valor.
• S, U, V etc: para segundos, terceros y cuartos tipos.
En el apartado de colecciones, definimos una clase que implementaba Comparable así:
class Peli implements Comparable {
public String nombre;
public int año;
@Override
public int compareTo(Object o) { //parámetro debe ser Object. Si no error.
Peli m = (Peli) o;
return this.año - m.año;
}
Como la interfaz Comparable emplea genéricos, podríamos hacerlo así, escribiendo menos código:
class Peli implements Comparable <Peli> {
public String nombre;
public int año;
@Override
public int compareTo(Peli p) { //parámetro debe ser Peli
return this.año - p.año;
}
public Peli(String n, int a) {this.nombre = n; this.año = a; }
}
Las interfaces también pueden ser genéricas. Las definiríamos como las clases, empleando el tipo
genérico en los métodos que deseemos:
interface NombreInterfaz <T> {
}
Las clases que implementen una interfaz genérica tienen dos posibilidades, ser genéricas o no.
a) Clases genéricas que implementan interfaz genérica. Ejemplo:
@Override
public void metodo2 (T op1) {. . . }
}
@Override
public void metodo2 (Integer op1) {return . . .}
}
En el caso, las instancias de la clase ya solo pueden ser del tipo determinado en la clase.
ClaseNoGenerica cNG1 = new ClaseNoGenerica();
cNG1.metodo1(3,2);
Optional
Optional es una clase genérica que “envuelve” a otra clase, y que permite gestionar los nulos de una
forma más eficaz y evitar excepciones de tipo “Null pointer exception”. Su constructor es privado,
pero podemos crear el método of () para su instanciación.
String nombre = "Daniel";
Optional<String> oNombre = Optional.of(nombre);
Tiene métodos como isPresent () o isEmpty () para comprobar si tiene contenido (sería equivalente a
preguntar si la variable subyacente es nula o no.
if (oNombre.isEmpty() { . . . }
El método orElse() devuelve un valor por defecto en caso de que la variable sea nula y get () nos
devolverá el valor:
if (nombre == null) nombre="default"
System.out.print (nombre);
Clases Anidadas
Es una clase definida dentro de otra clase. Puede ser estática o no estática, a las no estáticas las
llamamos clases internas.
Se usan como agrupamiento lógico, si una clase se usa solo dentro de otra, queda el código mejor
encapsulado. Veamos por separado las no estáticas (clases internas) y las estáticas.
Clases Internas
Ya hablamos de ellas en el apartado de los iteradores. Solo existen en el marco de una instancia de
una clase externa. Una de las características más importantes es que una instancia de una clase
anidada puede acceder a los atributos no estáticos de la clase que le envuelve.
Ejemplo:
public class Externa {
// atributos clase externa
// constructor y métodos de la clase externa
Este ejemplo de estas clases ya lo hemos visto previamente, en la implementación los iteradores.
class MiColeccion implements Iterable <Integer> {
MiColeccion() {
for (int i = 0; i < TAM; i++) arr[i] = i * 10;
}
@Override
public Iterator<Integer> iterator() {
Iterator<Integer> it = new MiIterador();
return it;
}
@Override
public boolean hasNext() {
if (sig < TAM) return true;
return false;
}
@Override
public Integer next() {
int val = arr[sig];
sig += 2;
return val;
}
}
}
Para crearlas tendríamos que emplear esta fórmula (suponiendo constructores sin parámetros):
ClaseExterna e = new ClaseExterna();
ClaseExterna.ClaseInterna i = e.new ClaseInterna();
Clases Locales
Son clases que se crean dentro de un bloque de código, en general en el cuerpo de un método. En
el siguiente ejemplo, la clase Persona tiene un método llamado validarDNI que valida que el DNI
sea correcto, esto es, tiene 8 dígitos más la letra correspondiente según una determinada fórmula.
Dicho método contiene una clase local llamada DniCorrecto. Esa clase local tiene un constructor al
que se le pasan los dígitos de un DNI y genera un DNI su letra correspondiente.
Así pues, para una persona concreta, el método validarDNI primero valida la cantidad de dígitos
del DNI de esa persona y luego, para validar la letra, crea una instancia de esa clase local con los
dígitos del DNI la persona y compara el DNI creado en la clase local (que sabemos que es correcto)
con el DNI de esa persona.
class Persona {
public String dni; public String nombre;
Clases Anónimas
Permiten definir e instanciar la clase a la vez, sin asignarle un nombre y para ser usadas una sola vez.
Podemos crearlas en el cuerpo de un método, de una clase o como argumento de un método. Esta
situación es muy frecuente, y de hecho ya las habíamos viso sin darnos cuenta.
Cuando creamos una aplicación Swing, main () contiene:
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new MainFrame().setVisible(true);
}
});
creando una instancia de una clase que implementa la interfaz Runnable y de la que redefine su
método run().
O cuando usamos swing.Timer:
timer = new Timer (100,
new java.awt.event.ActionListener () {
public void actionPerformed(java.awt.event.ActionEvent e) {
crono.Incrementar();
jTextField1.setText(crono.toString());
}
}
);
creando una instancia de una clase que implementa la interfaz ActionListener y de la que redefine
su método ActionPerformed().
El constructor new puede hacer referencia a una interfaz, y entenderemos que está creando una
clase que la implementa, o bien hacer referencia a una clase, y entenderemos que está definiendo
una clase hija. En ambos casos crea una instancia, pero la clase creada no tiene nombre. Tampoco
tendrá nunca constructor, solo llama implícitamente a super() pero no tiene constructor propio.
Otro ejemplo habitual, suele ser a la hora de ordenar una colección con un Comparator:
Collections.sort(lista, new Comparator<Peli>(){ (*)
@Override
public int compare(Peli p1, Peli p2) {
return p1.año-p2.año;
}
}
);
(*) new Comparator<Peli> está creando una instancia de una clase (sin nombre!!) que
implementa la interface Comparator (genérica). Luego abre llaves y ahí hace la
implementación de la clase, en este caso solo con el método compare().
@Override
public boolean hasNext() {
if (sig < TAM) return true;
return false;
}
@Override
public Integer next() {
int val = arr[sig]; sig += 2;
return val;
}
};
return it;
} }
Patrón Singleton
Singleton es un patrón de diseño que define una clase de la cual solamente queremos tener una
instancia. Un ejemplo de utilización podría ser un servicio del sistema operativo o la conexión a una
base de datos (solo queremos una única conexión, sobre la que luego haremos las operaciones sobre
la base de datos, pero no queremos una nueva instancia cada vez que hagamos una operación sobre
la base de datos). Para implementarla, podemos seguir los siguientes pasos:
• Definir un único constructor como privado, así no se podrán crear instancias desde el exterior.
• Obtener siempre la instancia a través de un método estático, que llama al constructor solo la
primera vez. Las veces siguientes que invoquemos a ese método devolverá esa primera
instancia creada.
Un constructor devuelve la referencia en memoria de la instancia que crea. Este método
estático “simula” el comportamiento de un constructor.
Ejemplo:
class ClaseSingleton {
private static ClaseSingleton instance = null;
Clases Inmutables
Son clases que una vez inicializadas con su constructor ya no pueden ser modificadas. Se diferencian
de las constantes (atributos final) en que las constantes se definen en tiempo de compilación y las
inmutables en tiempo de ejecución. String es un ejemplo de inmutable.
Para hacer una clase inmutable:
- Los atributos final y private
- No añadir métodos setter ni métodos que modifiquen los atributos.
- Declarar la clase como final (evitamos la herencia, que no haya una clase hija que pueda
implementar los setters.
- Los parámetros, del constructor también serán finales, así en el momento de pasarlos
también se hacen constantes y no se pueden modificar.
Ejemplo:
final class Cliente {
private final int numCliente;
private final String nombre;
private final LocalDate fechaAlta;
Este archivo lo usaremos como base para todos los ejemplos del capítulo.
Para que un documento cumpla el estándar XML tiene que cumplir las siguientes reglas:
• Debe de tener una cabecera donde se especifica la versión de XML que cumple el documento.
También suele aparecer tipo de codificación, normalmente UTF-8 o ISO-8859-1.
• El documento debe estar estructurado en forma de etiquetas de apertura <etiqueta> y
cierre </etiqueta>.
• Un documento debe de tener al menos una etiqueta de apertura y otra de cierre.
• Se pueden poner etiquetas que abran y cierren la etiqueta al mismo tiempo de la forma:
<etiqueta />. Por ejemplo: <profesor nombre="Angel" />
• El orden de las etiquetas es importante. Se deben cerrar en el orden inverso a cómo fueron
abiertas.
• Se distingue mayúsculas y minúsculas en la apertura y cierre de etiquetas.
• Una etiqueta puede llevar uno o más atributos. Por ejemplo:
<pedidos num_pedidos="10" fecha_pedido="10/01/2012">
• Todos los atributos deben ir entre comillas dobles.
• Se pueden añadir comentarios de la forma:<!-- Texto comentario -->
• Los nombres de las etiquetas y atributos no pueden tener espacios en blanco.
• No todos los “registros” tienen por qué tener la misma información, sobre el ejemplo, no todas
las recetas tienen porqué tener ingredientes o calorías.
• Podéis validar cualquier documento/página XML en el siguiente enlace:
https://validator.w3.org/
Existen diversas formas de tratar los XML en Java, son lo que se denominan “parsers”, nosotros
veremos la manera basada en DOM, la más estándar, aunque existen otras librerías como SAX o
JAXB. En esta página puedes ver el detalle de métodos disponibles para diferentes tipos de parsers:
https://www.tutorialspoint.com/java_xml/index.htm
En estos dos videos explica el proceso para SAX y JAXB.
SAX : https://www.youtube.com/watch?v=CaF8w9RjVac
JAXB: https://www.youtube.com/watch?v=3qMVLs3K0ws
Con las librerías estándar, cuando manejamos un documento XML o bien creamos uno nuevo, vamos
a tener que recorrer su estructura.
• Document: Contenedor de todos los nodos del árbol. También se conoce como la raíz del
documento, que no siempre coincide con el elemento raíz.
• Element: Cada etiqueta de apertura y cierre junto con su contenido. En un element puede
tener otros 'element' en su contenido.
• Attr: Son los atributos de un elemento y sólo pueden existir en su etiqueta de apertura.
• Text: La información (el texto) que va entre las etiquetas de apertura y cierre.
• Comment: Son los comentarios del documento.
Nosotros vamos a hacer uso de un parser sin validación que además va a ser compatible con el
modelo de objeto de documento (DOM) de XML. Usaremos las siguientes clases:
• javax.xml.parsers.DocumentBuilderFactory: instanciánolo mediante el método
newInstance().
• javax.xml.parsers.DocumentBuilder: Es la clase encargada de transformar el documento XML
a DOM, instanciándolo mediante el método newDocumentBuilder() de la clase anterior.
• org.w3c.dom.Document: Representa un documento XML pasado a DOM. Una vez que el
parser analice el documento XML creará una instancia de esta clase donde estará guardado
todo el documento en formato DOM.
En nuesto caso será <recetas>. Podemos indicar que muestre el nombre del nodo getNodeName ().
System.out.println ("Raíz: " + doc.getDocumentElement().getNodeName());
Este método devuelve un objeto de tipo Element, por lo que podemos aplicarle directamente el
método getNodeName(). Veremos a continuación que mediante otros métodos obtenemos objetos
de tipo Node, que nos obligarán a hacer castings a Element para utilizar determinados métodos.
Siguiendo con el ejemplo de las recetas arriba expuesto, NodeList contendría los dos nodos que
tienen la etiqueta nombre:
<nombre>Tarta de chocolate</nombre>
<nombre>Tiramisú</nombre>
y con getTextContent () obtenemos el texto contenido en cada una de ellas, por lo que el trozo de
código anterior mostraría:
Tarta de chocolate
Tiramisú
Lo que hemos hecho en el bucle anterior tiene un problema grave: al poner item(0).getTexTContent()
le estamos diciendo que nos muestre el texto del primer elemento que tenga esa etiqueta, sin
comprobar previamente que dicha etiqueta existe. Si no existiese se produciría una excepción.
Por ejemplo, si hubiésemos puesto:
c = element.getElementsByTagName("dificultad").item(0).getTextContent();
Si para cada receta lo que queremos es acceder a nodos que se repiten, como pueden ser los
<pasos>, deberemos incluir de nuevo un bucle adicional para cada receta. Por ejemplo:
NodeList listaRecetas = doc.getElementsByTagName("receta");
for (int i = 0 ; i < listaRecetas.getLength() ; i++) {
Element receta = (Element) listaRecetas.item(i);
String n = receta.getElementsByTagName("nombre").item(0).getTextContent();
String c = receta.getElementsByTagName("calorias").item(0).getTextContent();
System.out.printf("%s: (%s)%n", n, c);
Para navegar por todo el contenido de un nodo de tipo Element, disponemos de métodos muy útiles
que además nos ayudan a evitar las posibles excepciones de intentar acceder al texto contenido de
elementos inexistentes.
• getFirstChild (): devuelve el nodo primer hijo, puede ser Element, texto, etc. Null si no hay
hijos.
• getLastChild () devuelve el nodo último hijo, puede ser Element, texto, etc.
• getParentNode () devuelve el nodo padre.
• getPreviousSibling (): devuelve el nodo anterior (no el nodo hijo, sería “hermano”).
• getNextSibling (): devuelve el siguiente nodo “hermano”, no “hijo”. Null si no hay más.
• getChildNodes (): devuelve NodeList devuelve todos los nodos 'hijos'. Null si no hay.
• hasChildNodes (): devuelve true si el nodo tiene algún hijo. Falso en otro caso.
Si para un Node queremos saber de qué tipo es podemos invocar a su método
getNodeType() devolviéndonos un elemento enumerado: ELEMENT_NODE, TEXT_NODE,
COMMENT_NODE, etc. Este método es muy útil cuando no conocemos la estructura exacta del
documento.
Así pues, podemos usar las dos estrategias vistas para recorrer un archivo XML, o bien mediante
bucles anidados, accediendo a las etiquetas por su nombre (si conocemos la estructura de
documento y es bastante “fija”) o bien mediante los métodos que acabamos de ver, en la que
hacemos un recorrido exhaustivo (por todos los hijos, hermanos, etc.) aunque la estructura del
archivo sea muy variable.
IMPORTANTE: Al pasar los documentos XML al árbol DOM sin validar (sea con DTD o XSD) se
preservan los saltos de línea, espacios en blanco, etc. que pudiese haber en el archivo y se
convierten en nodos de tipo TEXT_NODE, con nombre #text. Esto habrá que tenerlo en cuenta en
el procesamiento cuando accedamos sin indicar el nombre de la etiqueta. Prueba este código para
comprobarlo:
node = node.getNextSibling();
}
Se podría prescindir de ellos chequeando su tipo y nombre, por ejemplo, al principio del while:
if (node.getNodeType()!=Node.TEXT_NODE ||
!node.getNodeName().equals("#text")) {
Tiene dos atributos: nombre y cantidad con valores “Cola-cao” y “250 gramos” respectivamente.
<paso orden="1">Mezclar el cola-cao con la mantequilla.</paso>
Ejemplos:
El siguiente código pasaría a mayúsculas todos los pasos de nuestras recetas:
NodeList listaPasos = doc.getElementsByTagName("paso");
for (int i = 0 ; i < listaPasos.getLength() ; i++) {
Element paso = (Element) listaPasos.item(i);
paso.setTextContent (paso.getTextContent().toUpperCase());
}
El siguiente código modifica el valor de los atributos, sustituyendo el ingrediente “azúcar” por
“sacarina” en todas las recetas:
NodeList listaIngredientes = doc.getElementsByTagName("ingrediente");
for (int i = 0 ; i < listaIngredientes.getLength() ; i++) {
Element paso = (Element) listaIngredientes.item(i);
if (paso.getAttribute("nombre").equals("azúcar"))
paso.setAttribute("nombre","sacarina");
}
Importante: Recordar que Node es una interface y por tanto no podemos crear un objeto
directamente. En el párrafo anterior hablamos de ‘Node’ pero realmente usaremos
‘Element’.
Para utilizar los métodos anteriores debemos de seguir estos pasos:
1) Crear un Element, llamando al método del document createElement (nombreEtiqueta) para crear
una etiqueta.
2) Crear un nodo de tipo texto llamando al método del document createTextNode (texto) que
añade el texto a la etiqueta creada en el paso anterior.
3) Añadir el Element del primer paso en el punto de la jerarquía que queramos, mediante los
métodos anteriores (appendChild, insertBefore) aplicados sobre un Element concreto.
El siguiente ejemplo añadiría al final de las recetas, como último nodo de elemento raíz <recetas>:
Element raiz = doc.getDocumentElement();
Element nuevoElementFabrica = doc.createElement("autor");
nuevoElementFabrica.appendChild(doc.createTextNode("Carlos Arguiñano"));
raiz.appendChild(nuevoElementFabrica);
En el ejemplo anterior elegimos raíz para insertar el nodo hijo por comodidad, pero podría ser un
elemento cualquiera, por ejemplo, un hijo de una receta concreta, o de todas la recetas…
Eliminar nodos: para eliminar nodos, tendremos que situarnos en el nodo padre y pasar 'una
referencia' al nodo hijo que queremos eliminar.
El siguiente ejemplo recorrería todas las recetas y eliminaría para todas ellas su nodo (etiqueta)
<calorias>:
NodeList lista = doc.getElementsByTagName("receta");
for ( int i = 0; i < lista.getLength(); i++) {
Element elementoPadre = (Element) lista.item(i);
Element elementoHijo = (Element)
elementoPadre.getElementsByTagName("calorias").item(0);
elementoPadre.removeChild(elementoHijo);
}
Podemos hacerlo de otra forma, recorriendo directamente los nodos calorias y obteniendo el
padre dinámicamente:
NodeList lista = doc.getElementsByTagName("calorias");
for (int i = 0; i < lista.getLength(); i++) {
Element elCalorias = (Element) lista.item(i);
elCalorias.getParentNode().removeChild(elCalorias);
i--;
}
(*) i--; hace falta para que retroceda el bucle, ya que, al eliminar el elemento
actual, la siguiente pasa a la posición actual, y al hacer i++ en el for, nos lo
saltaríamos. Quítalo para comprobar cómo no funciona correctamente.
Luego podemos añadir elementos, hijos de ese elemento raíz. El siguiente ejemplo añade un nodo
texto.
Element receta = doc.createElement("receta");
receta.appendChild(doc.createTextNode("Paella"));
raiz.appendChild(contacto);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.transform(new DOMSource(doc), new StreamResult(ficheroSalida));
Conexión - Desconexión
La conexión con la Base de Datos es la única parte de la programación con JDBC que depende
directamente del SGBD que se vaya a utilizar. No es un cambio muy grande puesto que simplemente
hay que seleccionar el drive adecuado y la cadena de conexión adecuada.
El primer paso será añadir el driver al proyecto. Este proceso difiere según el tipo de proyecto en que
nos encontremos, bien Maven, bien Ant.
Proyecto Maven:
En este caso no es necesario descargar el driver, Maven lo hará por nosotros. Lo único que debemos
hacer es incorporar esa dependencia en el archivo pom.xml. Para saber las líneas exactas a incluir
nos conectamos al repositorio de Maven: https://mvnrepository.com/, y buscamos: mysql, y elegimos
la última versión:
Proyectos Ant:
En este caso sí debemos descargar el zip con el driver MySQL. Descomprimiendo ese zip obteníamos
un archivo jar. Ya lo tenéis en el aula virtual con el nombre mysql-connector-java-5.1.49-bin.jar.
Crearemos en nuestro proyecto una nueva carpeta (New >> Folder) llamada ‘libs’, y copiaremos ese
archivo .jar en la carpeta creada. Una vez hecho eso, añadiremos esa carpeta al proyecto¸ es decir,
le diremos a Netbeans que cuando haga el build del proyecto, incluya ese jar. Para ello, en nuestro
proyecto, botón derecho sobre Libraries >> Add JAR/Folder y seleccionamos el archivo.
Establecer la conexión:
Una vez hecho esto, estemos en un tipo de proyecto u otro, en nuestros programas procederemos
a establecer la conexión. Para ello podemos usar dos clases distintas: DriverManager (más sencilla y
típica para aplicaciones pequeñas, y DataSource, más avanzado, más complejo, más habitual para
aplicaciones empresariales con mucho tráfico). Nosotros emplearemos la primera.
En esa clase, utilizaremos el método getConnection (), que necesita tres parámetros:
- Cadena de conexión incluyendo la URL y puerto del gestor de base de datos y el nombre de
la base de datos.
- Usuario y contraseña del gestor de base de datos, con permiso sobre la base de datos incluida
en el parámetro anterior.
Por sencillez, nosotros emplearemos el usuario root por defecto de MySQL que le habremos
asignado una contraseña en la instalación (ver anexo). Podríamos crear otro usuario desde una
herramienta como MySQL Workbench.
Connection conexion = null;
try {
conexion = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/bdPrueba", "root", "contraseña");
} catch (SQLException e) {
System.out.println("Código de Error:" + e.getErrorCode() +
"\nSLQState:" + e.getSQLState() +
"\nMensaje:" + e.getMessage()); }
Al igual que vimos en la gestión de ficheros, podemos utilizar desde Java 7, try with resources,
definiendo entre paréntesis la instancia de conexión. Así el sistema se encargará de hacer el close()
de la conexión automáticamente.
try ( conexion = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/bdPrueba", "root", "contraseña")) {
. . .
} catch (SQLException e) {
System.out.println("Código de Error: " + e.getErrorCode() +
"\nSLQState: " + e.getSQLState() +
"\nMensaje: " + e.getMessage()); }
Dependiendo de la versión, con la configuración por defecto de MySQL, al hacer la conexión puede
producirse el siguiente error:
Código de Error: 0 - SLQState: 01S00
Mensaje: The server time zone value 'Hora de verano romance' is unrecognized..
La primera opción es más cómoda, siempre que tengamos permisos para modificar la configuración
del servidor. La segunda opción hará que el programa no falle, independientemente del servidor
contra el que ejecutemos la conexión.
En un entorno real, los parámetros usuario y contraseña no deberían estar escritos en el programa,
deberían tomarse, por ejemplo, de un archivo. El método getConnection () está sobrecargado,
admitiendo como segundo parámetro un objeto Properties que contenga user y password¸
aportando más seguridad (Properties lo vimos en el capítulo de Ficheros).
Desconexión
Al abrir la conexión con try with resources ya no es necesario hacerla. En todo caso, sería así:
try {
conexion.close();
conexion = null;
} catch (SQLException e { e.printStackTrace();}
Operaciones
Una vez creada la base de datos, con sus tablas y resto de elementos (vistas, índices, etc.) las
operaciones típicas que realizaremos desde nuestras aplicaciones serán la lectura de esos datos, la
inserción, modificación y borrado. Estas operaciones las realizaremos mediante lenguaje SQL y
reciben las siglas de CRUD (acrónimo de "Crear, Leer, Actualizar y Borrar" en inglés: Create, Read,
Update and Delete.
Disponemos de dos clases básicas para ejecutar estas operaciones sobre nuestra base de datos, son
Statement y PreparedStatement.
Statement provee de métodos para ejecutar operaciones, recibiendo la instrucción SQL como
parámetro de tipo String. Crearemos un Statement a partir de la instancia de conexión:
Statement st = conexion.createStatement();
La inyección de código es una técnica de ataque a una base de datos que se basa en introducir
(inyectar) código malicioso en las partes dinámicas de la consulta, por ejemplo, valores solicitados
al usuario. Ejemplo: Supón que construyes esta cadena que será una consulta en tu base de datos:
String consulta = "select * from tabla where usuario = ' " + user + " ' " en la que
user es una variable que introduce el usuario, con intención de que la consulta similar a:
select * from tabla where usuario = 'Pepe'
Consultas
Para lanzar instrucciones SQL de consulta, que devuelvan datos, los pasos serán:
1.- Establecer la conexión con la base de datos, como vimos previamente.
2.- Construir la consulta SELECT sobre un String:
String sql = "SELECT nombre, precio FROM productos";
3.- Ejecutar la consulta con PreparedStatement y “recoger” los datos devueltos en un ResultSet.
PreparedStatement ps = conexion.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
Como ya comentamos, ResultSet será una especie de tabla con los resultados de la consulta. Dispone
de diversos métodos para su gestión.
• next () : pasa a la siguiente fila del resultado devolviendo false si no hay más filas. Ojo:
inicialmente no está en la primera fila, hay que hacer un primer next() para llegar a la primera
fila. Cuando no hay más filas devuelve false, por eso es típica la estructura del while anterior.
• getXXXX (n) : Son diversos métodos para obtener, de la fila actual, el dato que hay en la
columna ‘n’. La primera columna no es la 0, es la 1. Son varios estos métodos: getInt, getFloat,
getString, getDate, getBoolean, etc.
- En vez del número de columna, también se puede poner el nombre por ejemplo: rs.getFloat(“precio”);
- getDate(n) devuelve java.sql.Date, se puede convertir a LocalDate: rs.getDate(n).toLocalDate(),
• getRow () : Devuelve el número de fila actual del ResultSet.
• Otros métodos para navegación por el ResultSet. Además de next () disponemos de estos
otros métodos para desplazarnos por el conjunto de datos:
- previous (): pasa a la posición anterior a la actual.
- beforeFirst (): pasa a la posición previa a la primera del ResultSet.
- last (): pasa a la última posición del ResultSet.
- absolute (int f): pasa a la fila ‘f’ del ResultSet.
- relative (int f) : desplaza ‘f’ filas la posición actual (‘f’ puede ser positivo o negativo)
Importante: Por defecto, ResultSet es de solo lectura y se puede recorrer solo hacia adelante, sin
volver atrás, por lo que no se podría hacer un previous() o un beforeFirst(). Si hiciésemos un last()
no podríamos volver atrás. Este comportamiento se puede cambiar.
PrepareStatement ps = conexion.preparedStatement(sql,
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
Estos procesos deberán gestionar las diferentes excepciones que se puedan producir, quedando
finalmente el código completo así:
Parametrización de consultas: También se pueden parametrizar las consultas, tal y como se muestra
en el siguiente ejemplo, donde se mostrará la información de los productos de un determinado,
seleccionado en tiempo de ejecución:
float p1 = 100;
float p2 = 300;
String sql = "SELECT nombre, precio FROM productos " +
"WHERE precio > ? and precio < ?";
PreparedStatement ps = conexion.prepareStatement(sql);
ps.setFloat(1, p1);
ps.setFloat(2, p2);
ResultSet rs = ps.executeQuery();
Las interrogaciones del String con la sentencia SQL son sustituidas por los métodos setXXX, en los
que se indica el número de orden de interrogación y el valor que tomará. En este caso, ejecutaría la
consulta:
SELECT nombre, precio FROM productos WHERE precio > 100 and precio < 300;
A tener en cuenta:
• Aunque los parámetros sean textos o fechas, no tenemos que preocuparnos de las
comillas en la consulta, al sustituir la interrogación por el valor, JDBC se encarga
de ello.
• Si el parámetro es una fecha, el valor debe ser de tipo java.sql.Date, no LocalDate,
por lo que si tuviésemos una variable llamada ‘fec’ de tipo LocalDate, la llamada
debería ser:
ps.setDate(1, java.sql.Date.valueOf(fec));
Funciones agregadas: En el caso de las funciones agregadas, podremos tener en cuenta que sólo van
a devolver un valor, por lo que no será necesario preparar el código para recorrer el ResultSet.
Podremos acceder directamente a su primera fila. Ejemplo:
String sql = "SELECT count(*) FROM productos "
PrepareStatement ps = conexion.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
rs.next(); //pasa al primer registro directamente
System.out.println("Cantidad de productos: " + rs.getInt(1));
Ojo: Si dejas un espacio entre ‘count’ y ‘(*)’ MySQL produce error de sintaxis.
} catch (SQLException e) {
System.out.println("Código de Error: " + e.getErrorCode() + "\n" +
"SLQState: " + e.getSQLState() + "\n" +
"Mensaje: " + e.getMessage() + "\n");
}
ResultSet no es la estructura idónea para modificar los datos contenidos en nuestra base de datos,
siempre es preferible el método executeUpdate() de PreparedStatement pero para pequeños
proyectos puede ser cómodo. Por otra parte, estas operaciones que vamos a describir puede que
no funcionen en todos los gestores de bases de datos ni con todos los drivers. Si queremos
explotar esta técnica, disponemos de una evolución de ResultSet optimizada llamada RowSet.
Vimos previamente que un ResultSet por defecto solo se podía recorrer hacia adelante, pero también
vimos que podíamos cambiar esta condición en el momento de invocar a prepareStatement ().
Este método nos permite modificar también la propiedad de solo lectura que tiene por defecto el
ResultSet, de forma que podamos modificarlo, y por tanto actualizar la base de datos.
Así pues, la estructura de prepareStatement() sería así:
prepareStatement(String SQL, tipoResultSet, concurrenciaResultSet);
Actualizaciones: Para hacer actualizaciones, utilizaremos los métodos del ResultSet, updateXXXX (),
análogos a getXXXX() que en vez de obtener el valor en una determinada columna, la actualizarían.
Por ejemplo:
rs.updateFloat (2, 100f); actualizaría la segunda columna de la fila actual del ResultSet a 100 (debería
ser float).
Además de actualizar la columna o columnas que queramos, debemos hacer a continuación
updateRow() para hacer efectiva la actualización.
El siguiente ejemplo, subiría el precio un 10% a todos los productos de la tabla y luego los mostraría:
(*) Para que funcione, la tabla debe tener definida clave primaria e incluir el campo clave
en el ResultSet
Borrados: Para borrar simplemente, llamaremos al método deleteRow ( ) y borrará la fila actual. El
siguiente ejemplo, borraría los productos de menos de 200 euros.
while (rs.next()) {
if (rs.getFloat(2) < 200) {
System.out.println("Borrando producto: "+ rs.getString(1));
rs.deleteRow();
}
}
while (rs.next()) {
String n = rs.getString(1)+ " LowCost";
float p = rs.getFloat(2) /2f;
rs.moveToInsertRow();
rs.updateString(1, n);
rs.updateFloat (2, p);
rs.insertRow();
rs.next();
}
rs.beforeFirst();
while (rs.next()) {
System.out.println("Fila número: " + rs.getRow());
System.out.println("\t nombre: " + rs.getString(1));
System.out.println("\t precio: " + rs.getFloat(2));
}
} catch (SQLException e) {
System.out.println("Código de Error:" + e.getErrorCode() + "\n"
+ "SLQState:" + e.getSQLState() + "\n"
+ "Mensaje:" + e.getMessage() + "\n");
}
Por otra parte, en las actualizaciones y correcciones referentes al acceso a la base datos, es más fácil
localizar el código afectado, así como su testing.
La idea sería entonces tener una clase que agrupase todas estas operaciones. Supongamos que
tenemos una aplicación que gestiona una clase llamada Empleado:
public class Empleado {
private int id;
private String nombre;
private LocalDate fechaNacimiento;
private String categoria;
private float salario;
1) Primero deberíamos crear la clase y definir como vamos a establecer la conexión con la BD, una
única vez al comienzo de la aplicación o bien establecer la conexión en cada operación. Si elegimos
la primera opción, incluiríamos un método así:
public class EmpleadoRepository {
private Connection con = null;
public void conectar() throws SQLException {
String JDBC_URL = "jdbc:mysql://localhost:3306/prog";
con = DriverManager.getConnection(JDBC_URL, "usuario","contraseña");
}
De una forma más sofisticada, podemos establecer esa conexión en el constructor de la clase,
empleando el patrón Singleton visto previamente y tomando los datos de usuario y contraseña de
un fichero de tipo Properties¸ para dotar de mayor seguridad a la aplicación.
2) Ahora podríamos hacer métodos para la inserción, búsqueda, o cualquier otra funcionalidad que
precisase nuestra aplicación, estableciendo la conexión o no, dependiendo de la elección tomada en
el punto anterior:
public class EmpleadoRepository {
private String JDBC_URL = "jdbc:mysql://localhost:3306/prog";
4) Ahora ya podremos llamar a los métodos de la clase EmpleadoRepository para realizar las
operaciones CRUD sobre la base de datos: insertar, borrar, consultar, modificar…
try {
empleadoRepository.insert
(new Empleado(12, "Ana Pérez", "31/12/1999", "Analista",2000f));
} catch (SQLException e) { e.printStackTrace(); }
Decíamos que los métodos de la clase EmpleadoRepository podían lanzar las excepciones,
por lo que los programas y clases que los llamen deberán capturarlas.
5) Una última optimización que podemos incorporar a este esquema es añadir una interfaz que
defina los métodos de nuestro patrón Repository :
En la aplicación usaríamos una referencia a la interfaz instanciada con el constructor de la clase. ¿Qué
ocurre si el día de mañana cambiamos a un sistema gestor de base de datos totalmente diferente o
la foma de realizar las consultas? Simplemente crearíamos una nueva clase que implementase la
interfaz. Como nuestra aplicación usa los métodos de la interfaz, no se vería afectada (solo en la
instanciación inicial, que debería invocar al constructor de la nueva clase).
SQLite
SQLite es una librería gratuita que implementa un motor de base de datos sencillo, autocontenido,
sin necesidad de servidor alguno y ni de configuración.
Toda la información de una base de datos (tablas, índices, triggers, etc.) queda almacenada en un
solo archivo, que es fácilmente portable entre distintas plataformas, simplemente copiando un único
archivo. Como inconvenientes, cabe citar que no tiene gestión de usuarios ni privilegios y que tiene
pocos tipos de datos (dispone de tipos Integer, Real, Text y Blob pero no Boolean o Date/Time).
Todas estas características la hacen idónea en muchas situaciones: en pequeños proyectos o
proyectos con poco tratamiento de base de datos, en fase de prueba, cuando vamos a presentar un
prototipo previo a nuestros clientes, aplicaciones móviles, etc.
Para incluir una base de datos SQLite en nuestro proyecto seguiremos los siguientes pasos:
1.- Nuevo proyecto Maven (podría ser de otro tipo, pero con Maven es más sencillo).
2.- Vamos al repositorio Maven y buscamos la librería SQLite, para obtener la dependencia.
Siendo archivo.db el fichero donde está almacenado la base de datos. Puede estar en la raíz
del proyecto o en una subcarpeta. En este último caso incluiríamos la ruta en la cadena de
conexión:
"jdbc:sqlite:data/archivo.db"
Previamente deberíamos tener creado ese archivo .db con las tablas y demás elementos de
la base de datos. Podemos hacerlo con programas como SQLiteManager, DBBrowser, Navicat,
etc. y en nuestro caso será muy cómodo usar la extensión de Chrome: SQLiteManager.
5.- El restos de operaciones con la base de datos es exactamente igual a lo visto con MySQL a lo largo
de todo este tema.
Si contamos con un framework como Hibernate, esta misma operación se traduce en unas pocas
líneas de código en las que podemos trabajar directamente con el objeto Java, puesto que el
framework realiza el mapeo en función de las anotaciones que hemos implementado a la hora de
definir la clase, que le indican a éste con que tabla y campos de la misma se corresponde la clase y
sus atributos, respectivamente.
@Entity
@Table(name = "actor", catalog = "db_peliculas")
public class Actor {
private Integer id;
private String nombre;
private Date fechaNacimiento;
// Constructor/es
public Actor() { . . .}
. . .
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id")
public Integer getId() {return this.id; }
public void setId(Integer id) {this.id = id; }
@Column(name = "nombre")
public String getNombre() {return this.nombre; }
. . .
Luego, las operaciones de base de datos, la haremos a través de métodos hibernate sin ver el SQL
que hay por detrás. Ejemplo:
Session sesion = HibernateUtil.getCurrentSession();
sesion.beginTransaction();
sesion.save(unObjeto);
sesion.getTransaction().commit();
sesion.close();
Transacciones
Una transacción es un conjunto de operaciones sobre una base de datos que se deben ejecutar como
una unidad. Hay ocasiones en las que es necesario que varias operaciones sobre la base de datos
se realicen en bloque, es decir, que se ejecuten o todas o ninguna, pero no que se realicen unas sí y
otras no. Si se ejecutan parcialmente hasta que una da error, el estado de la base de datos puede
quedar inconsistente. En este caso necesitaríamos un mecanismo para devolverla a su estado
anterior, pudiendo deshacer todas las operaciones realizadas.
El objeto Connection por defecto realiza automáticamente cada operación sobre la base de datos.
Esto significa que cada vez que se ejecuta una instrucción, se refleja en la base de datos y no puede
ser deshecha. Por defecto está habilitado el modo auto-commit en la conexión.
Los siguientes métodos en la interfaz Connection son utilizados para gestionar las transacciones en
la base de datos:
void setAutoCommit(boolean valor)
void commit()
void rollback()
RowSet
Como vimos previamente, ResultSet no funciona para hacer actualizaciones con todos los gestores
de base de datos. RowSet es una evolución de ResultSet, que soluciona este problema y ofrece otras
ventajas, como las siguientes:
• ResultSet mantiene la conexión con la base de datos permanentemente, y RowSet puede ser
conectado o no.
• RowSet es Serializable, puede ser transmitido por la red y puede ser tratado como un
JavaBean, ResultSet no tiene ninguna de estas características.
Instalación
Lo primero que debemos hacer es descargar el archivo de instalación del servidor (MySQL versión
Community) desde: https://dev.mysql.com/downloads/mysql/
Elegimos “MySQL Installer for Windows” (All MySQL Products), que incluirá también MySQL
Workbench, herramienta que usaremos para conectarnos al servidor para tareas administrativas
como arrancar y parar el servidor y desde la que podremos realizar consultas a la base de datos.
Hacemos doble click sobre el instalador. En el proceso de instalación, seleccionamos el tipo de
instalación “Custom”.
Conexión al servidor
Para arrancar el servidor iremos a los servicios de Windows, buscaremos el servicio MySQL80 (o
como le hayamos llamado en la instalación) y lo iniciamos. Ahora usaremos MySQL Workbench para
todas nuestras operaciones. Lo primero que debemos hacer es seleccionar nuestro servidor para
conectarnos a él (nos pedirá la contraseña de root):
La siguiente operación a realizar es comprobar el estado del servidor: Menú superior Server > Server
Status. Podemos recibir este error:
En ese caso, nos dirigimos al Panel de Control de Windows > Región. Pestaña Administrativo. Botón
[Cambiar configuración regional del sistema…] y marcamos el check: Versión beta. Use UTF-8
Unicode. Reiniciamos el sistema y reiniciamos el servicio MySQL80.
3. Ahora podríamos crear las tablas y llenar los datos mediante el lenguaje SQL, por ejemplo: “create
table nombreTabla...” , “insert into nombreTabla values ( ”, etc. pero también lo podemos hacer
mediante un archivo que ya tiene todas esas instrucciones SQL preparadas en forma de script,
para que las ejecute, según se indica en el siguiente punto.
4. En el editor de SQL, seleccionamos el icono de abrir un script y seleccionamos el archivo .sql
proporcionado por el profesor.
5. Hacemos doble clic sobre la base de datos ‘empresa’ en el panel lateral de forma que quede en
negrita, y por tanto seleccionada como la base de datos sobre la que ejecutaremos el script y
pulsamos en el icono del “rayo” para ejecutarlo.
Parar el servidor
Para finalizar la sesión, de nuevo desde los Servicios de Windows, buscamos el servicio MySQL, botón
derecho > Detener.
Docker
Otra forma de instalar MySQL sería mediante un contenedor Docker. Puedes consultar estos videos:
Usar Docker, hay que instalar Docker Desktop:
• Instalación Docker en Windows: https://www.youtube.com/watch?v=_et7H0EQ8fY
• Instalación de MySQL: https://www.youtube.com/watch?v=kphq2TsVRIs
• Persistencia de Bases de datos en MySQL: https://www.youtube.com/watch?v=-pzptvcJNh0
Pero para llegar entender este código basado en Streams, tenemos que ver previamente lo que son
las interfaces funcionales y las funciones Lambda.
Interfaces Funcionales
Las interfaces definían métodos que suponían un “un compromiso” que debían cumplir las clases
que la implementaban. Adicionalmente podían tener métodos estáticos, métodos por defecto y
métodos privados.
Una interfaz funcional solo puede tener un método abstracto, pudiendo tener métodos por defecto,
privados o estáticos. En realidad, se podría matizar esta definición; una interfaz funcional puede
tener más de un método abstracto, pero todos menos uno deben ser sobrescritura de métodos de
la clases Object (por ejemplo: toString(), equals(), etc. ).
La anotación @FunctionalInterface no es obligatoria, pero comprueba en tiempo de complicación si
se cumplen las condiciones que comentamos. Este sería un ejemplo:
@FunctionalInterface
interface ICalculadora {
public double calcular (int a, int b);
}
Como ya sabemos de capítulos anteriores, necesitaremos crear una clase que implemente la interfaz
y por tanto los métodos definidos en ella (salvo los métodos default, estáticos o privados):
class Sumador implements ICalculadora {
@Override
public double calcular(int i, int j) { return (double) i+j;}
}
Podríamos crear más clases que implementasen esa interfaz, como un Multiplicador que
desarrollaría el código del método de forma distinta.
Finalmente, deberemos crear instancias de la definida e invocar a su método:
ICalculadora s = new Sumador(); //también: Sumador s = new Sumador();
System.out.printf("%f%n",s.calcular(3,5));
También vimos que las interfaces se podían implementar con una clase anónima, que en un solo
paso y sin crear la clase explícitamente, crea la instancia de la clase y sobrescribe el método abstracto
definido en la interfaz:
ICalculadora s = new ICalculadora () { //no creamos la clase Sumador
@Override
public double calcular(int i, int j) {
return (double) i+j;
}
};
System.out.printf("%f%n",s.calcular(3,5));
Un ejemplo de interfaz funcional es Comparator¸ vista también en capítulos anteriores. Tiene un solo
método abstracto: compare (Object o1, Object o2). Esta interfaz la usábamos para ordenar
colecciones por distintos atributos. El método Collections.sort () recibe como primer parámetro la
lista a ordenar y como segundo parámetro una instancia de una clase que implemente Comparator.
Teníamos una clase Peli:
class Peli {
public String nombre;
public int año;
Podíamos ordenar la lista por el atributo que deseásemos. Para ello creábamos una clase que
implementase Comparator y su método compare (). Este método tenía que devolver entero positivo
si el primer parámetro era mayor que el segundo según el criterio de ordenación deseado, entero
negativo si el segundo parámetro era mayor, o cero si ambos parámetros eran iguales.
Otro aspecto interesante de las interfaces (no solo de las funcionales) era que podíamos definirlas
sobre tipos genéricos, con lo que aún podemos reducir más el código, omitiendo los castings:
Consumer void accept (T t) Sirve para “consumir” los datos recibidos, por ejemplo,
mostrarlos por pantalla. Tiene el método default:
andThen() para encadenar con otro consumer.
Vamos a ver unos ejemplos. Para su uso es necesario incorporar los siguientes import:
import java.util.function.Predicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
Aunque ahora no le veamos mucha utilidad, luego, con las expresiones Lambda y la API
Stream veremos cómo el uso de estas interfaces funcionales simplifica mucho el código.
Ejemplo Predicate:
Predicate <String> cadLarga = new Predicate <>() {
@Override
public boolean test(String s) {
if (s.length() > 8) return true;
return false;
}
};
if (cadLarga.test("123456789")) System.out.println("Es larga");
else System.out.println("No Es larga");
Ejemplo Consumer:
Consumer <String> cadena = new Consumer <>() {
@Override
public void accept (String s) {
System.out.println(s);
}
};
cadena.accept("123456789");
Ejemplo Function:
Function <Integer, Long> cuadrado = new Function <>() {
@Override
public Long apply (Integer a) {return (long)a * a; }
};
System.out.println(cuadrado.apply(10));
Ejemplo Supplier:
Random random = new Random();
Supplier <Integer> aleatorio = new Supplier <> (){
Random random = new Random();
@Override
public Integer get () {
Random random = new Random();
int n = random.nextInt(10);
return n; }
};
System.out.println(aleatorio.get());
Funciones Lambda
A continuación, veremos lo que son las funciones Lambda, pero ya podemos adelantar que allá
donde haya una interfaz funcional, podremos emplear una expresión Lambda para definir su
método abstracto, de una forma más intuitiva y con menos código que con la programación
imperativa.
Las expresiones lambda son funciones anónimas, sin nombre cuya sintaxis básica se detalla a
continuación:
( parámetros ) -> { cuerpo de la función }
@FunctionalInterface
interface ICalculadora {
public double calcular (int a, int b);
}
Podríamos implementar (sobrescribir) el método calcular() mediante una Lambda. Así lo habíamos
hecho sin Lambda, con una clase anónima:
ICalculadora s = new ICalculadora () { //no creamos la clase Sumador
@Override
public double calcular(int i, int j) {
return (double) i+j;
}
};
Vemos como no es necesario hacer new ni especificar el método que sobrescribe, ya que solo hay
uno. Tampoco es necesario indicar los parámetros porque se infieren de la definición de la interfaz.
El otro ejemplo visto de interfaz funcional, de Comparator:
Collections.sort(lista, new Comparator <Peli> () {
@Override
public int compare(Peli p1, Peli p2) {
return p1.año - p2.año;
}}
);
};
Function<Integer, Long> cuadrado = a -> (long) a * a;
Referencias a métodos
Con las referencias a métodos no sólo se puede utilizar expresiones lambda para implementar la
interfaz funcional sino que se puede hacer referencia a los métodos del objeto utilizando el operador
double colon, dos puntos dobles, :: y sustituyen a una expresión lambda.
Es muy típico ver la referencia de métodos en las colecciones, en su método forEach, que recibe una
interfaz funcional representada mediante esta referencia de métodos.
No confundir con el bucle for each que llevamos utilizando todo el curso
ArrayList1.forEach(System.out::println);
También resulta muy visual la referencia al método estático comparing () de la interfaz funcional
Comparator que devuelve un comparador implementando el método compare () sobre el atributo
pasado.
class Peli {
private String nombre;
private int año;
public Peli (String n, int a) {this.nombre = n; this.año = a; }
public int getAño () {return año;}
public String getNombre () {return nombre;}
@Override
public String toString () {return nombre+" ("+año+")";}
}
API Stream
A través del API Stream podemos trabajar sobre colecciones de una manera limpia y clara, evitando
bucles y algoritmos que ralentizan los programas y hacen complejo entender su funcionalidad.
Existen 3 partes que componen un Stream que de manera general serían:
1.- Un Stream funciona a partir de una lista o colección, que también se la conoce como la fuente de
donde obtienen información. El método stream() convertirá la colección en Stream para comenzar
su tratamiento.
2.- Operaciones intermedias que actúan sobre el Stream. Estas son solo algunas:
• map: Obtiene un nuevo Stream resultado de aplicarle una función a cada elemento (en
general una expresión Lambda que implementa la interfaz funcional Function).
• filter: selecciona elementos según la expresión pasada como parámetro que será una
implementación de la interfaz funcional Predicate, generalmente mediante una expresión
Lambda.
• sorted: ordena el Stream. Lo hace por el criterio por defecto de la clase (Comparable) o con el
comparador pasado como parámetro, generalmente como expresión Lambda.
• skip (n) : elimina de Stream los ‘n’ primeros de elementos del Stream.
• limit (n) : se queda solo con los ‘n’ primeros elementos del Stream, eliminando los sobrantes.
3.- Operaciones terminales. Son la última operación que se hace con el Stream. Ejemplos:
• collect: Obtienen una colección con el resultado de los procesos previos aplicados al Stream.
• forEach: Itera sobre cada elemento de Stream (el método peek() valdría para iterar sobre el
Stream como operación intermedia, no terminal).
• reduce: Permite realizar un cálculo sobre los elementos del Stream, utilizando un operador
binario para definir la operación. Tiene muchas posibilidades, por ejemplo:
- .reduce(0, (a,b) -> a+b) acumula, sería como a+=b, empezando con a=0.
- .reduce(Integer::sum) hace lo mismo que la anterior, pero devuelve un Optional.
Esta es una clase que almacena un valor, gestionando los valores nulos. Para obtener
su valor hay que ejecutar su método get (luego lo veremos en los ejemplos)
- .reduce(Integer::min) Análogo al anterior, pero con el mínimo.
- .reduce(Integer::max) Análogo al anterior, pero con el máximo.
• count: Obtiene un long con la cantidad de elementos del Stream una vez procesado.
• sum: Obtiene la suma de los valores, previamente debemos hacer un mapToInt
(Integer::intValue)
Para entender todas estas operaciones lo mejor es ver ejemplos en funcionamiento. Partiremos en
todos los casos de una lista con los 10 primeros enteros.
List <Integer> numeros = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
O también:
numeros.stream()
.map(x->x*x)
.forEach(System.out::println);
Ejemplo 2: Obtener una nueva lista con elementos de la lista inicial elevados al cuadrado.
List cuadrados = numeros.stream()
.map(x->x*x)
.collect(Collectors.toList());
Ejemplo 3: Obtener un Set con elementos de la lista inicial que sean pares elevados al cuadrado:
Set cuadradosPares = numeros.stream()
.filter(x -> x%2==0)
.map(x->x*x)
.collect(Collectors.toSet());
O también:
int res2 = lista.stream()
.filter(x -> x%2!=0)
.mapToInt(Integer::intValue)
.sum();
System.out.println(res2);
Ejemplo 6: A partir de un ArrayList de String, hacer una lista con los que empiecen por “A”:
List <String> textos = Arrays.asList("Alfa","Bravo","Charlie","Aback");
textos.stream()
.filter(s->s.startsWith("A"))
.forEach(System.out::println);
Ejemplo 7: A partir de una lista de “Peli” mostrar los títulos de las películas posteriores al año 1998,
ordenadas alfabéticamente.
lista.stream()
.filter(x -> x.getAño() > 1998)
.map(x -> x.getNombre())
.sorted()
.forEach(System.out::println);
Ejemplo 8: A partir de una lista de “Peli” mostrar el título de la película más antigua:
Optional<Peli> var =lista.stream()
.min( (a,b)->a.getAño()-b.getAño());
System.out.println(var.get().getNombre());
El mundo de los Streams y la programación funcional es muy amplio, con muchas más
posibilidades de las vistas en este manual.
Últimas Consideraciones
Para terminar este manual deberíamos señalar que a lo largo de toda la materia y las prácticas
hemos programado de una forma más o menos intuitiva y organizada. Si bien es cierto que
comentamos ligeramente el patrón MVC y siempre procuramos separar la lógica de la presentación
(bien por consola o bien mediante interfaz gráfica Swing) no hemos seguido patrones de diseño ni
técnicas sofisticadas de programación que estábamos aprendiendo conceptos nuevos partiendo
dese cero y no era el momento apropiado.
Ahora, una vez afianzado todo este conocimiento, sí es momento de empezar a trabajar esos
aspectos de diseño que nos van a permitir que nuestro software sea más escalable, más fácil de
entender, más fácil de probar y más fácil de mantener.
Sería interesante entonces, estudiar asuntos como:
- Patrones de diseño: (ver https://refactoring.guru/es/design-patterns)
- Uso de estándares y convenciones: nomenclatura de elementos (variables, métodos,
programas, etc.), arquitectura de los proyectos, estructura y apariencia del código, etc.
- Refactorización.
- Principios SOLID: (ver https://devexperto.com/principios-solid/)
- Evitar acoplamiento de código.
- Diseñar orientado a interfaces.
En cuanto a la plataforma, el curso se ha basado en Java SE para aplicaciones de escritorio. El
siguiente paso sería dar el salto a aplicaciones de dispositivos (Java ME) y de aplicaciones de servidor
(Java EE). El desarrollo web probablemente el mercado más amplio hacia el que se dirija cualquier
desarrollador en Java. Sería interesante entonces aprender frameworks de desarrollo orientados
sobre todo al mundo web. Actualmente Spring figuran entre los más populares.
Además de lo que es la programación en sí, todo programador debe conocer tecnologías
complementarias para desarrollar las aplicaciones. Destacamos:
- Gestión de dependencias (Maven)
- Testing (JUnit, Mockito) y documentación (JavaDoc)
- Gestión de versiones (Git, Github)
- Contenedores (Docker, Kubernetes)
- Heramientas DevOps
- Planificación de proyectos (Scrum)
…y por supuesto, profundos conocimientos en seguridad y en bases de datos relacionales
y NoSQL.
Anexos
Instalación y Toma de Contacto con NetBeans
Instalación
En este curso instalaremos Netbeans en su versión 14. Como este IDE está escrito en Java,
necesitaremos el entorno de ejecución de Java aunque quisiésemos desarrollar aplicaciones en otro
lenguaje. En nuestro caso, que además vamos a programar en Java debemos instalar el JDK (Java
Development Kit). Emplearemos en este caso la versión jdk 17 de Oracle.
JDK es un conjunto de herramientas para desarrollar aplicaciones Java e incluye el entorno de
ejecución JRE y el compilador Java. JRE (Java Runtime Enviromment) es el entorno necesario para
ejecutar las aplicaciones Java e incluye entre otros la JVM (Java Virtual Machine) encargada de
interpretar el bytecode Java. Hay una versión oficial de Oracle, con restricciones de uso, y otras
versiones libres como puede ser OpenJDK.
Dispones de enlaces de descarga tanto de Netbeans como jdk desde la URL mostrada en
la primera página de este manual con su código QR.
Por lo tanto, la instalación constará de tres pasos:
1) Instalación de JDK: consiste en la descompresión del zip descargado en cualquier carpeta o bien
la ejecución del archivo, en el caso de que descargásemos un ejecutable.
2) La instalación de Netbeans consiste en la descompresión del archivo archivo .zip descargado.
En Windows podemos descomprimirlo en c:\Archivos de Programa\. Se creará una carpeta
llamada netbeans, que si queremos podemos renombrar a netbeans14 o a o que queramos.
3) Antes de la primera ejecución hay que informar a Netbeans de dónde está instalado Java. Para
ello debemos editar el archivo netbeans.conf situado en la carpeta etc, en la ruta donde esté
instalado el jdk de Java (otra opción sería configurar la variable PATH dentro de las variables de
entorno de nuetro sistema Windows para incorporar la ruta de los archivos de Java: jdk/bin).
Como último paso, podemos crear en el escritorio un icono del ejecutable situado en c:\Archivos de
Programa\netbeans\bin\ para acceder al IDE de una forma más cómoda.
Crear un programa
NetBeans permite editar un archivo independente, pero donde se saca todo el provecho al IDE es
utilizando un proyecto, es decir, una carpeta con una estructura controlada por NetBeans que puede
tener archivos de diferentes tipos relacionados que darán lugar a una aplicación completa.
Para crear un projecto nuevo: menú File> New Project indicando el tipo de proyecto, en nuestro caso
usaremos Java with Maven > Java application aunque también podríamos hacer proyectos bajo
Gradle o Ant.
En la siguiente ventana indicaremos el nombre del proyecto, dejando el resto de parámetros con su
configuración por defecto en estos primeros pasos.
Ahora en la ventana de proyectos, sobre Source Packages, en el nombre del paquete, haremos botón
derecho New > Java Main Class. La primera vez descargará e instalará los plugins necesarios.
En el menú File disponemos de las funciones para guardar nuestro proyecto, exportarlo a zip, etc.
Ejecutar el programa
Para ejecutar nuestro programa, menú Run > Run Project, o simplemente pulsando F6. Esta
operación compilará nuestro código (archivos .java) generando los bytecode (archivos .class). Si se
produce algún error nos lo comunicará y en caso de que todo esté correcto ejecutará el programa.
Funciones interesantes
Netbeans dispone de multitud de funcionalidades para hacernos más sencillo y cómodo la edición y
prueba de nuestros programas, y poco a poco las irás descubriendo. A modo de resumen vamos a
destacar las siguientes:
Menú File
• Open, Close, Save, etc…
• Import Eclipse Project
• Export e Import Project to ZIP
• Project Properties: Carpetas del proyecto, clase main, empaquetado, etc.
Menú Edit
• Buscar y reemplazar texto en todo un proyecto
• Grabación de macros
Menú View
• Editors > History: ver las distintas versiones por las que ha pasado el código solo en la sesión
actual. Se genera una versión cada vez cada vez que se guarda el archivo.
• Split: parte la ventana en dos para ver distintas partes del mismo programa/clase.
• Full Screen: Cntrl+May+ENTER, o Alt+May+ENTER.
• Barras de herramientas. Personalizar.
Menú Source
• Alt + Ins: generar código: constructor, getters y setters…
• Cntrl + P: muestra los parámetros de un método (hay que estar en los paréntesis del método)
• Cntrl + SPACE: Completa código
• Rename Cntrl+R: renombrar todas las ocurrencias de una variable, etc.
• Copy Cntrl+C: Copia una clase en otro paquete
Menu Run
• May + F6: compila y ejecuta archivo actual
• F6: compila y ejecuta proyecto
Menú Tools
• Options: Preferencias de código….
• Plugins: Nuevas funcionalidades o comportamiento.
Menú Debug
• F8 (Step Over) Ejecuta paso a paso el programa. Si hay una llamada a un método lo ejecuta
completamente y vuelve a la línea siguiente a la llamada.
• F7 (Step Into) Ejecuta paso a paso el programa. Si hay una llamada a un método, entra en el y
lo ejecuta línea a línea.
• F4 (Run to cursor) Ejecuta hasta donde tengas el cursor.
• F5 (Continue) Continua la ejecución (hasta el final o siguiente breakpoint si lo hay)
• Clic en el número de línea: pone/quita un breakpoint en esa línea
Y además:
• En la ventana de código, si tenemos dos o más códigos abiertos, podemos arrastrar unos
sobre otros para verlos en paralelo, o en vertical o en horizontal.
• Zoom in/out: con ALT+Rueda Ratón
• Cntr + May + W : Cierra todas las pestañas de código
• Cntr + May + I (o botón derecho: Fix Imports) Sobre una clase, mete import necesario.
• Alt + May + F (o botón derecho: Format code) Indenta el código
• Alt + F7: (Search usages) Busca dónde se usa una determinada clase
• Cntr + May + C: Pone/quita comentarios a las líneas seleccionadas
• May + Alt + Flecha arriba/abajo: Mover línea (o varias si seleccionadas)
• May + Cntrl + Flecha arriba/abajo: Clonar línea (o varias si seleccionadas)
• Cntr + E: Eliminar línea
• Selección rectangular (en vez de por línea)
De A: Código
O también:
String str = String.valueOf(i);
En los casos anteriores ‘int’ es extrapolable a ‘long’, ‘float’, ‘double’, etc. y por otra
parte ‘Integer’ es extrapolable a ‘Long’, ‘Float’, ‘Double’, etc.
Proyecto Lombok
Lombok es una librería que, a través de anotaciones, reduce el código común que tenemos que
codificar ahorrándonos tiempo y mejorando la legibilidad del mismo. Con esas anotaciones se
pueden generar de forma automática getters, setters, constructores, etc. y esas transformaciones en
el código se realizan en tiempo de compilación.
Tiene multitud de anotaciones, que se pueden emplear a nivel atributo, método, clase, etc. Estas
serían algunas de las más utilizadas:
• @Getter : genera getter público para el atributo. Si lo podemos antes de la clase, lo genera
para todos sus atributos. Los getters comienzan por ‘get’ salvo para atributos de tipo boolean
que comienzan por ‘is’
• @Setter: Análogo a getter, pero generando setters.
• @EqualsAndHashCode: Genera los métodos equals y hashCode de la clase. Por defecto,
ámbos métodos se basarán en todos los atributos de la clase, aunque se puede modificar
este comportamiento como veremos a continuación.
• @ToString: Genera una cadena con el nombre de la clase y con cada atributo y su valor
separado por comas.
• @NoArgsConstructor: genera el constructor por defecto.
• @RequiredArgsConstructor: genera el constructor con parámetros finales o marcados con
@NonNull
• @AllArgsConstructor: genera el constructor con parámetros para todos los atributos.
• @Data: es de los más utilizados y agrupa las anotaciones: Getter, Setter, ToString,
EqualsAndHashCode y RequiredArgsConstructor.
• @Builder: genera un método para instanciar la clase de una forma más legible que con un
constructor y desacopla dicha instanciación, de forma que, aunque en un futuro cambien los
constructores de la clase, la instanciación con builder seguirá funcionando.
Estas anotaciones y muchas otras son parametrizables, de forma que podemos adaptar su
comportamiento a nuestras necesidades. Por ejemplo, si queremos que los métodos equals y
hashCode solo tengan en cuenta un atributo para la comparación, podemos parametrizarlo así:
@EqualsAndHashCode(of="abributo")
Para emplear lombok en un proyecto Maven solo debemos buscar su dependencia en el repositorio
oficial: https://mvnrepository.com/ e incluir su dependencia en el pom.xml de nuestro proyecto.
Ahora, en la clase que deseemos, añadiremos las anotaciones que generarán el código, por ejemplo:
En este caso se generarían getters, setters, toString, el constructor con todos los parámetros, el
constructor sin parámetros, y los métodos equals y hashCode basados en el atributo nombre.
También el método builder que comentamos antes.
Si trabajamos en Netbeans, en la ventana Navigator podemos ver la firma de todos los métodos
generados.
Ahora podemos emplear todo el código generado como si lo hubiésemos escrito nosotros mismos:
Alumno a1 = new Alumno("Pepe", 13, false);
Alumno a2 = Alumno.builder()
.nombre("Pepe")
.edad(12)
.build();
a2.setGraduado(true);
if (a1.equals(a2)) System.out.println(a1.toString());
Enlaces de Interés
• Documentación oficial. Especificación API con las clases, atributos, métodos, etc.
https://docs.oracle.com/en/java/javase/14/docs/api/index.html
• JDK:
https://www.oracle.com/es/java/technologies/javase-downloads.html
• Dudas frecuentes:
https://stackoverflow.com/
• Manuales web:
https://javiergarciaescobedo.es/programacion-en-java
https://www.javatpoint.com/
https://guru99.es/java-tutorial/
https://www.discoduroderoer.es/curso-java/
• Gestor de paquetes (dependencias Maven):
https://mvnrepository.com/
• IDE Netbeans y documentación
https://apache.netbeans.org/
• Patrones De Diseño:
https://www.geeksforgeeks.org/design-patterns-set-1-introduction/
• MVC
http://aalmiray.github.io/griffon-patterns/
• Frameworks
https://curiotek.com/2017/06/16/java-que-es-spring/