Profesor: Juan Morillo Fernández

Módulo profesional: Programación

  • Código: MP0485
  • Duración: 240 horas

05.Arrays

Arrays

Arrays Unidimensionales

Introducción

Vamos a ver la primera estructura de datos del curso. Como estructura definiremos como una colección de objetos, que se agrupan o se almacenan juntos. Los arrays tienen varios nombres que podéis encontraros: vector y arreglo. Estos objetos o elementos que se agrupan tienen un índice o clave para poder acceder a ellos. Y estos objetos deben ser del mismo tipo (Clases o tipos primitivos). La forma de un array unidimensional es la de un String que habéis estado utilizando hasta ahora, pero ahora no sólo podemos guardar unos caractéres juntos sino todo tipo de objetos. Intro

Un array puede tener más de una dimensión, pero esto lo veremos más adelante. Lo que hay que tener claro que cada elemento del array es referenciado por la posición que ocupa dentro del índice. Es una indexación directa, y no hay necesidad de recorrer toda la estructura de datos. Importante: la primera posición del array es la 0.

Otro aspecto muy relavante es que los arrays son estructuras de datos estáticas, es decir debemos reservar el tamaño máximo del array (número de elementos que podemos almacenar o referenciar). Esta tamaño no podrá ser modificado, más adelante veremos estructuras de datos dinámicas.

Declaración

En la declaración de un array debemos indicar que tipo de objetos queremos almacenar en el, y se usar los corchetes: tipo [] nombre;

int []  id_productos;
String [] nombres;
Cliente [] listadosCliente;

Este paso sólamente crear la variable que va a referenciar un array.

Inicialización

Una vez que tenemos la “variable array”, tenemos que crear el array y se reserva la memoria para el. En este paso hay que indicar el tamaño que va a tener: nombre = new tipo [tamaño];

id_productos = new int [10]; // acabamos de crear un array de 10 números enteros
nombre = new String [5];

Por supuesto la declaración y la inicialización lo podéis hacer en una misma línea, pero es interesante diferenciar las dos fases:

Cliente [] listadoClientes = new Cliente [20]; // hemos creado un array unidimensional para 20 objetos cliente
float [] temp_maxs = new float [10];

Ojo!: al crear el array no creamos los objetos que van a ir dentro del array, sólo reservamos memoria para las referencias de ellos.

  • Para tipos numéricos -> 0
  • Para tipo boolean -> false
  • Para objetos -> null

Inicialización + Declaración + Creación

Java nos permite al crear un array inicializar con valores en el momento de la declaración. Esta es la forma: Ini

De esta forma no es necesario indicarle Java el tamaño del array. Ya se reserva y sea crea las posiciones necesarias en base a los valores indicados. Ejemplo:

String [] dias_semana = {"Lunes", "Martes", "Miércoles", "Viernes"};

Lectura y escritura de los elementos

Para la lectura como para asignar un valor/objeto a una posición de un array es necesario un índice que representará la posición, se usa los corchetes para ello:

dias_semana[2] = "Sabado"; // escritura
System.out.println(dias_semana[3]); //lectura

Para no cometer errores de acceso o de escritura debes siempre saber que el índice empieza en 0 y que la máxima posición de un array viene determinada por la propiedad length:

int [] lista = new int[10];
System.out.println (lista.length);

En caso de acceder a una posición del array que no existe se producirá la excepción ArrayIndexOutOfBoundsException en tiempo de ejecución.

Acceso y recorrer (bucle for-each)

Ya sabemos acceder a través de un índice a una posición concreta de un array, pero a veces necesitamos recorrer el array por completo de principio a fin.

  • Usando un for normal controlando los indices:
int [] numbers = {5, 3, 9, 2, 8};
for (int i=0; i<numbers.length; i++){
    System.out.println(numbers[i]);
}
  • Usando el for-each:
for (int n : numbers){
    System.out.println(n);
}

Fíjate bien cómo está construido este for porque ahí está la clave: int n : numbers. La variable numbers es el array y la variable n es un entero que toma el valor de un elemento del array en cada iteración. En este caso el array es de tipo int, por eso la variable n también lo es. Si el array almacenase datos de tipo float esta variable también tendría que ser de ese tipo. Siempre debe coincidir el tipo de esta variable con el tipo del array. Es un “for” más automático y cómodo de usar, pero no nos permite hacer saltos, o iteraciones diferentes -> siempre es del inicio al final salvo que uséis break o continue.

  • También podemos usar while, pero lógicamente debemos controlar el índice:
int [] numbers = {5, 3, 9, 2, 8};      
int i=0;
while (i<numbers.length){
    System.out.println(numbers[i]);
    i++;
}

Ejercicio 1

Crea un programa que cuente cuántos números pares e impares hay en un array.

Ejercicio 2

Eliminar duplicados: Crea un programa que elimine los elementos duplicados de un array. Ese array estará compuesto de números enteros. Para facilitar la tarea ordénado lo previamente.

Prueba una función estática de la clase Arrays (ya indagaremos en ellas): Arrays.sort(int [] a)

Una vez tengas “limpiado” el array puedes crear uno nuevo con el tamaño correcto con: Arrays.copyOf(array, indice)

Referencias

Cómo ya sabemos, los arrays son objetos y sus variables, contienen unicamente referencias a dichos objetos en memoria. Entonces si asignáis una variable array a otro no estamos copiando un array, estan ambas variables apuntando al mismo array. Ejemplo:

int[] ar1 = {3,7,8,9};
int[] ar2 = ar1;
ar2 [2] = 99;
for ( int i : ar1){ Syste.out.println(i);}

Ejercicio 3

Desarrolla una pequeña aplicación de agenda con unos contactos. La información de los contactos será: nombre, apellido, email y teléfono Se crean varios contactos de ejemplo internamente. Tendremos un máximo de 20 contactos, y las funcionalidades a implementar son las siguientes:

  • Mostrar todos los contactos por pantalla.
  • Búsqueda de contacto por email y actualizarlo (entrada de datos por teclado).
  • Validación del email: que tenga sólo una ‘@’

Método varargs

Un método varargs es aquel que recibe un número variable de argumentos. Fue introducido en Java 5 y es aplicable también a constructores. Es conocido también como método de argumentos variables. Se caracteriza por utilizar 3 puntos o puntos suspensivos en la declaración de los argumentos. Sintásis -> tipo de datos … nombre argumento Estos argumentos variables son un array unidimensional normal que podemos recorrer. Y el tipo de elementos del array es el tipo a la izquierda de tres puntos suspensivos:

public static int max(int... valores){
        int max = Integer.MIN_VALUE;
        for(int valor : valores) {
            if(valor > max)
                max = valor;
        }        
        return max;        
    }

Restriciones para varargs:

  • No puede haber más de 1 vargar en la cabecera del método.
  • Si hay otros argumentos en la cabecera, el varargs debe colocarse al final:
public static int max(int... valores, String msg) // ERROR
public static int max(String msg, int...valores) // CORRECTO
  • Si hay sobrecarga de metodos con varargs, Java siempre priorizará el método con los parámetros más exactos. Compruébalo!

Paso de argumentos por consola/terminal

Introducción

Vamos a ver otra forma (ya menos usada en la actualidad) de mandar datos a nuestro programa desde la ejecución de un programa. Siempre serán datos de entrada. Hoy dos alternativas, por terminal o por el IDE.

Mandar datos a traves de terminal

Para ello al ejecutar un programa en Java, con el nombre de la clase sin la extensión y a continuación los parámetro/s de entrada con espacios:

javac MyCompute.java
java MyCompute 4

Logicamente antes hay que compilar. Y en el ejemplo anterior a nuestro programa MyCompute le mandaríamos un “4”.

Usando un IDE

Usando un entorno de desarrollo como Eclipse hay que acceder sobre una clase que tenga main, “Run As”, “Run Configurations”, pestaña “Arguments”, introducimos los parámetros en “Program arguments” y ejecutamos:

IDE Eclipse

Tener cuidado en “Run Configurations” en elegir el proyecto y la clase correcta.

¿Cómo se recogen los datos de entrada?

En la cabecera del método main tenemos un arrays de Strings que se llama por defecto args:

public class HelloCommandLineArguments {
   
    public static void main( String[] args ){
       
        // Check if a command line argument exists
        if(args.length == 0)
            System.exit(0);
           
        // Display the arguments from the command line
        for(int counter = 0; counter < args.length; counter++){
            System.out.println("argument index " + counter + ": " + args[counter]);  
        }
    }

}

En el anterior ejemplo podemos comprobar si el programa recibe datos de entrada (si no hay termina) y en caso afirmativo mostrarlos por pantalla. Siempre recibiremos datos en String y se comportará args como un array unidimesional normal.

Novedad con varargs

Lo hemos visto con arrays unidimensionales en el anterior punto, nos podemos encontrar con un main de esta forma:

public class SimpleTesting {
  public static void main(String... args) {
    String val1 = args[0];
    String val2 = args[1];
    System.out.println(val1);
    System.out.println(val2);
  }
}
}

Fuente: enlace

Arrays Multidimensionales

Introdución

Estos arrays tienen la característica de tener más de una dimensión, es decir tendrán más de un índice para poder acceder a una posición del mismo. El ejemplo más sencillo será el de dos dimensiones y la forma conceptual sería el de una tabla con columnas y filas. Para Java no es más que un array de arrays. Multi

Declaración

La declaración es igual que con los arrays unidimensionales, pero con más par de corchetes “[]”, un par por cada dimensión:

tipo [][]…[] nombre_array = new tipo [cap1][cap2]…[capn];

Para crear un array de dos dimensiones de dos filas y tres columnas de “doubles”:

double [][] tabla = new double [2][3];

El acceso a una posición a partir de los dos indices: Multi2

Acceso y modificación o escritura

int[][] myNumbers = { {1, 2, 3, 4}, {5, 6, 7} };
System.out.println(myNumbers[1][2]); // Outputs 7
myNumbers[1][2] = 9;
System.out.println(myNumbers[1][2]);

Inicialización

También podemos inicializar con valores en la declaración de un array multidimensional, se usarán las llaves “{}” para agrupar los elementos de cada dimensión, y cada dimensión separado por “,”. Ejemplo:

double [][] tabla = {
    {3.6, 8.2, 12,1},
    {4.0, 23.4, 11}
};

Extra: podemos crear array irregulares y sólo tendremos que declarar el tamaño de la primera dimensión -> enlace.

Recorrer

Para recorrer array de más de una dimensión siempre habrá que programar un bucle por cada dimensión, y normalmente anidados. Se suelen utilizar for o for-each:

int[][] matriz={{1,2,3},{4,5},{6,7,8,9,10},{11}};
for(int i=0;i<matriz.length;i++){
    for(int j=0;j<matriz[i].length;j++){
        System.out.print(matriz[i][j]);
    }
}

Ejemplo de array de 3 dimensiones:

public class Array3DimensionesAleatorio {
     
    private static Scanner entrada;
 
    public static void main(String[] args) {
        System.out.print("\n");
        int array3D[][][];
        int x = pedirNumeroEntero("Introduce la primera dimensión: ");
        int y = pedirNumeroEntero("Introduce la segunda dimensión: ");
        int z = pedirNumeroEntero("Introduce la tercera dimensión: ");
        array3D = new int[x][y][z];
        inicializarArray3D(array3D);
        visualizar(array3D);
    }
     
    static void inicializarArray3D(int[][][] pArray){
        Random r = new Random();
        for(int i=0;i<pArray.length;i++){
            for(int j=0;j<pArray[i].length;j++){
                for(int k=0;k<pArray[i][j].length;k++){
                    pArray[i][j][k] = r.nextInt(11);
                }
            }
        }
    }
     
    static void visualizar(int[][][] pArray){
        int numElementos = 0, sumaElementos = 0;
        for(int i=0;i<pArray.length;i++){
            for(int j=0;j<pArray[i].length;j++){
                for(int k=0;k<pArray[i][j].length;k++){
                    System.out.print("\t" + pArray[i][j][k]);
                    numElementos++;
                    sumaElementos += pArray[i][j][k];
                }
                System.out.print("\n");
            }
            System.out.print("\n\n");
        }
        System.out.printf("La media de los elementos es %.2f", (double)sumaElementos/numElementos);
    }
     
    static int pedirNumeroEntero(String s){
        int num;
        entrada = new Scanner(System.in);
        do {
            System.out.print(s);
            num = entrada.nextInt();
            if(num<=0){
                System.out.println("La dimensión de la matriz debe ser mayor que 0.\n");
            }
        } while (num<=0);
        return num;
    }
}

Clase Arrays

La API de Java incluye la clase java.util.Arrays que ofrece una funcionalidades muy prácticas con sus métodos estáticos. Estos son los más importantes:

  • static String toString ( tipo[] array): imprime los elementos del array.
  • static void fill (tipo[] array, tipo valor): inicializa el array con el valor que se le pasa.
  • static boolean equals (tipo[] array, tipo[] arrayb): compara uno a uno los elementos de los dos arrays.
  • static void sort (tipo[] array): ordena ascendentemente el array.
  • static tipo[] copyOf (tipo[] array, int len): devuelve un nuevo array de len elementos inicializados con los valores de array.
  • hay muchos más descúbrelos!

Ejercicio 1

class Unidad4 {

  public static void main(String args[]){

      int[][] arrayDosD= new int[3][];

      arrayDosD[0]=new int[4];

      arrayDosD[1]=new int[2];

      arrayDosD[2]=new int[3];

      System.out.println("cargamos e imprimimos arrayDosD[0]. Observa que su  tamaño es 4");

      for(int j=0;j<4;j++){   //utilizamos la variable j pero podría ser i, z, ....

        arrayDosD[0][j]=0*j + 0 + j*2; //cargamos la matriz

        System.out.println("arrayDosD[0]["+ j +"]="+ arrayDosD[0][j]);

      }

 

      System.out.println("\ncargamos e imprimimos arrayDosD[1]. Observa que su  tamaño es 2");

      for(int j=0;j<2;j++){

        arrayDosD[1][j]=1*j + 1 + j*2; //cargamos la matriz

        System.out.println("arrayDosD[1]["+ j +"]="+ arrayDosD[1][j]);

      }

 

      System.out.println("\ncargamos e imprimimos arrayDosD[2]. Observa que su  tamaño es 3");

      //para ahondar en el concepto de referencia a array hago la ultima impresión con paso intermedio

      int[] x=arrayDosD[2];

      for(int j=0;j<3;j++){

        x[j]=2*j + 2 + j*2; //cargamos la matriz

        System.out.println("arrayDosD[2]["+ j +"]="+ x[j]);

      }}}

Volver a escribir el ejemplo anterior utilizando el atributo length, lo que permite utilizar de nuevo un bucle anidado en otro y hacer un código más compacto.

Ejercicio 2

int[][] matriz ={{0,2,4},{1,3,5}};

Inicializa arrayDosD con la sintaxis anterior e imprime su contenido según el siguiente gráfico: Ejercicio 2

Ejercicio 3

Si no lo hiciste ya, vuelve a escribir el ejercicio anterior de forma que utilice el “for mejorado”. Puedes simplificar la impresión a algo del estilo de

11 12 13 14

21 22

31 32 33

Ejercicio 4

Con una matriz cuadrada, introduciendo el tamaño por argumento, inicializar aleatoriamente la matriz y luego modificarla intercambiando la diagonal principal con la secundaria. Ejemplo:

Ejercicio 3

Ejercicio 5

Con una matriz cuadrada, introduciendo el tamaño por argumento, inicializar aleatoriamente la matriz y luego modificarla invirtiendo los valores de la diagonal principal entre ellos, y los de la diagonal secundaria entre ellos:

Ejercicio 2

Ejercicio 6

Como caso concreto del cuadro anterior, se pide que simules un conteo con 3 dimensiones: hora, dia y mes. Llena aleatoriamente el array y se pide:

  • Que imprimas mes a mes los valores de la matriz
  • Que muestres de cada mes el día con más coches
  • Y al final, que muestres del 3 marzo la hora que registra el mayor conteo

Para simplificar, desprecia años bisiesto y febrero siempre tiene 28 días. Para inicializar el array supon conteo entre 0 y 999

06.Orientación a Objetos avanzado

Orientación a Objetos avanzado

Modificador Static

Introducción

Inaguramos esta parte de conceptos de orientación a objetos más avanzados con la palabra reservada static. Vamos a tener:

  • Variables static.
  • Métodos static.

La idea de estas variable o métodos pertenecen a la clase en sí, no a un objeto en concreto (cómo lo visto hasta ahora). Es como si fueran una variable “global” para todos los objetos de una clase. Pero realmente forma parte de la Clase, de una manera “estática”. No de lo dinámico de los objetos, que cada objeto tiene sus atributos y métodos con sus valores, estados y momentos de ejecución.

Gráfico explicativo

Diagrama static

Variables Static

Serán atributos de la clase, también se le llama una constante de clase (pero se puede variar el contenido), o atributos comunes a todos los objetos de la clase. Todos los objetos de la misma clase pueden acceder a los datos static, pero incluso, aunque la clase no tenga objetos, estos datos existen y son utilizables a través del nombre de la clase. Como cabría esperar, los datos static no se almacenan con los objetos si no en una zona de memoria especial para dichos datos.

Utilidad clara -> llevar la cuenta de cuantos objetos se están creando:

class Cohete{
 static int numCohetes=14;
 String nombre;
 Cohete(String nombre){
 numCohetes++;
 this.nombre=nombre;
 }
}
class Unidad4 {
 public static void main(String[] args) {
 System.out.println(" total de cohetes "+Cohete.numCohetes);
 }
}

Tal como véis en el anterior ejemplo, se puede inicializar las variables tipo static. Esta inicialización se realizar al cargar la clase en memoria, no al crear objetos de la misma por primera vez. Ojo! -> las variables locales no se pueden declarar “static”.

Formas de acceder a esa variable estática

Existen múltiples formas pero la que se interpreta más “natural” es através del nombre de la clase:

Cohete.numCohetes

Pero se tiene acceso desde cualquier objeto de la clase:

Cohete c1=new Cohete("Apollo 1");
 System.out.println("nombre de cohete c1:"+c1.nombre+" y numero total de cohetes "+c1.numCohetes);
 Cohete c2=new Cohete("Apollo 2");
 System.out.println("nombre de cohete c2:"+c2.nombre+" y numero total de cohetes "+c1.numCohetes);
 Cohete c3=new Cohete("Apollo 3");
 System.out.println("nombre de cohete c3:"+c3.nombre+" y numero total de cohetes "+c1.numCohetes);
 System.out.println("numero total de cohetes con c1: "+c1.numCohetes);
 System.out.println("numero total de cohetes con c2: "+c2.numCohetes);
 System.out.println("numero total de cohetes con c3: "+c3.numCohetes);
 System.out.println("numero total de cohetes con Cohete: "+Cohete.numCohetes);

Ejercicio 1

Añade a la clase Punto la capacidad de controlar el número de objetos Punto creados

class Punto {
 int x , y ;

 Punto ( int x, int y ) {
 this.x = x ;
 this.y = y;

 }
}

Ejercicio 2

Intenta escribir un programa para representar el consumo de energía de una instalación eléctrica. Para ello, se hará una clase que representa los aparatos conectados en la instalación. Cada aparato tiene un consumo eléctrico determinado. Al encender un aparato eléctrico, el consumo de energía se incrementa en la potencia de dicho aparato. Al apagarlo, se decrementa el consumo. Inicialmente, los aparatos están todos apagados. Además se desea consultar el consumo total de la instalación.

Dicho todo ésto, haz un programa que declare dos aparatos eléctricos, una bombilla de 150 watios y una plancha de 2000 watios. El programa deberá imprimir el consumo nada más crear los objetos. Después, se enciende la bombilla y la plancha, y el programa imprime el consumo. Luego se apaga la bombilla, y se vuelve a imprimir el consumo.

Bloque estáticos

Es un bloque que podemos definir en una clase para inicializar las variables estáticas. De la misma forma cuando usamos los constructores con variable normales. Sólo se pueden usar variables estáticas dentro del bloque, y puede haber más de uno -> el orden de ejecución será el orden en el código de la clase.

class Test{
 static {
 //Codigo iria aqui
 }
}

public class Demo {
 static int a;
 static int b;
 static {
    a = 10;
    b = 20;
 }
 public static void main(String args[]) {

  System.out.println("Value of a = " + a);
  System.out.println("Value of b = " + b);
    }
}

Recordad que lo estático se ejecuta al cargar la clase en memoria no al crear objetos de ella.

Métodos Static

Es la misma idea que con variables estáticas, en este caso serán métodos globales a una clase, que desde la clase o cualquier instancia de ella (objetos) podemos ejecutar:

class Impuesto{
 static int valormax=200;
 static int valormaxDiv2(){
   return valormax/2;
 }
}
public class Unidad4 {
 public static void main(String[] args) {
   System.out.println("valormax: "+ Impuesto.valormax);
   System.out.println("valormax dividido por 2: "+ Impuesto.valormaxDiv2());
   Impuesto.valormax=6000;
   System.out.println("valormax: "+ Impuesto.valormax);
   System.out.println("valormax dividido por 2: "+ Impuesto.valormaxDiv2());
 }
}

Desde el ejemplo anterior probar a crear objetos y ejecutar el método estático desde los mismos. Aspectos importantes a tener en cuenta es que desde métodos estáticos no podemos usar this, por motivos obvios. Y desde los métodos estáticos no pueden acceder a atributos que no sean static.

Ejercicio 3

Observa el siguiente ejemplo:

class Racional{
 int numerador;
 int denominador;
 Racional(int numerador, int denominador){
   this.numerador=numerador;
   this.denominador=denominador;
 }
 void multiplicar(Racional r1, Racional r2){
   this.numerador=r1.numerador*r2.numerador;
   this.denominador=r1.denominador*r2.denominador;
 }
}
class Unidad4{
 public static void main(String[] args) {
   Racional r1=new Racional(3,4);
   Racional r2=new Racional(1,2);
   Racional r3=new Racional(1,1);
   r3.multiplicar(r1, r2);
   System.out.println("MUTIPLICACIÓN DE NÚMEROS RACIONALES");
   System.out.println("r1 vale: "+r1.numerador+"/"+r1.denominador);
   System.out.println("r2 vale: "+r2.numerador+"/"+r2.denominador);
   System.out.println("r3 vale: "+r3.numerador+"/"+r3.denominador);
 }
}

Modifica el ejemplo anterior, de forma que ahora la multiplicacion se puedan usar de la siguiente forma: r3=Racional.multiplicar(r1,r2);

Ejercicio 4

Observa el siguiente ejemplo ya hecho:

class Potencia{
 int elevar(int base,int exponente){
   int resultado=1;
   for(;exponente>0;exponente--){
   resultado=resultado*base;
   }
   return resultado;
 }
}
class Unidad4{
 public static void main(String[] args) {
   Potencia p = new Potencia();
   System.out.println(p.elevar(2,1));
   System.out.println(p.elevar(5,3));
   System.out.println(p.elevar(9,0));
 }
}

Reescribe el ejemplo anterior para que funcione el siguiente main:

public static void main(String[] args) {
 System.out.println(Unidad4.pot(2,1));
 System.out.println(pot(5,3));
 System.out.println(new Unidad4().pot(9,0));
}

Ejercicio 5

Construye una clase Complejo con dos atributos:

● real: parte real del número complejo ● imag: parte imaginaria del número complejo

Puedes consultar la estructura de una clase en el apartado correspondiente de la unidad, o bien partir de la definición de la clase Persona del apartado anterior. A continuación crea los siguientes métodos dentro de la clase:

  • public Complejo(): Constructor que inicializa los atributos a cero.
  • public Complejo(double real, double imag): Constructor que inicializa los atributos a los valores indicados por los parámetros.
  • public double getReal(): Devuelve la parte real del objeto.
  • public double getImag(): Devuelve la parte imaginaria del objeto.
  • public void setReal(double real): Asigna a la parte real del objeto el valor indicado en el parámetro real.
  • public void setImag(double imag): Asigna a la parte imaginaria del objeto el valor indicado en el parámetro imag.
  • public String toString(): Convierte a String el número complejo, mediante la concatenación de sus atributos y devuelve como resultado la cadena de texto 3 + 4i, si 3 es la parte real y 4 la parte imaginaria.
  • public void sumar(Complejo b): Suma la parte real con la parte real del número complejo b y la parte imaginaria con la parte imaginaria del número complejo b. La clase Complejo pertenecerá al paquete llamado numeros. Crea otra clase denominada DemoNum, dentro del mismo paquete, que pruebe todos los métodos de la clase Complejo.

Conclusiones

  • No se debe abusar de lo “estático” porque el paradigma O.O. se debilitaría.
  • Muchas librerías y clases para ejecutarlas es necesario ejecutar sus métodos estáticos: Math, Integer.parseInt() etc…
  • Fijaros que el main es static -> no es necesario crear una instancia de esa clase principal.
  • Los métodos static no pueden usar la referencia this.
  • Un método static no puede acceder a miembros(de su clase) que no sean static
  • Un método no static puede acceder a miembros static y no static.

Modificadores de Acceso

Introducción

Uno de los principios básicos de los lenguajes orientados a objetos es la encapsulación, mediante la cual se garantiza que los datos de una clase solo son modificados por las operaciones apropiadas implementadas en los métodos de sus clases para preservar su invariante, las reglas que define la clase y el estado consistente de su estado.

El acceso a las propiedades y métodos se determina mediante las palabras reservadas de los modificadores de acceso, en Java hay cuatro modificadores de acceso que definen ámbitos de visibilidad de más restrictivos a menos restrictivos: Java proporciona cuatro tipos de acceso a los miembros de una clase:

  • public
  • private
  • protected
  • default

Por supuesto son palabras reservadas. A continuación, te explico a detalle cada uno de ellos.

Niveles de acceso

Diagrama niveles de acceso

Diagrama niveles de acceso fuente Wikipedia

Public

Este modificador es el menos restrictivo, y no protege a los miembros de la clase. Una clase, propiedad, método, constructor o interfaz declaradas como public pueden ser accedidos desde cualquier otra clase. No existen restricciones en el acceso a los miembros de una clase. Incluso se pueden usar los miembros de una clase que está en otro paquete. Relativo a Herencia: Todos los métodos y variables públicas de una clase, son heredadas por sus subclases.

Para ejemplificar el uso de éste modificador, observa la siguiente clase, que define una variable y un método público, y que pertenece a un paquete diferente al que se encuentra el programa principal:


package otropaquete;
public class A {
    public int unaVariable;
    public void mostrarVariable(){
        System.out.println("El valor de unaVariable es: " + this.unaVariable);
    }
}

package proyectomodificadores;
import otropaquete.*;

public class ProyectoModificadores {
    public static void main(String[] args) {
       A objetoA = new A();
       objetoA.unaVariable = 10;
       objetoA.mostrarVariable();
    }
}

Aspectos a tener en cuenta: Observa cómo la clase A se encuentra en el paquete llamado otropaquete. Para que main pueda utilizar la clase A, debe importar otropaquete, cómo puedes ver: import otropaquete.*; Debido a que unaVariable está declarada con acceso público, main puede modificar directamente su valor a través del operador punto. Por directamente me refiero a que no es necesario usar un setter para asignarle un valor. El método mostrarVariable también es público, por lo que main también lo puede invocar sin problema.

Private

Los miembros y los métodos de una clase declarados de esta forma, permanecen privados para las clases externas, y sólo son accesibles dentro de la clase en la que se están declarando.

  • Todos los miembros declarados como private sólo pueden ser accedidos dentro de la clase, pero no fuera de ella.
  • Las variables que son declaradas private pueden ser accedidas fuera de la clase sólo si los métodos setter y getters son públicos. Esta regla se cumple aún con clases hijas (Herencia).
  • El modificador de acceso privado es el nivel de acceso más restringido, por lo que ni las clases, ni los constructores pueden ser private (en la mayoría de las situaciones).

En general, se aconseja utilizar private para declarar variables de una clase, y a los métodos de acceso getters y setters tipo public.

Protected

Lo veremos con Herencia, esto es un adelanto. Este modificador de acceso busca proteger a los miembros declarados de esta forma. No es el nivel de protección más alto que tiene Java. En este nivel de acceso:

  • Las variables, métodos y constructores que son declarados protected en una superclase pueden ser accedidos solo por las subclases en otro paquete o cualquier clase dentro del paquete de la clase con los miembros protegidos.
  • El acceso protegido les da a las clases hijas la oportunidad de usar el método o variable mientras que a su vez lo protege de que clases no relacionadas traten de acceder a ellos.
  • Los miembros protected de una superclase sólo están accesibles para la superclase, la subclase y otras clases del MISMO PAQUETE.
  • Este modificador de acceso no puede ser aplicado a clases e interfaces, sólo a miembros y métodos. Observa el siguiente ejemplo. La clase A es la clase padre de B, y ambas se encuentran en el paquete otropaquete.
package otropaquete;
public class A {
    public int unaVariable;
    protected int otraVariable;
    
    public void mostrarVariable(){
        System.out.println("El valor de unaVariable es: " + this.unaVariable);
    }
}

El atributo de A llamado otraVariable ha sido declarado como protected. Por esa razón, B puede utilizarlo directamente y modificar su valor como si estuviera declarado en B. Eso se realiza en un método que también tiene acceso protegido llamado modificarOtraVariable.

package otropaquete;
public class B extends A{
    
    protected void modificarOtraVariable(){
        this.otraVariable = 18;
        
    }   
}

La clase principal ProyectoModificadores está en un paquete diferente, llamado proyectomodificadores. En el método main se declaran dos objetos, uno de la clase A y otro de la clase B. El atributo unavariable tiene acceso público, eso ya lo habíamos discutido arriba. Pero el método modificarOtraVariable tiene acceso protegido, y como B está en otro paquete, no puede ser accedido, de ahí que se presente el error que debería salirte:

package proyectomodificadores;
import otropaquete.*;
public class ProyectoModificadores {
    public static void main(String[] args) {
       A objetoA = new A();
       B objetoB = new B();
       objetoA.unaVariable = 10;
       objetoA.mostrarVariable();
       objetoB.modificarOtraVariable();
    }
    
}

Default

Cuando no utilizas un modificador de acceso específico (public, private, o protected), Java le asigna uno por default a la variable o método que estés declarando. Este se conoce como el modificador de acceso por defecto o por default. Este nivel de acceso:

  • Permite a la clase y a TODAS las clases del mismo paquete acceder a los miembros y/o métodos (semejante a public)
  • Pero las clases que no estén en el mismo paquete no podrán acceder a los miembros y/o métodos.

Es un error común olvidar escribir el modificador de acceso al declarar una variable o definir un método. Pero de esa manera sin querer le estarás dando acceso a todas las clases del paquete y eso no necesariamente es bueno.

Encapsulamiento

Muchos gente interpretan que los modo de acceso se aplica a “nivel de objeto”, pero OJO, se aplican a “nivel de clase”. Quiere decir esto que si obj1 y obj2 son objetos de la misma clase, obj2 tiene acceso a los miembros private de obj1 y viceversa. Esta observación es de la máxima importancia cuando tenemos clases con métodos que referencia a objetos de su misma clase, por ejemplo:

class MiClase {
    private int conAccesoPrivado;
    public int conAccesoPublico;
    int conAccesoPaquete; //en este caso es equivalente a public
    
    void setConAccesoPrivado(int i){
        conAccesoPrivado=i;
    }
    int getConAccesoPrivado(){
        return conAccesoPrivado;
    }
    
    void imprimeOtroObjeto(MiClase o){
        System.out.println("Desde obj2 puedo acceder a los miembros publicos y privados de obj1");
        System.out.println("el atributo privado vale: " + o.conAccesoPrivado); //no da error
        System.out.println("el atributo publico: " + o.conAccesoPublico);
    }
}

class Unidad4{
 
 public static void main(String[] args) {
    MiClase obj1 = new MiClase();
    obj1.setConAccesoPrivado(5);
    obj1.conAccesoPublico=6;
    //demuestro que desde otro objeto obj2 de la misma clase que obj1 puedo acceder a lo privado de obj1
    MiClase obj2 = new MiClase();
    obj2.imprimeOtroObjeto(obj1);
 }
}

Los niveles de acceso te pueden ayudar al respecto.

  • usa el nivel más restrictivo que tenga sentido para un miembro. Usa private a no ser que tengas una buena razón para no hacerlo
  • evita campos públicos excepto para constantes ( muchos ejemplos en este tutorial usan campos públicos. Esto nos puede ayudar a ilustrar algunos puntos con concisión, pero no se recomienda para código real en explotación). Los campos públicos tienden a encadenarte a una implementación concreta y a limitar tu flexibilidad al respecto de cambios en tu código.

Extra: Modularidad

Los ámbitos de visibilidad es un mecanismo bastante limitado ni es suficiente para proporcionar encapsulación. No hay ningún impedimento a que cualquiera pueda crear una clase en un paquete que contiene clases privadas de paquete o métodos package private o heredar de esas clases y de esta manera tener acceso a clases, métodos y propiedades que el autor original no las diseñó para esos propósitos. Puede ser incluso un problema de seguridad.

La modularidad añadida en Java 9 viene a complementar y dar una solución más completa a los ámbitos de visibilidad así como garantizar mejor la encapsulación tal y como el programador del paquete original ha diseñado. Por si os interesa indagar: enlace

Ejercicio 1

Pon los atributos private y haz los cambios necesarios:

class Racional{
 int numerador;
 int denominador;
 Racional(int numerador, int denominador){
    this.numerador=numerador;
    this.denominador=denominador;
 }
 static Racional multiplicar(Racional r1, Racional r2){
    Racional resultado= new Racional(1,1);
    resultado.numerador=r1.numerador*r2.numerador;
    resultado.denominador=r1.denominador*r2.denominador;
    return resultado;
 }
}

class Unidad4{
 public static void main(String[] args) {
    Racional r1=new Racional(3,4);
    Racional r2=new Racional(1,2);
    Racional r3=new Racional(1,1);
    r3=Racional.multiplicar(r1, r2);
    System.out.println("MUTIPLICACIÓN DE NÚMEROS RACIONALES");
    System.out.println("r1 vale: "+r1.numerador+"/"+r1.denominador);
    System.out.println("r2 vale: "+r2.numerador+"/"+r2.denominador);
    System.out.println("r3 vale: "+r3.numerador+"/"+r3.denominador);
 }
}

Ejercicio 2

Pregunta 1: Diga si sale error de compilación esto

package entidades;
private class Cliente {          
           
}

package formato;
import entidades.Cliente;
public class FormatoExcel {
            Cliente c = new Cliente();
                       
}
Solución

Pregunta 2: Diga si sale error de compilación

package entidades;
class Cliente {           
           
}

package formato;
import entidades.Cliente;
public class FormatoExcel {
                                  
}
Solución

Pregunta 3: Diga si sale error de compilación

package entidades;
class Cliente {           
           
}

package entidades;
public class FormatoExcel {
                        Cliente c = new Cliente();    
}
Solución

Pregunta 4: Diga si sale error de compilación

package entidades;
public class Cliente {
            int demo;
}

package entidades;
public class FormatoExcel {
            Cliente c = new Cliente();    
            public void método(){
                        c.demo = 0;
            }
                                  
}
Solución

Pregunta 5: Diga si sale error de compilación

package entidades;
class Cliente {           
            public int demo;
}

package formato;
import entidades.Cliente;

public class FormatoExcel {
            Cliente c = new Cliente();    
            public void método(){
                        c.demo = 0;
            }
                                  
}
Solución

Herencia

Introducción

Es uno de los pilares en los que se basa la orientación a objetos. Con la cual podremos crear una relación jerárquica entre clases. Es decir habrá clases “padre” o “base” y clases “hijos” o “derivadas”. Esto implica que habrá unas clases más genéricas y otras más específicas a partir de esas generales. ¿Cuando usaremos Herencia? Sobre todo hay dos enfoques:

  • Cuando veamos que una clase es muy grande y que engloba muchas situaciones o diferentes estados en los objetos derivados, donde unos atributos o métodos se usen o no. Es muy probable que necesitemos clases hijas a partir de la clase padre. Estas clases hijas harán de especialización de la base. Supertipo -> Subtipos
  • Si tenemos varias clases que se parecen en sí, y compartan métodos o atributos, implicará poder crear una clase padre de ellas. Esta clase padre unirá lo que tienen en común las clases hija. Subtipos -> Supertipo

La herencia nos permite crear jerarquías de clases en las que, a medida que descendemos por la misma se va refinando sus comportamientos (mayor especialización).

Por otro lado nos permite crear clases genéricas que defines rasgos comunes para una serie de clases.

La herencia nos permitirá crear arquitecturas O.O. de mayor calidad.

Implementación

Palabras reservadas: extends, super.

Para indicar que una clase hereda de otra , añadiremos la palabra reservada extends en su declaración con el nombre de la superclase. Ejemplo sencillo donde consideramos que la clase Alumno va a heredar de Persona:

class Persona { 
    String nombre; 
    int edad; 
   
    public void imprimePersona() {System.out.println("Datos personales: " + nombre + ", "+ edad ); 
    } 
} 
class Alumno extends Persona{ 
    char grupo; 
}
class Unidad4 {
    public static void main(String[] args) {
        Persona p1 = new Persona();
        p1.nombre="Elías";
        p1.edad=5;
        p1.imprimePersona();
        
        Alumno a1= new Alumno();
        a1.nombre="Román";
        a1.edad=3;
        a1.grupo='a';
        a1.imprimePersona();
    }
}

¿Qué se hereda? La subclase hereda todos los atributos y métodos de la superclase.

Consideraciones

  • Atributos static también se heredan, a los static de una superclase se hereda el acceso ya que del miembro en sí no se hace una copia en la memoria del objeto.
  • Una supeclase puede tener muchas subclases (las que queramos).
  • Pero ojo! No hay herencia múltiple Java. Es decir una clase no puede tener dos padres. Sólo puede tener una superclase (exceptuando Java.lang.Object que por defecto todas las clases Java derivan de ello). Para solventar esa posible necesidad podemos usar Interface que veremos más adelante.
  • Una subclase no puede acceder a un miembro private de su superclase.
  • Cada vez que invoquemos un método, siempre se busca si está implementado o sobreescrito en la clase. Si no, se busca en el padre, y así sucesivamente hacia arriba hasta encontrarlo.

Constructores y super

En una jerarquía tanto las subclases como las superclases pueden tener sus propios constructores. En general, el constructor de la subclase invocará al constructor de la superclase, y posteriormente se encargará del resto de sus atributos en su propio constructor. La llamada al constructor “padre” se ejecuta antes que cualquier otra instrucción. Hay en general dos situaciones:

  • Sino existe un constructor en la clase padre se ejecutará el constructor por defecto.
  • Si hay varios constructores con parámetros en la clase padre, ¿cual se ejecutará?: Existe la instrucción super(lista_parametros): que nos permite ejecutar el constructor de la clase padre que queramos (en base a los parámetros…como siempre). Lo que tenemos que acordarnos es que sea la primera instrucción dentro del constructor de la clase hijo. Con los constructores se realiza una llamada en cadena. Ejemplo para que probeis herencia y el super:
public class Animal {

    private int edad;
    private String nombre;

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return super.toString();
    }

    public int getEdad() {
        return edad;
    }
    public String getNombre() {
        return nombre;
    }
    public void setEdad(int edad) {
        this.edad = edad;
    }
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }


    public Animal () {
        System.out.println("Constructor de Animal");
    }

    public Animal (String nombre, int edad) {
        System.out.println("Constructor de Animal");
        this.setEdad(edad);
        this.setNombre(nombre);
    }
}

public class Perro extends Animal{

    public String habla() {

        return "guau!!";
    }

    public Perro () {
        System.out.println("Constructor de Perro");

    }
}
public class Gato extends Animal{

    String habla() {
        return "miauu!";
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "Gato:" + this.getNombre() + ":" + this.getEdad();
    }

    public Gato () {
        System.out.println("Constructor de Gato");
    }

    public Gato (String nombre, int edad) {
        super(nombre, edad);
        System.out.println("Constructor de Gato");
    }
}

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

        Animal animal = new Animal();
        System.out.println(animal);
        animal.setNombre("Dude");
        animal.setEdad(3);
        System.out.println(animal);
        Gato yin = new Gato();
        yin.setEdad(2);
        yin.setNombre("YinYang");
        System.out.println(yin);
        System.out.println(yin.habla());

        Perro blob = new Perro();
        blob.setEdad(5);
        blob.setNombre("Blob");
        System.out.println(blob);
        System.out.println(blob.habla());
    }
}

Modificador de acceso protected

No le doy mucha importancia a este modificador, pero con herencia es cuando implica ciertas limitaciones. Os dejo un enlace donde se explican las diferentes situaciones: enlace

Ejercicio 1

Un ordenador se caracteriza por su procesador(un String) y memoria RAM(un int). Los ordenadores pueden ser de sobremesa o portátiles. Para los ordenadores de sobremesa interesa su tipo de caja y para los portátiles su peso. Crea una jerarquía en java para la situación anterior y desde un main() crea un ordenador de sobremesa con la CPU y RAM que tu elijas y con caja de tipo “micro-atx”, lo mismo para un portátil de peso 1.5kg..

Ejercicio 2

Una persona se caracteriza por su dni, nombre y dirección. Queremos trabajar con dos tipos de personas: empleados y clientes. Los empleados son personas que además tienen un sueldo, los clientes son personas que además tienen una deuda. Crea una jerarquía en java para la situación anterior y desde un main() crea un empleado, un cliente y una persona genérica que no es ni empleado ni cliente. Imprime por pantalla los atributos de los objetos creados.

Ejercicio 3

Clases: Principal, Figura, Cuadrado, Circulo. Todas las clases en paquete por defecto.

Figura es superclase de las subclases Cuadrado y Circulo

la clase Figura:

  • atributo color(String) la clase Cuadrado:

  • atributo lado(double) la clase Circulo

  • atributo radio(double)

Todos los atributos son de acceso privado. Utiliza los set/get estrictamente necesarios. Los constructores permiten crear Cuadrados y Circulos indicando su color e indicando la longitud del lado (caso cuadrado) o la longitud del radio (caso Circulo).

En main() de clase Principal:

class Principal{
public static void main(String[] args) {
    Cuadrado miCuadrado=new Cuadrado(2.5,"azul");
    System.out.println("Lado de miCuadrado: "+ miCuadrado.getLado());
    Circulo miCirculo=new Circulo(3.6,"blanco");
    System.out.println("Color de miCirculo: "+ miCirculo.getColor());
}
}

Ejercicio 4

Escribe la siguiente jerarquía:

  • superclase Persona y subclases: Hombre y Mujer
  • la clase Persona: atributo edad
  • la clase Hombre: atributo boolean hizoMili
  • la clase Mujer: atributo boolean fueMadre

La jerarquía anterior debe de pertenecer al paquete personas

La clase Unidad4 pertenece al paquete por defecto Todos los atributos son de acceso privado.

Los constructores permiten crear Hombres y mujeres indicando su edad e indicando si hizo la mili (caso hombres) o si fue madre (caso mujeres). Si la edad que se indica en el constructor es mayor de 65, el programa inicia este atributo con el valor 65. Utiliza los set/get estrictamente necesarios. El main() debe ser obligatoriamente el siguiente:


public static void main(String[] args) {
    Hombre carmelo=new Hombre(85,true);
    Mujer telma=new Mujer(21,false);
    System.out.println("Edad Carmelo: "+ carmelo.getEdad() +" Hizo mili Carmelo: "+ carmelo.isHizoMili());
    System.out.println("Edad Telma: "+ telma.getEdad() +" Fue madre Telma: "+ telma.isFueMadre());
}

Y genera:

Edad Carmelo: 65 Hizo mili Carmelo: true

Edad Telma: 21 Fue madre Telma: false

Sobreescritura

Introducción

Vamos a ver un concepto muy ligado a Herencia y es la sobreescritura de métodos. Esto nos ayudará a crear arquitecturas más complejas y aprovechar la herencia de manera más optima. La sobreescritura de métodos constituye la base de uno de los conceptos más potentes de Java: la selección dinámica de métodos, que es un mecanismo mediante el cual la llamada a un método sobrescrito se resuelve durante el tiempo de ejecución y no en el de compilación.

Definición

Se produce cuando en una jerarquía de clase, un método escrito en una subclase tiene el mismo tipo de retorno y la misma firma que un método de la superclase.

Firma: la firma de un método es el nombre del método junto a los valores de entrada (número, orden y tipo).

En una jerarquía de clases, cuando un método de una subclase tiene el mismo nombre y tipo que un método de su superclase, se dice que el método de la subclase sobreescribe el método de la superclase. Cuando se llama a un método sobrescrito desde una subclase, ésta siempre se refiere a la versión del método definida en la subclase.

El ejemlo más sencillos donde habéis utilizado la sobreescritura es con toString, ya que usamos métodos de la clase suprema Object. Pero hay más como equals(), hashCode() etc…

También lo podéis ver la sobreescritura cuando aparece la etiqueta @override.

Se llama ligadura dinámica al uso de la herencia y la sobreescritura, aspecto que veremos más adelante con Polimorfismo.

Ejemplo:

class A {
    int i, j;
    A(int a, int b) {
        y = a;
        j = b;
    }

    // Se imprimen i y j.

    void show() {
        System.out.println("i y j:" + y + "" + j);
    }
}

class B extends A {
    int k;
    B (int a, int b, int c) {
        super (a, b);
        k = c;
    }

    // Se imprime k sobreescribiendo el método () en A.

    void show() {
        System.out.println("k:" + k);
    }
}

class Override {
    public static void main (String args[]) {
        B subOb = new B (1, 2, 3);
        subOb.show(); // llamada a show () en B.
    }
}

La sobreescritura de métodos sólo se da cuando los nombres y los tipos de dos métodos son idénticos, sino lo son, entonces los métodos simplemente están sobrecargados.

Sobrecarga

Se parecen los dos conceptos pero no es lo mismo. La sobrecarga es: generar una nueva versión de un método(o constructor). La nueva versión debe de tener una firma diferente a las existentes. Se tiene en cuenta la firma del método pero no lo que devuelve el método.

Diferencia entre Sobrecarga y Sobreescritura

public class Main{
	public static void main(String[] args) {
		Hijo.sobreEscritura("Se bienvenido ", "Pablo");
		Hijo.sobreCarga( 3 );
	}
}
class Papa{
	public static void sobreEscritura(String txtBienvenida, String nombre){
		System.out.println(txtBienvenida + nombre);
	}
	public static void sobreCarga(String txtDespedida, String nombre){
		System.out.println(txtDespedida + nombre);
	}
}
class Hijo extends Papa {
	//El tipo devuelto no debe cambiar.
	//los parámetros de entrada no deben cambiar
	//La accesibilidad no será más restrictiva que la del método original.
	//Si el método original es static, el método que hace el override, tambień debe serlo.
	public static void sobreEscritura(String txtBienvenida, String nombre){
		System.out.println(txtBienvenida + "......" + nombre);
	}
	//El tipo de dato devuelto puede cambiar.
	//Los parámetros de entrada deben cambiar.
	//La accesibilidad puede ser más restrictiva que la del método original.
	protected static int sobreCarga(int codigoDespedida){
		System.out.println(codigoDespedida);
		return 4;
	}
	
}

Uso del Super

Como ya vimos en anteriores ejemplos, se puede usar super para invocar miembros de la superclase “ocultados”. Si lo que queremos es que mostrar() imprima todos los atributos de B incluyendo los heredados, podemos hacer:

class A{
 int i,j;
 A(int a, int b){
    i=a;
    j=b;
 }
 void mostrar(){
    System.out.println("i y j son: "+ i +" y "+ j);
 }
}

class B extends A{
 int k;
 B(int a, int b, int c){
    super(a,b);
    k=c;
 }
  void mostrar(){
    super.mostrar();
    System.out.println("k es: "+k);
 }
}

class Unidad4{
 public static void main(String[] args) {
    B obj_b=new B(1,2,3);
    obj_b.mostrar();
 }
}

Ejercicio 1

Crea un programa con una superclase Figura que almacena las dimensiones (2). También define un método area que calcula el área del objeto (versión no definida). El programa deriva dos subclases de Figura, Rectángulo y Triángulo. Cada una de las subclases sobrescribe area para devolver el área del rectángulo y del triángulo.

class Figura {
    double dim1;
    double dim2;
}

Ejercicio 2

Distinguir entre composición o herencia

  • Clase Persona y clase Empleado
    Solución
  • Clase Persona y clase Domicilio
    Solución
  • Clase Lista y clase Nodo de lista
    Solución
  • Clase Empresa, Empleado, y Jefe de grupo
    Solución

Ejercicio 3

Necesitamos gestionar un hotel para perros. El capacidad del hotel es de 10 perros. Perso siempre necesitaremos conocer el número de perros registrados. Al registrar un perro le pedimos nombre, peso y color.
También necesitamos alimentarlos, todos a la vez. Al alimentarlos se les da 500 gr. de pienso, y engordan medio kilo.
Al sacarlos al patio media hora, adelgazan medio kilo. Debemos tener siempre actualizado el peso de los perros.

Pasado un tiempo&hellip;

Algunos clientes se han quejado, de que a todos los perros les damos la misma cantidad de comida. Necesitaremos que algunos perros se le suministre una cantidad específica. Y va a depender del peso que tengan.

Crea funcionalidades básicas, como mostrar todos los perros, número de perros en el hotel, alimentar, sacarlos, registrar perros etc…

Object y Final

La clase Object

Java define una clase especial o clase suprema denominada Object, que es la superclase de cualquier otra clase que se pueda crear en tu proyecto. Implica que cualquier clase creada heredará una serie de métodos que algunos nos interesa sobreescribir. Otra implicación es que una variable de tipo Object podría referenciar a un objeto de cualquier clase.

Algunos métodos definidos por Object y que heredará cualquier clase son: Object Todos pueden ser sobreescritos excepto getClass() que es final (lo vemos a continuación).

Método HashCode

Devuelve un identificador o “dni” de cada objeto único. Se puede sobreescribir pero no es recomendable.

Métodos Equals

Se utiliza la sobreescritura para comparar contenido interno de los objetos. Recordad que con el operador == se comparan referencias a objetos.

Ejemplo del uso de ambos métodos:

public class Student {

    private int id;
    private String name;

    public Student(int id, String name) {
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");

        System.out.println("alex1 hashcode = " + alex1.hashCode());
        System.out.println("alex2 hashcode = " + alex2.hashCode());
        System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
    }
}

Tienen diferente código o id porque porque son dos instancias diferentes de la clase, aunque el contenido de ambos objetos es el mismo. Es decir equals sin sobreescribir usa el hashCode(). Por tanto es necesario sobreescribir el equals:

@Override
public boolean equals(Object obj) {
    if (obj == null) return false;
    if (!(obj instanceof Student))
        return false;
    if (obj == this)
        return true;
    return this.getId() == ((Student) obj).getId();
}

El operador instanceof lo trabajaremos más adelante, un enlace de referencia. Tanto hashCode() como equals() se usaran mucho con colecciones de objetos.

Ejemplo de sobreescritura con equals()

class Punto2D{
 private int x;
 private int y;
 private String nombre;
  
 public Punto2D(int x, int y, String nombre) {
  super();
  this.x = x;
  this.y = y;
  this.nombre=nombre;
 }
 @Override
 public boolean equals(Object o) { 
  if (!(o instanceof Punto2D)) return false;
    Punto2D punto = (Punto2D) o;
    return (x == punto.x) && (y == punto.y);
 }
}

El modificador final

El modificador final lo hemos usado para crear constantes de una clase, es decir una vez creada e inicializada no se puede modificar:

private final float PI = 3.1416f;

Pero ahora habiendo usado herencia lo podemos usar con métodos que no queramos que se sobreescriba. Es decir desde la clase padre podemos indicar con final, que métodos no queremos que se modifiquen en los hijos o subclases. Es una manera de “protección” funcional en herencia. Para declarar un método final seguimos esta sintaxis: modificador_acceso final tipo nombreMetodo(listaParam)

Ejercicio 1

En un ejemplo de herencia prueba el modificador final, y provoca el error de intentar sobreescribir un método del padre.

Final con Clases

Este modificador también lo podemos usar con clases, dichas clases no podrán ser heredadas. También implica que sus métodos serán final y no podrá ser declarada abstracta (abstract), aspecto que veremos a continuación.

Ejercicio 2

Hacer una clase llamada Deposito que tiene un nombre, un largo, un ancho y un alto. Hacer un método equals que devolverá true cuando dos depósitos tengan el mismo volumen (largo * ancho * alto).

Clases Abstractas

Introducción

Palabra reservada: abstract

En Java, una clase abstracta es una clase que no se puede instanciar directamente, lo que significa que no puedes crear objetos directos de una clase abstracta. En su lugar, se utilizan como clases base para otras clases derivadas, a menudo llamadas subclases o hijas. Las clases abstractas son una parte fundamental de la programación orientada a objetos en Java y se utilizan para definir una estructura común y compartir comportamientos entre clases relacionadas.

Definición

Para declarar una clase como abstracta, debes usar la palabra clave abstract en la definición de la clase. Por ejemplo:

abstract class Figura {
    // ...
}

Una vez que creamos una clase abstracta, mediante herencia la podemos usar a modo “plantilla” para las subclases:

class Circulo extends Figura {
    private double radio;

    public Circulo(double radio) {
        this.radio = radio;
    }

    @Override
    double calcularArea() {
        return Math.PI * radio * radio;
    }
}

Métodos abstractos

Una clase abstracta puede contener métodos abstractos. Un método abstracto es un método que se declara en la clase abstracta pero no se implementa en la misma. Los métodos abstractos deben ser implementados en las subclases. Y ójo -> si creamos un método abstracto la clase también tiene que ser. Pero una clase abstracta no implica que todos sus métodos sean abstractos.

Ejemplo

Suponemos que no existen los “empleados genéricos”, solo pueden ser Fijos, temporales y comerciales, por tanto, no tiene sentido calcular la nómina de un empleado genérico. Pero obligamos a todas las subclases a que lo implementen:

class Empleado{
    String nombre;
    int ant;
    int sueldoBase=1000;
    public Empleado(String nombre, int ant) {
        this.nombre = nombre;
        this.ant = ant;
    }
}

class EmpleadoFijo extends Empleado{
 public EmpleadoFijo(String nombre, int ant) {
    super(nombre, ant);
 }
 int calcularNomina() {
    return sueldoBase+100*ant;
 }
}

class EmpleadoTemporal extends Empleado{
 public EmpleadoTemporal(String nombre, int ant) {
    super(nombre, ant);
 }
 int calcularNomina() {
    return sueldoBase+50*ant;
 }
}

class EmpleadoComercial extends Empleado{
 int comision;
 public EmpleadoComercial(int comision, String nombre, int ant) {
    super(nombre, ant);
    this.comision = comision;
 }
 int calcularNomina() {
    return sueldoBase+comision;
 }
}

public class Unidad4 {
 public static void main(String[] args) {
    EmpleadoFijo eFijo=new EmpleadoFijo("fijo",10);
    System.out.println("Nomina de "+ eFijo.nombre+": "+eFijo.calcularNomina());
    EmpleadoTemporal eTemporal=new EmpleadoTemporal("temporal",5);
    System.out.println("Nomina de "+ eTemporal.nombre+": "+eTemporal.calcularNomina());
    EmpleadoComercial eComercial=new EmpleadoComercial(30,"comercial",5);
    System.out.println("Nomina de "+ eComercial.nombre+": "+eComercial.calcularNomina());
}
}

Motivo

Las clases abstractas son útiles cuando deseas definir una estructura común para un grupo de clases relacionadas o cuando deseas asegurarte de que ciertos métodos se implementen en las subclases. No puedes crear objetos directos de una clase abstracta, pero puedes crear instancias de las subclases. Es un elemento clave para el polimorfismo (un pilar de la O.O.) que veramos más adelante.

Otra justificación de una clase sin objetos es esta:

  • A veces queremos que el padre (superclase) garantice que todos sus hijos se vean obligados a implementar los métodos del padre -> herencia obligatoria.
  • Que un diseñador de la jerarquía (un analista o un jefe de programadores por ejemplo) imponga reglas a los programadores que la desarrollan.
  • Si es una clase muy genérica y padre y no queremos que se crean objetos de ella, se configura abstracta.

Ejercicio 1

Escribe un ejemplo cualquiera, lo más simple posible, en el que se aprecie que una clase abstracta, aunque no tenga sentido, no tiene porqué contener un método abstracto. Es decir, no hay error de compilación al escribir una clase abstracta sin método abstracto.

Ejercicio 2

De la misma forma, demuestra ahora que si una clase contiene un método abstracto, dicha clase debe declararse abstracta, en caso contrario, obtenemos error de compilación.

Ejercicio 3

De la misma forma, demuestra ahora que una clase abstracta no es instanciable.

Ejercicio 4

¿Que falta en el siguiente código?

abstract class Item {
    protected String titulo;
    protected float precio = 5.0f;
    public abstract boolean esAlquilable();
    public float getPrecio() {
        return precio;
    }
}
class Pelicula extends Item {
 public boolean esAlquilable() {
    return true;
 }
}
class Libro extends Item {
 public float getPrecio() {
    return 0.0f;
 }
}
class TestAbstract {
    public static void main (String[] args) {
        Pelicula pelicula = new Pelicula();
        Libro libro = new Libro();
        System.out.println(pelicula.esAlquilable());
        System.out.println(libro.getPrecio());
 }
}

Ejercicio 5

Escribe código para implantar la siguiente estructura. Luego en main de Unidad4 crea un triángulo y un círculo y calcula su área. Sabemos que una clase/método es abstracta por que su nombre está en cursiva. Las flechas indican una relación de herencia “un triángulo es una Figura” y un “Círculo es una figura”. Ejercicio

Ejercicio 6

Ejercicio

Interfaces

Uso de Interfaces

Otro elemento o concepto que agregar a nuestro dominio en la orientación de objetos.

Palabras reservadas clave: implements, interface

Surge de la necesidad de indicar a otras clases el “que” y no el “como”. Aspecto muy similar a los métodos abstractos con las clases Abstractas. En esencia, estos métodos abstractos especifican una interfaz del método, pero no su implementación. Son las subclases de la clase abstracta las que se encargaran de proporcionar, cada una, su propia implementación.

Utilidad

  • Propagar funcionalidades a clases sin relación (sin herencia).
  • Comportamientos comunes en grupos de clases pertenecientes a diferentes jerarquías. Ejemplo: Supongamos, por ejemplo, que recibimos información en tiempo real de una serie de estaciones meteorológicas. A partir de dicha información deseamos: mostrarla (app, web,…), generar estadísticas relativas al día en curso (max, mín, promedio,…), realizar predicciones para las próximas horas o días… Clases funcionalmente diferentes pero con algo común, deben actualizarse con cada nueva medida realizada.
  • Simular “herencia múltiple” (la cual con sólo herencia nose puede): una clase podrá implementar más de una interfaz.

Al lío

Declaración:

[public] interface NombreInterfaz [extends InterfazPadre] {
 tipo VAR1 = valor1;
 tipo VAR2 = valor2;
 ...
 [private*
] tipo nombreMetodo1(lista_de_params);
 [private*
] tipo nombreMetodo2(lista_de_params);
}

Aspectos importantes:

  • Una clase deberá implementar todos los métodos de una interfaz.
  • Un interface podrá especificar acceso public o por defecto (si se omite). Si se declara public, debe estar en un fichero con el mismo nombre.
  • Variables: siempre serán constactes y hay que inicializarlas: public+static+final
  • Por lo general todos los métodos son públicos, novedades últimas versiones del JDK:
    • Se pueden crear métodos private (uso interno para la interfaz).
    • Métodos static: se evita la sobreescritura.

Ejemplo de declaración:

public interface Producto {
 double IVA_G = 0.21;
 double IVA_R = 0.10;
 double IVA_SR = 0.04;

 double getPrecio();

 String getNombre();
 
 public static double getTotal(Producto[] lista) {
 
    double sum = 0.0;
    for(Producto p: lista) sum += p.getPrecio();
        return sum;
    }
}

Implementación:

Una vez el interface ha sido declarado, podrá ser implementado por una o más clases. Para ello, la clase incluirá la cláusula implements seguida por el nombre del interface (pueden ser varios separados por comas):

class Nombre [extends Superclase] implements Interface1[, Interface2,...] {
 // Cuerpo de la clase
}

Aspectos importantes:

  • La clase deberá implemetar todos los métodos abstractos del interface (sin cuerpo) y deberá establecer para ellos el modo de acceso public.
  • No está obligada a sobreescribir los métodos por defecto (con cuerpo) del interface. En dicho caso, utilizará la implementación del propio interface.
  • No tiene acceso a los métodos private del interface. Son de uso interno de los métodos con implementaciones por defecto o private del interfaz.
  • No “hereda” ni puede sobreescribir los métodos static del interface. Estos sólo se pueden invocar haciendo uso del propio interface. Producto.metodo()

Ejemplo de implementación:

public class Libro implements Producto {
 
 private final double precio;
 private final String titulo;
 private final int numpag;

 public Libro(String titulo, double precio, int numpag) {
    this.titulo = titulo;
    this.precio = precio;
    this.numpag = numpag;
 }

 @Override
 public double getPrecio() { return this.precio; }
 
 @Override
 public String getNombre() { return this.titulo; }
 
 public int getNumpag() { return this.numpag; }
 
 public class Pelicula implements Producto {
 
 private final double precio;
 private final String titulo;
 private final int durac;
 
 public Pelicula(String titulo, double precio, int durac) {
    this.titulo = titulo;
    this.precio = precio;
    this.durac = durac;
 }

 @Override
 public double getPrecio() { return this.precio; }
 
 @Override
 public String getNombre() { return this.titulo; }
 
 public int getDuracion() { return this.durac; }
}

public class Tienda {
 public static void main(String[] args) {
 
    Libro libro1 = new Libro("Crimen y Castigo", 10.40, 752);
    Pelicula peli1 = new Pelicula("Matrix", 9.99, 150);
    System.out.println(libro1.getNombre() + ", " + libro1.getPrecio() + "€");
    System.out.println(peli1.getNombre() + ", " + peli1.getPrecio() + "€");
}
 

Con Herencia:

  • Un interface puede derivar (extender) de otro interface
  • Como en el caso de la herencia de clases, se empleará la cláusula extends
  • Un interface sólo podrá tener un padre.
  • Una clase que implemente una interface que, a su vez, herede de otro interface, deberá implementar todos los métodos de dicha cadena de herencia (que no tengan un cuerpo por defecto, sean static o private).
  • Una clase podrá implementar cuantos interfaces desee, añadiéndolos a la cláusula implements. Aunque conceptualmente no podemos hablar de herencia múltiple, este mecanismo proporciona a las clases Java cierta capacidad para “incorporar” diferentes comportamientos.

Ejercicios:

Ejemplo: una clase que implementa el interface serie. Para simplificar todas las clases en Unidad4.java

package series;
interface Serie{
    int obtenerSiguiente(); //devuelve el siguiente número de la serie,
    void restablecer(); //reinicia
    void establecerInicio(int x); //establece el valor inicial
}
class MasDos implements Serie{
    int inicio;
    int val;
    MasDos(){
        inicio=0;
        val=0;
    }
    public int obtenerSiguiente(){
        val +=2;
        return val;
    }
    public void restablecer(){
        inicio=0;
        val=0;
    }
    public void establecerInicio(int x){
        inicio=x;
        val=x;
    }
}
class Unidad4{
    public static void main(String[] args) {
        MasDos ob = new MasDos();
        for(int i=0;i<5;i++)
            System.out.println("el siguiente valor es: "+ ob.obtenerSiguiente());
        System.out.println("restableciendo ...");
        ob.restablecer();
        for(int i=0;i<5;i++)
            System.out.println("el siguiente valor es: "+ ob.obtenerSiguiente());
        System.out.println("empezando en 100 ...");
        ob.establecerInicio(100);
        for(int i=0;i<5;i++)
            System.out.println("el siguiente valor es: "+ ob.obtenerSiguiente());
    }
}

Comprueba con el código anterior que:

  • Si quiero declarar el interface como public tengo que escribirlo en otro fichero. Recuerda que un fichero .java sólo puede contener una clase/interface public, y que si existe una clase o interface public su nombre debe coincidir con el nombre del fichero.
  • Recuerda que en un interface un método siempre es público y que en su declaración se ponga o no public un método siempre es public. Pero ojo, en su implementación también tiene que ser public pero hay poner el public explícitamente de forma obligatoria. Observa por ejemplo el error al retirar el acceso public a obtenerSiguiente().

Ejercicio 1:

El main es ahora:

class Unidad4{
    public static void main(String[] args) {
        MasDos ob2 = new MasDos();
        MasTres ob3 = new MasTres();

        System.out.println("De dos en dos ...");
        for(int i=0;i<5;i++)
        System.out.println("el siguiente valor es: "+ ob2.obtenerSiguiente());

        System.out.println("De tres en tres ...");
        for(int i=0;i<5;i++)
        System.out.println("el siguiente valor es: "+ ob3.obtenerSiguiente());
    }
}

De dos en dos … el siguiente valor es: 2 el siguiente valor es: 4 el siguiente valor es: 6 el siguiente valor es: 8 el siguiente valor es: 10 De tres en tres … el siguiente valor es: 3 el siguiente valor es: 6 el siguiente valor es: 9 el siguiente valor es: 12 el siguiente valor es: 15 Y por tanto debo crear una clase MasTres que produzca la salida anterior.

Ejercicio 2:

Tenemos una serie de clases muy diferentes que implementan el mismo interface Parlanchin según el siguiente diagrama UML: El método habla() genera una descripción textual del sonido del objeto que habla. Probamos la estructura desde Unidad4: UML ejercicio2

public class Unidad4 {
    public static void main(String[] args) {
        Gato g = new Gato();
        Perro p = new Perro();
        RelojCuco rc = new RelojCuco();
        g.habla();
        p.habla();
        rc.habla();
    }
}

que genera la salida

¡Miau! ¡Guau! ¡Cucu, cucu, ..!

todas las clases pertenecen al paquete parlanchines, y se permite que todas las clases hagan println().

Ejercicio3:

Ahora ampliamos el ejercicio anterior para incluir a Gato y Perro como subclase de la clase Abstracta Animal. UML ejercicio3

Interfaz vs Clases Abstractas

Comparación

Hay aspectos o usos que comparten ambos elementos dentro de la O.O. como principios de abstración, modularidad y polimorfimo (lo veremos más adelante). Pero también hay notables diferencias:

Clase Abstracta Interfaz
Puede tener método abstractos y no-abstractos Sólo métodos abstractos (desde Java 8 puede tener métodos no-abstractos por defecto y estáticos)
No soporta herencia múltiple Soporta implementación múltiple de interfaces
Puede tener variables final, no-final, static y no-static Sólo variables public, final y static
Puede tener miembros con diferentes modos de acceso Los métodos son public (desde Java 9 puede incluir métodos private para uso interno del interface)
Proporciona un nivel de abstracción parcial Proporciona un nivel de abstracción total

Optaremos por una clase abstracta si:

  • Existen clases relacionadas entre sí que necesitan compartir o reutilizar parte del código.
  • Existen clases relacionadas que presentan una estructura común.

Optaremos por una interfaz:

  • Deseamos especificar un comportamiento (funcionalidad pura) común para clases no necesariamente relacionadas.
  • Necesitamos que las clases puedan incorporar múltiples comportamientos independientes (herencia múltiple).

Polimorfismo

Introducción

Como sabemos, Java es de tipado estático y estricto. Salvo contadas excepciones (promociones automáticas y conversiones), a una variable de tipo primitivo, no se le podrá asignar un valor de un tipo diferente al suyo. Del mismo modo, una variable de tipo referencia de una clase, no podrá eferenciar un objeto de otra clase. Existe, sin embargo, una importante excepción de este último supuesto: “A una variable de tipo referencia de una superclase, se le podrá asignar una referencia a un objeto de cualquiera de sus subclases”

Ejemplo de Polimorfismo con Herencia

Es necesario una relación jerárquica entre las clases para poder usar el polimorfismo. Para el polimorfismo se utiliza ligadura dinámica.

Ligadura dinámica: Cuando se llama a un método sobrescrito mediante una referencia a una superclase, java determina qué versión del método ejecutar basándose en el tipo del objeto al que se referencia. El funcionamiento descrito en el enunciado anterior recibe el nombre de “despacho dinámico de métodos”, “ligadura dinámica”, …. Y otros nombres. Esta técnica consiste en que java puede decidir que versión de método sobreescrito a ejecutar en tiempo de ejecución, de ahí lo de “dinámico”. Poder tomar esta decisión en tiempo de ejecución es muy importante porque es uno de los mecanismos con los que java soporta el polimorfismo.

La ligadura dinámica se consigue con dos mecanismos analizados anteriormente:

  • Asignar a una referencia de superclase, un objeto de subclase:
class A{
    int a;
    A(int i){a=i;}
}
class B extends A{
    int b;
    B(int i, int j){
        super(i);
        b=j;
 }
}
class C{
    int c;
    C(int k){c=k;}
}
class Unidad4{
 
    public static void main(String[] args) {
        A a1 = new A(2);
        A a2;
        B b = new B(2,4);
        C c=new C(8);
        a2=a1; //ningun problema, a2 y a1 son del mismo tipo
        //a2=c; // mal, a2 y c no son tipos compatibles
        a2=b; //bien, una referencia a superclase puede referenciar a un objeto subclase
        System.out.println(a2.a);//bien
        // System.out.println(a2.b); //mal, a2 sólo puede acceder a la parte de superclase
 }
}
  • Sobreescribir un método de la superclase en todas sus subclases:

Primero creamos em método genérico en la superclase (comportamiento común):

public class Animal {
 ...
 public String habla() { return "Animal no habla!!"; }
}

Sobreescribimos en cada subclase el método habla (especificación funcional):

public class Gato extends Animal {
 public Gato() {
    System.out.println("> Constructor de Gato");
 }
 public String habla() { return "Miau!!";} 
 }

public class Perro extends Animal {
 public Perro() {
    System.out.println("> Constructor de Perro");
 }
 public String habla() { return "Guau!!";} 
 }

 public class Fauna {
 
 public static void main(String[] args) {
 ...
    Animal[] lista = new Animal[3]; //referencias a la superclase
    lista[0] = new Gato("Tom", 7);
    lista[1] = new Perro("Scooby", 10);
    lista[2] = new Estudiante("Rigby", 14); //Añadimos al array varias instancias de las distintassubclases
    for(Animal a: lista)
        System.out.println(a.habla());
 }
}

Observamos como, en cada caso, se ha empleado el tipo del objeto referenciado y no el tipo de la variable, para determinar qué implementación del método se ejecuta.

Conversión (cast) de referencias

Podemos usar el operador cast para que una referencia de un tipo A se convierta en una Referencia de un tipo B. La conversión la hace el programador por su cuenta y riesgo, si hay algún tipo de incompatibilidad “salta” una excepción en tiempo de ejecución. ¿Para qué hacer cast de referencias?. Suponemos que B extiende a A como el ejemplo de abajo. Si yo como programador tengo una referencia a de tipo A que referencia a un objeto de tipo B(ver apartado anterior), me puede interesar convertir esa referencia de tipo A en tipo B para tener acceso a todas las partes del objeto B.

Ejemplo:

class A {
    public int atrib1;
}
class B extends A {
    public int atrib2;
}

public class Unidad4{
 public static void main(String[] args) {
    int temp;
    A a; // Referencia a objetos de la clase A
    a= new B (); // Referencia a objetos clase A, pero apunta realmente a objeto clase B
    //temp=a.atrib2; //error, obj apunta a un objeto de clase B pero solo tiene acceso a parte de superclase
    B b= (B) a;//la refencia a la convierto a otra de tipo B
    temp=b.atrib2;
 }
}

El operador instance of

En general “preferimos” trabajar con referencias de superclase para lucrarnos del mecanismo de polimorfismo, no obstante, puede darse el caso que necesite por alguna razón saber realmente a qué tipo de subclase está apuntando la referencia de la superclase o como comprobación previa para hacer un cast (punto anterior). Para ello utilizamos el operador instance of, por ejemplo si f es una referencia de tipo Figura

if(f instance of Triangulo)
    System.out.println("esta figura es un triángulo");

Ejemplo de Polimorfismo con Interfaces

  • Java nos permite crear variables referencia cuyo tipo sea un interface.
  • Una variable de este tipo podrá referenciar a cualquier objeto que implemente dicho interface.
  • Al invocar un método de un objeto a través de una referencia de tipo interface, se ejecutará la versión implementada por el objeto referenciado.
  • En tiempo de ejecución ligadura.
  • El proceso es similar al empleo de variables del tipo de la superclase para “mapear” el acceso a las implementaciones sobreescritas de sus métodos a través de los objetos de sus subclases.

Ejemplo:

public interface MiInterface {
   
    public void calcularArea();
    public void calcularPerimetro();
}

public class Triangulo implements MiInterface{
    int base = 3;
    int altura = 5;
   
    @Override
    public void calcularArea() {
        System.out.println(base*altura);
    }
    @Override
    public void calcularPerimetro() {      
    }
}

public class Cuadrado implements MiInterface{
    int lado = 4;
    @Override
    public void calcularArea() {
        System.out.println(lado*lado);
    }
    @Override
    public void calcularPerimetro() {
    }
}

public class Principal {
 
    public static void main(String[] args) {
        MiInterface tri = new Triangulo();
        MiInterface cua = new Cuadrado();
        tri.calcularArea();
        cua.calcularArea();
    }
}

Ejercicios

Ejercicio 1:

Volvemos al ejercicio de Series. Usamos las mismas clases y creamos en el siguiente main():

class Unidad4{
    public static void main(String[] args) {
        MasDos serie1 = new MasDos();
        MasDos serie2= new MasDos();
        MasTres serie3 = new MasTres();
        MasTres serie4= new MasTres();
        serie2.establecerInicio(200);
        serie4.establecerInicio(300);
        
        System.out.print("\nSerie1: ");
        for(int i=0;i<5;i++)
            System.out.print(serie1.obtenerSiguiente()+" ");
        System.out.print("\nSerie2: ");
        for(int i=0;i<5;i++)
            System.out.print(serie2.obtenerSiguiente()+" ");
        System.out.print("\nSerie3: ");
        for(int i=0;i<5;i++)
            System.out.print(serie3.obtenerSiguiente()+" ");
        System.out.print("\nSerie4: ");
        for(int i=0;i<5;i++)
            System.out.print(serie4.obtenerSiguiente()+" ");
   }
}

SE PIDE: Generando exactamente la misma salida que con el código anterior, escribir un código mejorado que utilice un array de interfaces de tipo Serie para evitar tanto código duplicado.

Ejercicio 2:

Escribe código para implantar la siguiente estructura. Luego en main de Unidad4 crea 3 triángulos, 3 rectángulos y 3 círculos almacenando las figuras en un array. Recorre el array e imprime el area y color de todas las figuras, cambiando el color a “negro” en aquellas figuras cuya área se mayor que 4.0.: UML ejercicio2

Ejercicio 3:

Se necesita crear un sistema para El control de pago del personal para la compañía Marejada Feliz. El mismo debe contar con:

  • Clase Barco:
    • Con los atributos: Nombre y tipo de tipo String, Capacidad de pasajero y capacidad de carga tipo int.
    • Método para mostrar todos los datos del barco
  • Clase GPS:
    • Con los atributos: coordenadas en X, coordenada Y, fecha y hora de tipo String, días tripulado tipo int.
  • Clase abstracta Tripulante:
    • El mismo debe tener los siguientes atributos: numero carnet, posición gps, edad, tiempo en la empresa de tipo int; nombre y telefono tipo String, sexo tipo char, barco de tipo barco
    • Métodos abstractos sueldo y mostrar datos
      • El método sueldo se calculara según el rango de cada tripulante en el barco
      • El método mostrar dato deberá mostrar todos los datos (atributos) según la clase derivada.
  • Clase Capitan:
    • Atributos horas de experiencia tipo int, constante sueldo de 4.500.000, sueldo total y bono tipo float.
    • Método propio para calcular el bono de la siguiente manera:
      • Si las horas de experiencia es mayor igual a 5000 y menor a 150000 tendrá un bono del 20%
      • Si las horas de experiencia es mayor igual a 150000 y menor a 300000 tendrá un bono del 40%. Y si es mayor a 300000 será un 75% de bono.
      • Su sueldo total se calculará: sueldo mas bono.
  • Clase Jefe de flota:
    • Atributos peso Pescado y peso mariscos tipo int, constante sueldo de 350.000.000, sueldo total y bono pescado y bono mariscos tipo float.
    • Método propio para calcular los bonos de la siguiente manera:
      • Si son pescados, se multiplicará la cantidad 1 y si son mariscos por 2.
      • Su sueldo total se calculará: sueldo mas bonos.
  • Clase Marinero:
    • Atributos peso total pescado tipo int, constante sueldo de 90.000, sueldo total y bono tipo float.
    • Método propio para calcular el bono de la siguiente manera:
      • Si la cantidad pescada es mayor o igual a 1 se multiplicara por 0.25
      • Su sueldo total se calculará: sueldo mas bonos.

Al menos prueba el programa para 7 marineros/jefe de flota/capitan y comprueba el polimorfismo en tu arquitectura O.O.

Excepciones

Introducción

Las excepciones en Java son errores que se producen en tiempo de ejecución. Cuando realizamos el proceso de compilación pueden aparecer una serie de errores que deben ser corregidos para que el programa pueda compilarse y generarse. Más tarde, durante la ejecución del mismo, pueden producirse una serie de errores, los cuales son imposibles de detectar durante la fase de compilación. Son este tipo de errores los que, en Java, acaban lanzando una excepción si llegan a producirse:

Estamos escribiendo en disco y éste se llena (IOException ) Estamos descargando un fichero y cae la conexión a Internet (ConnectionException ) Estamos recorriendo un array en un bucle y accedemos a una posición que no existe (IndexOutOfBoundException ) Accedemos atributos o métodos de un objeto cuyo valor es nulo (NullPointerException ) Realizamos una operación matemática no válida (división por cero) (ArithmeticException )

En Java se propone el control de excepciones para evitar que se produzcan estos errores en lugar de tener que escribir continuamente estructuras if que controlen que no ocurre nada anómalo que impida la ejecución de un cierto código. Así, lo que haremos será colocar dentro un bloque controlado todo el código (y el que dependa de éste) que sea susceptible de producir una excepción, sin interrumpir el flujo de nuestro programa (al contrario de lo que ocurre añadiendo sentencias if ).

Palabras reservadas clave: try, catch, exception, throws, finally

Uso de try catch

Por tanto podemos predecir que tipo de errores/excepciones tendrá nuestro programa. Y Java nos aporta el “try catch” para “capturarlas”. Es decir capturar y tratar dichas excepciones para que no se rompa el programa. Crear programas más robustos con control de errores. Ejemplo:

class App {
public static void main(String[] args) {
    int pesoPaquete;
    try{ 
        System.out.println("antes de que se genere la excepción");
        pesoPaquete=10/0;
        System.out.println("esto jamás se imprime");
    
    }catch(ArithmeticException miExcepcion){
        System.out.println("muy, muy, mu mal: no se puede dividir por cero");
    }
}
}

En el anterior ejemplo observamos que el try es el que va a definir el espacio donde esperamos que surga la excepción. Si se genera dicha excepcion el catch determina lo que se va a ejecutar. El try y el catch son inseparables, y dentro de los paréntisis del catch creamos una variable (o varias) que referenciará la excepción. También no puede haber instruciones entre el try y el catch.

Un try y varios catch

En un mismo bloque de sentencias nos puede interesar capturar diferentes excepciones:

class App {
public static void main(String[] args) {
    int pesoPaquete=10;
    int divisor=0;
    try{ 
        System.out.println("antes de que se genere la excepción");
        pesoPaquete=pesoPaquete/divisor;
        pesoPaquete= Integer.parseInt("10.5");
        System.out.println("esto jamás se imprime");
    }catch(ArithmeticException miExcepcion){
        System.out.println("muy, muy, muy, mal no se puede dividir por cero");
    }catch(NumberFormatException miExcepcion){
        System.out.println("imposible convertir en entero ése string");
    }
    System.out.println("el programa sigue su ejecución se recupero de la excepción ...");
}
}

Una formas más compacta:

class App {
    public static void main(String[] args) {
        int pesoPaquete=10;
        int divisor=0;
        try{ 
            System.out.println("antes de que se genere la excepción");
            pesoPaquete=pesoPaquete/divisor;
            pesoPaquete= Integer.parseInt("10.5");
            System.out.println("esto jamás se imprime");
        }catch(ArithmeticException|NumberFormatException miExcepcion){
        System.out.println("mi código es excepcional....");
        }
        System.out.println("el programa sigue su ejecución se recupero de la excepción ...");
    }
}

Ejercicio 1

Si al trabajar con un array x de tamaño n, el último elemento es x[n-1]. Si sobrepasamos los límites del array por ejemplo intentando usar x[n], java genera una excepción. Reforma el código para que capture la excepción y el programa no rompa.

class App{
public static void main(String[] args){
int[] x= {0,1,2,3,4};
x[5]=5;
System.out.println("El programa se recupera de la excepción y continua");
}
}

Ejercicio 2

Queremos calcular el factorial de un número pero asegurándonos que el usuario introduce un entero por teclado. Hasta que no introduzca un entero le estaremos pidiendo repetitivamente que introduzca el entero. Investiga InputMismatchException y NumberFormatException.

class App{
public static void main(String[] args) {
    Scanner teclado= new Scanner(System.in);
    int n=0; //Numero entero introducido por teclado.
    //añadir código para obtener número entero correcto usando mecanismo excepciones
    //calculamos factorial
    int factorial=n;
    String salida=n+"! = "+n;
    for(int i=n-1;i>0;i--){
        salida+="*"+i;
        factorial*=i;
    }
    salida+=" = "+factorial;
    System.out.println(salida);
}
}

Uso del throws

Como estáis observando en Java todas las excepciones son clase y derivan de Throwable, cuando se genera una excepción se crea un objeto de alguna de ellas. La clase padre de las excepciones es Throwable y las subclases:

  • Error: se corresponden con errores serios de la JVM y que nuestra aplicación no capturará (OutOfMemoryError,…).
  • Exception: se corresponden con errores debidos a la propia actividad del programa (ArithmeticException, NullPointerException,…) y serán habitualmente capturadas.

Jerarquía Métodos más usados

A veces nos interesa propagar una excepción a métodos o clases superiores o también generar una excepción. Para ello tenemos el throws o el throw:

public class DemoEx5 {
 
 public static int dameNum() throws NumberFormatException {
    
    java.util.Scanner cin = new java.util.Scanner(System.in);
    return Integer.parseInt(cin.nextLine().trim());
 }

 public static void main(String[] args) {
    System.out.println("Dame un número: ");
    try {
        System.out.println(dameNum());
    }
    catch(Exception e) {
        System.out.println("Excepción recibida --> " + e);
    }
}
}
public class DemoEx4 {
 public static int dameNum() {
    java.util.Scanner cin = new java.util.Scanner(System.in);
    int num;
    try {
        num = Integer.parseInt(cin.nextLine().trim());
    }
    catch(NumberFormatException e) {
        System.out.println("Entrada no válida. Relanzando excepción...");
        throw e;
    }
    return num;
 }

 public static void main(String[] args) {
    System.out.println("Dame un número: ");
    try {
        System.out.println(dameNum());
    }
    catch(Exception e) {
    System.out.println("Excepción recibida --> " + e);
 }
}
public class TestThrow1 {   
    public static void validate(int age) {  
        if(age<18) {  
            throw new ArithmeticException("Person is not eligible to vote");    
        }  
        else {  
            System.out.println("Person is eligible to vote!!");  
        }  
    }  
    public static void main(String args[]){  
        validate(13);  
        System.out.println("rest of the code...");    
  }    
}  

Ejercicio 3

Partiendo del código fuente siguiente añádele la capacidad de generar una excepción cuando se genere una figura de color blanco de forma que funcione el siguiente main.

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

try {
    Circulo c = new Circulo(2.0, "blanco");
    System.out.println("Area circulo " + c.area());
} catch (Exception e) {
    System.out.println("NO SE PUDO CREAR OBJETO: " + e.getMessage());
}
try {
    Triangulo t = new Triangulo(2.0, 3.0, "rojo");
    System.out.println("Area triangulo " + t.area());
} catch (Exception e) {
    System.out.println("NO SE PUDO CREAR OBJETO" + e.getMessage());
}
}
}

abstract class Figura {
    protected String color;
    public Figura(String color) {
        this.color = color;
    }
    abstract public double area();
}

class Triangulo extends Figura {
    private double base;
    private double altura;
    public Triangulo(double base, double altura, String color) {
        super(color);
        this.base = base;
        this.altura = altura;
    }
    @Override
    public double area() {
        return base * altura / 2;
    }

}

class Circulo extends Figura {
    private double radio;
    public Circulo(double radio, String color)  {
        super(color);
        this.radio = radio;
    }
    @Override
    public double area() {
        return Math.PI * radio * radio;
    }
}

Uso del finally

Existe un tercer bloque de instrucciones cuando usamos try-catch, se llama finally. En este bloque incluiremos las instrucciones que siempre queremos ejecutar al finalizar el try, haya o no excepciones. Y ante cualquier situación de error o exit siempre se ejecuta. Ejemplo:

class UsoFinally{
    static void generarExcepcion(int paraSwitch){
        int t=10;
        System.out.println("Recibiendo " + paraSwitch);
        try{
        switch(paraSwitch){
            case 0:
            t=10/paraSwitch;//forzamos division por zero
            break;
            case 1:
            t=Integer.parseInt("10.5");//provocamos error de formato
            break;
            case 2:
            return;
    }
    }catch(ArithmeticException exc){
        System.out.println("no se puede dividir por cero ¡animal!");
        return;
    }catch(NumberFormatException miExcepcion){
        System.out.println("imposible convertir en entero ese string");
    }finally{
        System.out.println("dejando try");
        System.out.println();
    }
    }
} 
class App {
    public static void main(String[] args) {
        for(int i=0;i<3;i++){
        UsoFinally.generarExcepcion(i);
        }
    }
}

Este bloque es muy práctico para asegurarnos cierres de conexión, liberación de recursos etc…

Ejercicio 4

Escribe de nuevo el siguiente código, respetándose tal cual, pero añadiendo la capacidad de trabajar con excepciones (clase Exception) de forma que cuando se crea un rectángulo cuyo origen es un punto con alguna coordenada negativa se lanza una Exception.

class Punto {
    int x = 0;
    int y = 0;
    Punto(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
class Rectangulo {
    Punto origen;
    int ancho;
    int alto;
    Rectangulo(int x, int y, int w, int h) {
        origen = new Punto(x,y);
        ancho = w;
        alto = h;
    }
}
class App{
    public static void main(String[] args){
        Rectangulo miRectangulo=new Rectangulo(-2,3,4,5);
}
}

Jerarquía

Jerarquía

Crear tu propias excepciones

En Java a parte de terner las excepciones “oficiales”, también podemos implementar nuestras propias excepciones “a medida”. Y nos aporta un mayor grado de flexibilidad y libertad. A través de la creación y uso de estas excepciones propias, podremos manejar errores que sean específicos de nuestra aplicación. Estas nuevas excepciones serán gestionadas empleando los mismos mecanismos proporcionados por Java para capturar, procesar y relanzar cualquiera de las excepciones predefinidas en el lenguaje. Por convenio el nombre de la nueva clase para tu excepción debe terminar en “Exception”.

La forma es usar herencia con la clase Exception, y podemos usar estos métodos con sobreescritura:

  • getMessage()
  • printStackTrace()
  • toString()
public class DatoNoValidoException extends Exception {
 int errCode;
 String valor;
 public DatoNoValidoException(String valor, int errCode, String msg) {
    super(msg);
    this.valor = valor;
    this.errCode = errCode;
 }
 public int getErrorCode() {
    return this.errCode;
 }
 public String toString() {
    return "[ERR: " + this.errCode + "] " +
    this.getMessage() + " (value: " + this.valor + ")";
 }
}

public class DemoEx6 {
 public static void validaEdad(int edad) throws DatoNoValidoException {
    if(edad < 18)
        throw new DatoNoValidoException(Integer.toString(edad), 101, "Menor de edad");
    }
 public static void main(String[] args) {
    try {
        validaEdad(15);
    }
    catch(Exception e) {
        System.out.println("Excepción recibida --> " + e);
    }
 }
}

Ejercicio 5

Tenemos un pequeño programa que controla el stock de un producto (id, precio y cantidad). La cantidad por defecto es 50 unidades. Crea un método de vender producto (puede ser estático), y crea tu propia clase de excepción de falta de stock.

Ejercicio 6

Amplia el ejercicio de Ordenadores del inicio del curso: ejercicio6

  • Añadir a la clase Ordenador el atributo: private String numeroDeSerie;

  • El constructor de Ordenador también se encarga de lanzar una excepción de tipo OrdenadorException cuando se intenta crear un Ordenador en las siguientes situaciones:

    • Se intenta configurar un ordenador con procesador modelo I7 sin disco tipo SATA3
    • El número de serie comienza por HP y tiene menos de 4gb de ram. (Busca en API método de clase String que devuelve comienzo de String)
  • Un objeto OrdenadorException recibe en su constructor un número de serie y un mensaje y reescribe con esta info getMessage()

  • Funciona el siguiente main:

//Principal.java
import ordenador.Ordenador;
import ordenador.OrdenadorException;
class Principal{
    public static void main(String[] args) {
       
        //formato contructor Ordenador:
        //NumeroDeSerie,capacidadMemoria,tipMemoria,velocidadMemoria,capacidadDisco,tipoDisco,velocidadDisco,tipoProcesador,velocidadProcesador,precio
        try{
                new Ordenador("DELL122",8,"DDR2",533,(float)2.0,"SATA",7200,"i7",(float)3.3,400);
                System.out.println("ORDENADOR DELL122 OK");
        }catch(OrdenadorException e){
                System.out.println(e.getMessage());
        }
        try{
                new Ordenador("CLONIC900",8,"DDR2",533,(float)2.0,"SATA3",7200,"i7",(float)3.3,400);
                System.out.println("ORDENADOR CLONIC900 OK");
        }catch(OrdenadorException e){
                System.out.println(e.getMessage());
        }
        try{
                new Ordenador("HP511",2,"DDR2",533,(float)2.0,"SATA",7200,"i5",(float)3.3,400);
                System.out.println("ORDENADOR HP511 OK");
        }catch(OrdenadorException e){
                System.out.println(e.getMessage());
        }
    }
}

Que genera la salida: DELL122: I7 sin SATA3 no se monta ORDENADOR CLONIC900 OK HP511: Serie HP no puede tener menos de 4gb de memoria

Validación de datos

Introducción

Una vez vistas las excepciones surge la necesidad de poder validar y comprobar la información que recogemos, ya sea desde teclado, web etc…

Validar si un caracter tiene letra

Con el método estático de Character isAlphabetic() que devuelve boolean:

public class StudyTonight 
{
    public static void main(String[] args) 
    {
    int cp1 = 56;
    int cp2 = 88;

    boolean b1 = Character.isAlphabetic(cp1);
    boolean b2 = Character.isAlphabetic(cp2);

    System.out.println((char) cp1 + " is alphabet? " + b1);
    System.out.println((char) cp2 + " is alphabet? " + b2);

    }
}

Character tiene más métodos interesantes como: isDigit() isDefined() etc…

isEmpty()

Este método comprueba sólo la longitud de la cadena, y devuelve boolean true si es 0.

isBlank()

Método que nos sirve para comprobar si el String está vacio o con espacios vacios (devolvería true):

System.out.println("ABC".isBlank());          //false

System.out.println(" ABC ".isBlank());        //false

System.out.println("  ".isBlank());         //true

System.out.println("".isBlank());            //true

Uso de NumberFormatException

Una de las formas de comprobar si un String tiene un número es con los siguientes métodos de Wrappers:

  • Integer.parseInt()
  • Integer.valueOf()
  • Double.parseDouble()
  • Float.parseFloat()
  • Long.parseLong()

Estos métodos generarán la excepción NumberFormatException si no se puede parsear a número. Método que comprueba lo anterior:

public static boolean isNumeric(String string) {
    int intValue;
    System.out.println(String.format("Parsing string: \"%s\"", string));
    if(string == null || string.equals("")) {
        System.out.println("String cannot be parsed, it is null or empty.");
        return false;
    }
    
    try {
        intValue = Integer.parseInt(string);
        return true;
    } catch (NumberFormatException e) {
        System.out.println("Input String cannot be parsed to Integer.");
    }
    return false;
}

Uso de InputMismatchException

Ejemplo de validación de datos con excepciones:

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int [] array = {4,2,6,7};
    int n;
    boolean repetir = false;
    do{
         try{
                repetir = false;
                System.out.print("Introduce un número entero > 0 y < " + array.length + " ");                     
                n = sc.nextInt();
                System.out.println("Valor en la posición " + n + ": " + array[n]);
         }catch(InputMismatchException e){
                   sc.nextLine();
                   n = 0;
                   System.out.println("Debe introducir un número entero ");
                   repetir = true;
         }catch(IndexOutOfBoundsException e){
                  System.out.println("Debe introducir un número entero > 0 y < " + array.length + " ");           
                  repetir = true;
         }catch(Exception e){ //resto de excepciones de tipo Exception y derivadas
                   System.out.println("Error inesperado " + e.toString());
                   repetir = true;
         }
     }while(repetir);
}

Comprobar objetos nulos

Ya estáis acostumbrados a usar “==” o “!=” con null para comprobar si una variable de un objeto referencia o no un objeto. En versiones recientes de Java también tenéis Objects.nonNull:

List<String> lista= new ArrayList<String>();
    lista.add("juan");
    lista.add("ana");
    lista.add("gema");
    lista.add(null);
    lista.add("blanca");
    lista.add(null);
    lista.add("david");
    for (String p:lista) { 
      if (Objects.nonNull(p)) {
        System.out.println(p);
      }
    }

Expresiones regulares

¿Qué son las expresiones regulares? –> Es una formula o conjunto de caracteres que nos serviran para definir o validar un String. Es decir la expresión regular la construiremos indicando que Strings aceptamos.

Diagrama resumen

Las más utilizadas:

  • ^ Indica el principio de una cadena
  • $ Indica el final de una cadena
  • () Un agrupamiento de parte de una expresión
  • [] Un conjunto de caracteres de la expresión
  • {} Indica un número o intervalo de longitud de la expresión
  • . Cualquier caracter salvo el salto de línea
  • ? 0-1 ocurrencias de la expresión
  • “+” 1-n ocurrencias de la expresión
  • “*” 0-n ocurrencias de la expresión
  • \ Para escribir un caracter especial como los anteriores y que sea tratado como un literal
  • | Para indicar una disyunción lógica (para elegir entre dos valores: a|b se tiene que cumplir al menos uno de los dos)

Portal web de prueba –> https://regex101.com/

Guía completa

Método matches() de String

Es la forma más sencilla de usar expresiones regulares con Java, devuelve booleano (verdadero si la expresión regular encaja con el String). Hay que tener cuidado con , ya que tanto para Java como para la expresiones regulares tiene un significado por tanto hay que “escaparlo” “\”:

 String str = new String("Welcome to Tutorialspoint.com");

      System.out.print("Return Value :" );
      System.out.println(str.matches("(.*)Tutorials(.*)"));

      System.out.print("Return Value :" );
      System.out.println(str.matches("Tutorials"));

      System.out.print("Return Value :" );
      System.out.println(str.matches("Welcome(.*)"));

Otra forma de usar este método pero de manera estática es con la clase Pattern:

System.out.println(Pattern.matches("-?\\d+","1234")); 

Pero lo más frecuente y con más posibilidades es usar la clase Pattern para crear el patrón de la expresión regular con el método compile() que genera el objeto. Este objeto tiene el método matcher() para ir obteniendo los tokens que encage con la expresión regular:

Pattern pat; 
Matcher mat; 
pat = Pattern.compile("-?\\d+"); 
mat=pat.matcher("-1234"); 
System.out.println(mat.matches());

Método split() de Pattern

Este método lo podemos usar para recoger los tokens que encagen en una expresión regular:

Pattern p = Pattern.compile(" ");
String tmp = "this is a test";
String[] tokens = p.split(tmp);
for (int i = 0; i < tokens.length; i++) {
    System.out.println(tokens[i]);
}

Hay muchas posibilidades podéis investigar por ejemplo el método find() de Matcher.

Ejercicio 1

Actualiza la clase DNI para que el constructor procese el String usando una expresión regular que de por bueno el siguiente formato: de 1 a 8 dígitos seguidos por una letra mayúscula o minúscula. Si el dni no es válido que se lance una excepción personalizada.

Ejercicio 2

Crea un validador de direcciones IP. Recuerda que las direcciones IP se especifican en decimal como 4 grupos de números separados por “.” Cada grupo puede contener un número decimal de 0 a 255. Por ejemplo son IP válidas:

  • 0.1.2.3
  • 255.255.255.255
  • 9.234.1.199

Son inválidas:

  • 0.1.2.
  • 0.1.2.3.
  • 256.1.2.3

Solución:

  String trozoER="(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";
  String ERCompleta=trozoER+"\\."+trozoER+"\\."+trozoER+"\\."+trozoER;	

 /**
  *  la base de la ER es (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]), observa:
	va entre paréntesis  para que la opcionalidad de | sea dentro de lo que hay en los paréntesis
	Cada parte de una dirección IP puede tener 1, 2 o 3 dígitos:
	3 dígitos pueden ser números que:
	empiezan por 25: 251,252,253,254 o 255. Lo que se resume con 25[0-5]
	empiezan por 2 pero siendo algo menor que 250. Lo que se resume con 2[0-4][0-9]
	empiezan por 1: 1[0-9][0-9]
	2 digitos, : pero el primero de los números  no puede ser cero [1-9][0-9]
	1 digito [0-9]
  * /    

Enumerados Avanzados

Introducción

Ya hemos visto y usado los tipos enumerados en la primera evaluación. Son apropiados para datos que tienen un rango de valores específicos:

enum Size { 
   SMALL, MEDIUM, LARGE, EXTRALARGE 
}

Esos rangos de valores son como constantes (por convenio se escriben con mayúsculas) que podemos asignar:

Size tamaño = Size.SMALL

Y son muy comodos de usar con switch:

enum Size {
 SMALL, MEDIUM, LARGE, EXTRALARGE
}

class Test {
 Size pizzaSize;
 public Test(Size pizzaSize) {
   this.pizzaSize = pizzaSize;
 }
 public void orderPizza() {
   switch(pizzaSize) {
     case SMALL:
       System.out.println("I ordered a small size pizza.");
       break;
     case MEDIUM:
       System.out.println("I ordered a medium size pizza.");
       break;
     default:
       System.out.println("I don't know which one to order.");
       break;
   }
 }
}

class Main {
 public static void main(String[] args) {
   Test t1 = new Test(Size.MEDIUM);
   t1.orderPizza();
 }
}

Métodos

Dentro de enumerados podemos crear métodos, para luego usarlos:

enum Size{
  SMALL, MEDIUM, LARGE, EXTRALARGE;

  public String getSize() {

    // this apunta en tiempo de ejecución en el main a SMALL
    switch(this) {
      case SMALL:
        return "small";

      case MEDIUM:
        return "medium";

      case LARGE:
        return "large";

      case EXTRALARGE:
        return "extra large";

      default:
        return null;
      }
   }
}
public static void main(String[] args) {
    System.out.println("The size of the pizza is " + Size.SMALL.getSize());
  }

Métodos predefinidos de Enum

  • ordinal(CONSTANTE): nos devuelve la posición dentro del enumerado.
  • compareTo(ENUM.CONSTANTE): comparación basada en ordinal(). Resultado como un compareTo normal.
  • toString(), name(), valueOf(): ya os imagináis lo que devuelven.
  • values(): nos devuelve un array con las constantes: Size[] enumArray = Size.values();

Constructores y atributos

En Java podemos tener constructores dentro de enumerados, como si fuera una clase normal. Y la diferencia que al definir las constantes podemos asignar valores. Y atributos:

enum Size {
   // enum constants calling the enum constructors 
   SMALL("The size is small."),
   MEDIUM("The size is medium."),
   LARGE("The size is large."),
   EXTRALARGE("The size is extra large.");

   private final String pizzaSize;

   // private enum constructor
   private Size(String pizzaSize) {
      this.pizzaSize = pizzaSize;
   }

   public String getSize() {
      return pizzaSize;
   }
}

class Main {
   public static void main(String[] args) {
      Size size = Size.SMALL;
      System.out.println(size.getSize());
   }
}

Otro ejemplo, con constructor, atributo y un método:

enum Direction {
  EAST(0), WEST(180), NORTH(90), SOUTH(270);

  // constructor
  private Direction(final int angle) {
    this.angle = angle;
  }

  // internal state
  private int angle;

  public int getAngle() {
    return angle;
  }
}

Incluso pueden soportar interfaces y algún aspecto más…

Ejercicio 1

Crea una interfaz con algún método, y a continuación que un enumerado la implemente. Prueba a ejecutar dicho método.

Ejercicio 2

Crea un interfaz que tengo un método llamado dia(), el enumerado tendrá de valores los días de la semana. Y el método los devolverá el día en numérico de la semana (Martes -> 2).

07.Colecciones

Colecciones

Java Collections Framework

Java Collections Framework

El Java Collections Framework es un conjunto de clases e interfaces que implementa estructuras de datos de uso habitual para colecciones de objetos: listas, conjuntos, mapas,…

  • Aunque su nombre oficial incluye el término “framework”, su funcionamiento es el de una librería. Basicamente, pone a nuestra disposición su conjunto de interfaces y las clases que los implementan para reutilizarlas libremente en nuestras aplicaciones.
  • Tanto colecciones como arrays nos permiten almacenar un conjunto de referencias a objetos y manejarlas como un único grupo. La principal diferencia radica en que, a diferencia de los arrays, las colecciones son dinámicas. No necesitan que especifiquemos cuál va a ser su capacidad en el momento de la instanciación y pueden “crecer” y “encoger” de forma automática a medida que añadimos o eliminamos objetos de la misma.
  • Las colecciones de Java sólo pueden almacenar tipos referenciados, por tanto, no podemos crear colecciones de tipos primitivos (int, double,…). Para almacenar valores de estos tipos, deberemos hacerlos a través de sus clases wrapper (envoltorio): Integer, Long, Double,…

Diagrama: Diagrama completo del framework

Resumen del diagrama: List: ordenada y con duplicados Set: no duplicados, no acceder por posición. Map: asociación claves y valores Queue: FIFO y otras formas.

Descripción de la intefaz

El interfaz java.util.Collection define la funcionalidad central que esperamos de cualquier colección que no sea un mapa. Sus métodos (ver figura de la página siguiente) se agrupan en cuatro grupos principales:

  • Añadir elementos.
  • Eliminar elementos.
  • Consultar el contenido de la colección.
  • Hacer accesible el contenido de la colección para procesamiento fuera de la colección.

Al ser Collection un subinterfaz de Java.lang.Iterable, cualquier colección que lo implemente podrá ser recorrida mediante bucles for-each.

Métodos para agregar objetos

  • boolean add(E e): Añade el nuevo objeto e a la colección
  • boolean addAll(Collection<? extends E> c): Añade todos los elementos de la colección c

Ambos métodos devuelven booleano, nos indicará si se agregado un elemento con éxito o si ha modificado la colección (hay colecciones que no aceptan duplicidad de objetos y pueden rechazar la inserción de objetos).

Métodos para eliminar objetos

  • void clear(): Elimina todos los elementos de la colección
  • boolean remove(Object o): Elimina el objeto o de la colección
  • boolean removeAll(Collection c): Elimina todos los elementos que se encuentren también en c.
  • boolean retainAll(Collection c): Mantiene sólo los elementos que se encuentren también c.

El argumento boolean devuelto por este método indica si la colección ha sido modificada en la ejecución.

Métodos para consultar a los objetos

  • boolean contains(Object o): true si la colección contiene al objeto o; false en otro caso.
  • boolean containsAll(Collection c): true si la colección contiene a todos los elementos de c.
  • boolean isEmpty(): true si la colección está vacía.
  • int size(): Devuelve el número de elementos de la colección.

Métodos para recorrer la colección

  • Iterator iterator(): Devuelve un iterador sobre la colección, lo más frecuente.
  • Object[] toArray(): Devuelve un array con todos los objetos de la colección, tipo Object
  • T[] toArray(T[] a): Devuelve un array con todos los objetos del tipo del array a

Introducción a las colecciones de objetos

Introducción a las colecciones de objetos

Surgen las colecciones como forma de agrupar objetos dentro de una arquitectuctura O.O.. Es decir organizar los objetos de una forma que se adapte a nuestras necesidades, y de forma dinámica. Y eso incluye organización de información y también funcionalidades específicas. Estas colecciones serán estruturas de datos dinámicas que cambian de tamaño en tiempo de ejecución, justo lo contrario a Arrays que debemos fijar el tamaño máximo previo a la ejecución del programa es una estrutura de datos estática. Para ello java nos ofrece Java Collections Framework que veremos a continuación.

Listas

Listas (interfaz List)

Es una colección de objetos en forma de nodos enlazados, donde poder insertar los objetos de manera dinámica y flexible. La interfaz List es la base para las colecciones que vamos a ver

interfaz list

Las dos implementaciónes más usadas de list son: ArrayList y LinkedList

ArrayList

Los elementos se guardan en una estructura de array dinámico. Es muy eficiente para recorrer todos los elementos, y algo más lento en insertar y borrar los objetos.

Crear un ArrayList

List<String> listStrings = new ArrayList<String>();
listStrings.add("One");
listStrings.add("Two");
listStrings.add("Three");
listStrings.add("Four");
System.out.println(listStrings);

Versión a partir de JDK7 (sintaxis más corta):

List<String> listStrings = new ArrayList<>();
listStrings.add("One");
listStrings.add("Two");
listStrings.add("Three");
listStrings.add("Four");
System.out.println(listStrings);

Forma alternativa y rápida (versiones ultimas de JDK) List.of: Se crea una lista de Integers indexados, a partir del método estático. Pero ojo se crea una lista inmutable -> nose puede modificar (concepto que veremos más adelante)

List<Integer> listNumbers = List.of(1, 2, 3, 4, 5, 6);

Alternativa y que nos genere algo modificable, usando el constructor de ArrayList:

 ArrayList<Integer> listaEspecial = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
listaEspecial.add(9);
listaEspecial.forEach(l -> System.out.println(l));

Crear una ArrayList a partir de otro (volcado de elementos): usando el constructor con parámetro

List<Integer> listNumberOne;  // existing collection
List<Integer> listNumberTwo = new ArrayList<>(listNumberOne);

Agregar objetos

Método add, que tiene dos versiones: add(Object): inserta en lista el objeto en la posición última. add(index, Object): insertar en la posición index el objeto

List<String> listStrings = new ArrayList<>();

listStrings.add("One");
listStrings.add("Two");
listStrings.add("Three");

listStrings.add(1, "Four");

Método addAll: agrega los objetos de una lista a otra (existe la versión con índice a partir de una posición se vuelcan).

listStrings.addAll(listWords);
listStrings.addAll(2, listWords);

Obtener objeto

Con el metodo get y un número se obtiene el objeto de dicha posición

String element = listStrings.get(1);

Actualizar objeto

Para ello tenemos el método set, especificando cual con la posición del arrayList:

listStrings.set(2, "Hi");

Eliminar objetos de la lista

Tenemos el método remove, y con un índice indicamos que objeto queremos eliminar de la lista, este método siempre es conveniente comprobar con el tamaño de la lista (size) que la posición existe:

listStrings.remove(2);

Para vaciar toda lista existe el clear().

Buscar objeto en la lista

Se pueden usar los métodos contains (devuelve booleano) y indexOf (devuelve la posición). Ejemplo:

import java.util.*;
public class ListaDemo3 {
 public static void main(String[] args) {
 List<String> lista = new ArrayList<>();
 String s = "bye";
 lista.add("hi");
 lista.add(s);
 System.out.println("hello está en la lista? " + lista.contains("hello"));
 System.out.println("bye está en la lista? " + lista.contains("bye"));
 System.out.println(s + " está en la lista? " + lista.contains(s));
 System.out.println("posición de " + s + " en la lista? " + lista.indexOf(s));
 }
}

Recorrer la lista

Hay muchas posibilidades para este menester, estas son las más típicas: For normal:

for (String element : listStrings) {
  System.out.println(element);
  }

Uso de Iterator o ListIterator (más posibilidades de recorrer y modificar), con esta forma los métodos imprescindibles:

  • next(): mueve el puntero de objeto a objeto, pero antes se obtiene el objeto
  • hasNext():devuelve verdadero si hay objeto en la siguiente posición. Ejemplo:
Libro libro1 = new Libro("0345404475", "Do Androids Dream of Electric Sheep?", "Philip K.Dick");
Libro libro2 = new Libro("0451457998", "2001: A Space Odissey", "Arthur C. Clarke");
List<Libro> biblio = new ArrayList<>()
biblio.add(libro1);
biblio.add(libro2);
Iterator<Libro> it = biblio.iterator(); // Iterator que apuntará al inicio de la colección
while(it.hasNext()) { // mientras haya "siguiente" elemento...
    Libro libro = it.next(); // leemos el "siguiente" elemento
    System.out.println("Título: " + libro));
}

ListIterator: métodos a parte de los de recorrido que podéis probar: add(), remove() y set() enlace a javadoc

Desde Java 8, se puede usar el forEach() con lambda:

listStrings.forEach(s -> System.out.println(s));

Volcar Array a List

Tenemos de la clase vista anteriormente Arrays un metodo muy cómodo que es asList. Ejemplo:

String[] array = {"new", "String", "array"};
List<String> list = Arrays.asList(array);
// y es modificafle es decir no inmutable

Ejercicios

Ejercicio 1:

Realiza un pequeño programa de agenda de personas, los datos recogidos serán número de telefono y nombre. El programa debe almacenar los contactos, nos debe dar el número de contactos de la agenda,listar la agenda (prueba todas las formas vistas), eliminar un contacto y resetear la agenda. Utiliza un ArrayList.

Ejercicio 2:

Escribe un método denominado swapPairs que intercambie los valores de un ArrayList de Strings por parejas. Es decir, intercambiará el primer elemento con el segundo, el tercero con el cuarto,…

En caso de que el número de elementos sea impar, el último elemento no se intercambiará.

Ejercicio 3:

Escribe un método denominado minToFront que reciba un ArrayList de enteros y mueva el mínimo de ellos al comienzo de la lista, manteniendo el orden del resto de números

Puedes asumir que la lista tendrá, al menos, un elemento. Por ejemplo: Test: 3, 8, 92, 4, 2, 17, 9 Resultado: [2, 3, 8, 92, 4, 17, 9]

Ejercicio 4:

Escribe un método denominado markLength4 que reciba un ArrayList de Strings como argumento e inserta la cadena “****” en frente de cada elemento que tenga 4 caracteres. Por ejemplo: Test: “this”, “is”, “lots”, “of”, “fun”, “for”, “every”, “Java”, “programmer” Resultado: **** , this, is, **** , lots, of, fun, for, every, ****, Java, programmer

Ejercicio 5:

Escribe un método denominado filterRange que reciba tres parámetros: un ArrayList de enteros, un valor min y un valor max. El método debe eliminar de la lista todos los elementos cuyo valor se encuentren en el rango [min, max]. Por ejemplo: Test: List lista = new ArrayList<>(Arrays.asList(new Integer[]{4, 7, 9, 2, 7, 7, 5, 3, 5, 1, 7, 8, 6, 7})); filterRange(lista, 5, 7); Resultado: [4, 9, 2, 3, 1, 8]

LinkedList

El diseño de esta colección de objetos varia a la del un ArrayList, en este caso es una lista enlazada. Y es más eficiente en inserciones y borrado de objetos. Ahora veremos sólo lo diferente al ArrayList.

Crear un LinkedList

List<String> listStrings = new LinkedList<>();

Obtener objeto en LinkedList

Con este implementación de List tenemos dos métodos bastante útiles: getFirst() y getLast(). Ejemplo:

LinkedList<Number> numbers = new LinkedList<Number>();
// add elements to the list...
// get the first and the last elements:
Number first = numbers.getFirst();
Number last = numbers.getLast();

Ejercicios Extra

Ejercicio 6:

Partiendo del esta interfaz:

public interface Ejercicio6 {
	
void insert(Object x); //Inserta x 
void remove(Object x ); //Elimina el primer x
void removeCurrent();  //Elimina el elemento current
boolean find(Object x); //Coloca current para poder ver x
void goFirst(); // Coloca current en la primera posición
void advance(); // Avanza current al siguiente nodo
boolean isOnList(); //No está vacía
Object getCurrent(); //Elemento en la posición current
Object getPrevious(); // Elemento de la posición anterior al current
}

Nos piden que implementemos dichas funcionalidades con la colección LinkedList, sobre un programa de reservas para un avión. Todas las reservas serán de un mismo avión. Los datos necesarios son origen, destino, plaza, businnes y los datos del pasajero (dni, nombre, apellidos, edad y nacionalidad).

Conjuntos (Sets)

Aspectos a tener en cuenta

Un conjunto (set) es una colección de objetos que no puede contener duplicados. Añadir un elemento ya presente en la colección no tendrá ningún efecto. Conjuntos

El interfaz Set define los mismos métodos que el interfaz Collection. Hay muchos tipos de colecciones que implementan la interfaz Set: HashSet, EnumSet, AbstractSet, LinkedHashSet, TreeSet, CopyOnWriteArraySet,…

Elegir una u otra dependerá del rendimiento en estos factores: búsqueda, lectura, ordenación etc…

Clases que implementan Set

Implementación HashSet

Es la implementación más usada de Set. Como su nombre indica, se implementa mediante un tabla hash. Una tabla hash básicamente es un array donde cada uno de sus elementos se asocia a una posición calculada a partir del propio contenido que se almacenará en dicha posición. En el caso concreto de Java, se emplea el valor hash devuelto por cada objeto (método hashCode( ) de Object) y se aplica una máscara sobre los bits menos significativos de dicho valor.

Importante: Dado que la posición que ocupa un elemento en la tabla hash depende de su propio contenido, y no del orden o instante en que se añadió a la tabla, no podemos garantizar el orden en que un iterador sobre la tabla nos devolverá su contenido (como veremos en el próximo ejemplo).

Ventaja:

  • Velocidad de acceso de lectura/escritura constante y rápido.

Desventaja:

  • No recomendable para recorrer todos los objetos de la colección.

Método HasCode()

Método de Object que podemos sobrescribir para cambiar el “id” de nuestros objetos. Es decir crear un hash único. Para una correcta sobrescritura de este método, hay que tener en cuenta estos aspectos:

  • Debe devolver un int. Si es necesario crear cast y/o parsear.
  • Debe tener la suficiente complejidad para que no se produzcan colisiones.
  • Es conveniente implementar el método equals: Este método viene a complementar al método hashCode y sirve para comparar objetos de una forma más rápida en estructuras Hash ya que únicamente nos devuelve un número entero. Cuando Java compara dos objetos en estructuras de tipo hash (HashMap, HashSet etc) primero invoca al método hashcode y luego el equals. Si los métodos hashcode de cada objeto devuelven diferente hash no seguirá comparando y considerará a los objetos distintos. En el caso en el que ambos objetos compartan el mismo hashcode Java invocará al método equals() y revisará a detalle si se cumple la igualdad. De esta forma las búsquedas quedan simplificadas en estructuras hash.
  • Se debe usar los mismos atributos para hashCode() como para equals().
  • Si hay algún cambio de valor en los atributos de equals(), el hashCode debe cambiar.
  • Al sobrescribir este método, dos objetos con el mismo contenido (iguales para nosotros) tiene que devolver el mismo hash: Probarlo!

Por tanto a partir de ahora si sobreescribimos el equals() debemos sobreescribir el hashCode() y viceversa.

Ejemplo:

class Money {
    
    private int amount;
    private String currencyCode;
    
    public Money(int dinero, String code) {
        this.amount = dinero;
        this.currencyCode = code;
    }
    
    public int getAmount() {
        return amount;
    }
    
    public String getCurrencyCode() {
        return currencyCode;
    }
    
    
    @Override
    public boolean equals(Object o) {
        
        if (o == this)
            return true;
        if (!(o instanceof Money))
            return false;
        Money other = (Money)o;
        boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
          || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
        return this.amount == other.amount && currencyCodeEquals;
    }
    
    @Override
    public int hashCode() {
        //Todos los strings tienen este método
        if (currencyCode != null)
            return currencyCode.hashCode() + amount;
        return amount + 3;
    }
    

Implementar este método en cada clase de una aplicación es tedioso, repetitivo y propenso a errores, para hacer más sencilla su implementación existe el método Objects.hash desde la versión 7 de Java. Un ejemplo para una clase que representa un número de teléfono quedaría así:

public class PhoneNumber {

    ...

    @Override
    public int hashCode() {
        return Objects.hash(areaCode, prefix, lineNumber);
    }
}

Creación, agregar y recuperar

Ejemplo básico de declararlo y agregar objetos:

 // HashSet declaracion
      HashSet<String> hset = 
               new HashSet<String>();

      // Agregar elementos al HashSet
      hset.add("Apple");
      hset.add("Mango");
      hset.add("Grapes");
      hset.add("Orange");
      hset.add("Fig");

      //Agregar (intentar) objetos duplicados
      hset.add("Apple");
      hset.add("Mango");

      //Esta permitido agregar valores nulos
      hset.add(null);

      System.out.println(hset);

Los demás métodos típicos de Collection se usan igual con esta colección, cabe destacar algún método de la teoría de conjuntos como el retainAll(): ¿Que hace?

HashSet<Integer> numbersValues = new HashSet<Integer>();
  
            numbersValues.add(4);
            numbersValues.add(6);
            numbersValues.add(8);
            numbersValues.add(10);
            numbersValues.add(12);
  
            System.out.println("Initial hashSet " + numbersValues);
  
            HashSet<Integer> numbersValuesToRetain = new HashSet<Integer>();
            arrset2.add(4);
            arrset2.add(6);
            arrset2.add(8);
  
            System.out.println("Values to retain" + numbersValuesToRetain);
            numbersValues.retainAll(numbersValuesToRetain);
  
            System.out.println("HashSet after retainAll " + numbersValues);

Hay más…

Ejercicio 1

Escribe un método denominado maxLength que acepte un HashSet de cadenas de caracteres y devuelva la longitud de la cadena más larga. Si el Set está vacío, devolverá 0

Escribe un método denominado hasOdd que acepte un Set de enteros y devuelva true o false dependiendo de si el Set contiene algún número impar o no.

Escribe un método denominado removeEvenLength que acepte un Set de cadenas de caracteres y elimine del Set todas las cadenas de longitud par.

Ejercicio 2

Modifica el ejercicio de agenda de arrayList, y usa ahora HashSet. Debes evitar que en la agenda se pueda agregar dos personas con el mismo número. Crea tu propio calculo de hash para los objetos Persona. ¿Cómo lo harías?

Implementación TreeSet

Almacenamiento basado en un árbol. Un árbol normalmente se implementa como una variante de lista enlazada en la que cada nodo puede contener 1 o más hijos. A medida que se van insertando elementos en la colección, estos se irán “colocando” de forma ordenada sobre el árbol. La estructura normal es de un arbol binario. Por defecto en orden ascendente. El TreeSet no mantiene el orden de inserción, los elementos se ordenan por orden natural. Y tampoco tiene objetos duplicados. Implementa estas dos interfaces NavigableSet y SortedSet, esto provoca métodos adicionales para odenación y recorrer la coleccción:

Metodos principales

Ejemplo:

import java.util.*;
public class SetDemo3 {
 
 public static void main(String[] args) {
    TreeSet<Integer> ts = new TreeSet<>();
    ts.add(4); ts.add(-1); ts.add(0);
    ts.add(2); ts.add(7); ts.add(5);
    ts.add(3); ts.add(3); ts.add(3); // Sólo almacenará uno
    System.out.println("ts: " + ts); // Estarán ordenados de menor a mayor
    System.out.println("Mayor: " + ts.last() + ", Menor: " + ts.first());
    System.out.println("Primer elemento mayor que 0: " + ts.higher(0));
    System.out.println("Primer elemento menor o igual que 6: " + ts.floor(6));
    NavigableSet<Integer> range = ts.subSet(-5, true, 3, false);
    System.out.println("Elementos en el rango [-5, 3): " + range);
 }
}

Recorrer un TreeSet

Para este tipo de colección podemos usar los “mecanismos” ya vistos (for, iterator, forEach), pero también podemos usar otra forma más de lambda stream() y Map:

 TreeSet<Integer> ts = new TreeSet<Integer>();

     ts.addAll(Arrays.asList(10, 61, 87, 39));

     System.out.print("TreeSet:");

     // Usando forEach
     ts.forEach(i -> System.out.print(i + " "));
     System.out.println();

     System.out.print("TreeSet:");

     // Usando stream con map (mapear cada elemento), el Collectors lo transforma en una lista
     System.out.print(
         ts.stream()
             .map(i -> String.valueOf(i)) .collect(Collectors.toList())
             );

Aclaración final

Hemos visto en TreeSet se hace una inserción y se ordena de manera ascendente, ¿que ocurre si son objetos complejos? –> Será necesario comparar y ordenar Prueba a agregar al TreeSet objetos complejos ¿qué ocurre?

Ejercicio 3

Las operaciones típicas de conjuntos son: union, diferencia e intersección. Haz un programa que prueba con los datos de los conjuntos A y B las tres operaciones indicadas. El resultado del programa debe ser el siguiente. Consulta el API para saber los métodos que debes usar para hacer las operaciones deseadas. A:[5, 7, 9, 19] B:[10, 20, 5, 7] A union B: [19, 20, 5, 7, 9, 10] A diferencia B: [19, 9] A intersección B: [5, 7]

Ejercicio 4

Crea un pequeño programa donde se inserten números aleatorios del 1 al 100 en una estructura de datos. Debemos insertar 50 elementos y que no se repitan.

Ejercicio 5

Crea un método que muestre el ultimo elemento de un TreeSet de enteros y lo elimine.

Ordenación y Comparación

Acabamos de ver que con TreeSets se insertan los objetos de manera ordenada ascendentemente, pero cuando usamos objetos más complejos que un Integer necesitamos establecer como queremos que los objetos se ordenen, una condiciones de ordenación que debemos establecer. Hay dos formas:

  • Que las clases usen interfaz Comparable.
  • O crear una clase Comparator.

Nota: Usaremos TreeSet para estos ejemplos pero es aplicable para casi todas las colecciones.

Implementando la interfaz Comparable

Dicha interfaz sólo define un método:

public interface Comparable<T> {
 int compareTo(T o);
}

Este método devolverá:

  • Negativo: this menor que “o” (parámetro de entrada del método).
  • Cero: this igual que “o”.
  • Positivo: this mayor que “o”.

Ejemplo:

import java.time.LocalDateTime;
public class User implements Comparable<User> {
    private int id; // id del usuario
    private String loginName; // login de inicio de sesión
    private LocalDateTime lastLogin; // último inicio de sesión
 
    User(int id, String loginName) {
        this.id = id;
        this.loginName = loginName;
    }
 
    public LocalDateTime getlLastLogin() { return this.lastLogin; }
    public void regSystemLogin() { this.lastLogin = LocalDateTime.now(); }
 
    @Override
    public String toString() {
        return "{" + this.id + ": " + this.loginName + ": " + this.lastLogin + "}";
    }

    @Override
    public int compareTo(User other) {
        // Comparamos los id de ambos usuarios. Podemos aprovechamos del propio método compareTo()
        // de la clase Integer pues la clase Integer implementa el interfaz Comparable
        return Integer.valueOf(this.id).compareTo(other.id);
    }
 
    @Override
    public boolean equals(User other) {
        return this.id == other.id;
    }

    @Override
    public int hashCode() {
        int result = Integer.hashCode(this.id);
        result = 31*result + this.loginName.hashCode();
        result = 31*result + ((this.lastLogin==null)? 0: this.lastLogin.hashCode());
        return result;
    }

Como observais en el anterior ejemplo también están implementados el hashCode() y el equals().

Creando un Comparator

De esta manera es necesario crear una clase a parte que implemente la interfaz Comparator. Nos ofrece más flexibilidad y posibilidades que la anterior forma. Y podemos usar esta clase para otras clases que compartan mismos criterios de ordenación. En lo que coincide con la anterior forma es que debemos implementar el método compare(), pero recibirá los dos objetos a comparar. En este ejemplo se ordenará por el último acceso a los usuarios:

import java.time.LocalDateTime;
class UserComparator implements Comparator<User> {
 @Override
 public int compare(User u1, user u2) {
    // Podemos aprovechamos del propio método compareTo() de la clase LocalDateTime
    // ya que implementa el interfaz Comparable
    return u1.getLastLogin().compareTo(u2.getLastLogin());
 }
}

Una vez creada la clase podemos “insertarla” en el constructor del TreeSet:

 TreeSet<User> users2 = new TreeSet<>(new UserComparator()); 

Pero también al método sort() de Collections y Arrays.

Uso de Collections.sort()

Por último también podemos utilizar un método de Collection que modifica la coleción, es el sort():

List<String> fruits = new ArrayList<String>();
fruits.add("Apple");
fruits.add("Orange");
fruits.add("Banana");
fruits.add("Grape");

Collections.sort(fruits);
System.out.println(fruits);

Con objetos “de verdad” seguimos utilizando el compareTo():

public class Fruit implements Comparable<Object>{
    private int id;
    private String name;
    private String taste;

    Fruit(int id, String name, String taste){
        this.id=id;
        this.name=name;
        this.taste=taste;
    }
    @Override 
    public int compareTo(Object o) {
        Fruit f = (Fruit) o; 
        return this.id - f.id ;
    }
}

//Uso el mismo
Collections.sort(fruitList);
fruitList.forEach(fruit -> {
    System.out.println(fruit.getId() + " " + fruit.getName() + " " + 
      fruit.getTaste());
});

El .sort() también acepta una clase Comparator:

class SortByName implements Comparator<Fruit> {
    @Override
    public int compare(Fruit a, Fruit b) {
        return a.getName().compareTo(b.getName());
    }
}
//Uso
Collections.sort(fruitList, new SortByName());

Método estático Compare

Los clases wrappers de java tienen un método estático para comparar, y se comporta de la misma manera que lo anteriormente visto. Ejemplo de uso con Integer:

int val1 = 200;
int val2 = 250;
int val3 = 200;
System.out.println(Integer.compare(val1, val2));
System.out.println(Integer.compare(val1, val3));

Ejercicio 1

Partiendo de la clase Articulo, crea una lista de objetos que los almacene y los ordene por el código del artículo:

class Articulo {
 String codArticulo;
 String descripcion;
 int cantidad;
 Articulo(String codArticulo, String descripcion, int cantidad) {
    this.codArticulo = codArticulo;
    this.descripcion = descripcion;
    this.cantidad = cantidad;
 }

Implememta todo lo necesario.

Luego implementa la ordenación de menos a más cantidad de los articulos.

Ejercicio 2

Añade al siguiente código el comparador necesario para que los teléfonos aparezcan ordenados (Strings). Primero aparecerán ordenados de mayor a menor los números locales (sin símbolo “+” delante) y después los internacionales, también ordenados de mayor a menor: Input: 981555555 34981565656 666666666 +34666666666

Resultado: 981555555 666666666 +34981565656 +34666666666

Podéis probar las dos formas que hemos visto de implementar la ordenación.

Ejercicio 3

Partiendo de una clase estudiante con nombre y edad. Nos piden agregarlo a una lista y que los objetos se ordenen primero por su nombre y luego por la edad (dos condiciones de ordenación). Usa Collections.sort().

Mapas

Mapas (Interfaz Map)

Los mapas en Java van a ser una estructura que almacene objetos a partir de una clave. Se introducen en las colecciones de objetos pero es la única que no depende de Collection. Ver -> diagrama

Por tanto para Java no es una colección como tal, pero el concepto es el mismo. Almacenar y organizar los objetos de un programa. Aspectos importantes:

  • Las claves deben ser únicas -> K
  • Los objetos (V) se almacenarán y se recuperán a partir de la clave.
  • Se basa en una estructura dinámica de tipo diccionario cuyos nodos, o entradas, son instancias de la clase interna Map.Entry<K,V>

Existen tres implementaciones principales de Map:

  • HashMap: permite valores nulos y no es predecible el orden de los objetos (tablas Hash). Conveniente para inserciones y recuperaciones.
  • LinkedHashMap: permite valores nulos y si es predecible el orden de los objetos. Si se va iterar con frecuencia es la mejor opción.
  • TreeMap: no permite valores nulos y si es predecible el orden de los objetos. El más usado, buen rendimiento en búsquedas, acceso y eliminación.

Los Mapas son muy utilizados cuando queremos velocidad de acceso a los objetos y actualizarlos.

Funciones básicas de Map

Creación e inserción de elementos

Método V put(K key, V value): añade o reemplaza la entrada del mapa de la clave K. Devuelve el objeto antiguo si la entrada existe, en caso contrario null. Ejemplo:

Map<Integer, String> mapHttpErrors = new HashMap<>();
 
mapHttpErrors.put(200, "OK");
mapHttpErrors.put(303, "See Other");
mapHttpErrors.put(404, "Not Found");
mapHttpErrors.put(500, "Internal Server Error");
 
System.out.println(mapHttpErrors);

Para volcar un mapa a otro tenemos el método putAll o también el constructor de Map:

Map<Integer, String> mapErrors = new HashMap<>(mapHttpErrors);

Recuperación de objetos

Método principal es el V get(Value k): devuelve el objeto de la clave, null si no existe Otros métodos muy prácticos son el size(), isEmpty(), y por supuesto:

  • boolean containsKey(Object k) Devuelve true si existe una entrada de clave k o false si no existe.
  • boolean containsValue(Object v) Devuelve true si existe una entrada de valor v o false si no existe.

Ejemplo:

String status301 = mapHttpErrors.get(301);
System.out.println("301: " + status301);

if (mapHttpErrors.containsKey("200")) {
    System.out.println("Http status 200");
}
if (mapHttpErrors.containsValue("OK")) {
    System.out.println("Found status OK");
}

String status301 = mapHttpErrors.get(301);
System.out.println("301: " + status301);

Actualizar o eliminar

Para eliminar tenemos:

  • remove(Object key): elimina la entrada al mapa, si existe devuelve el objeto eliminado, sino null.

Para reemplazar:

  • replace(Object Key, Object Value): cambia un objeto por otro a partir de la clave.

Recorrer un Map

Hay muchas formas para recorrer un mapa: Método keySet() y usando un Iterator:

Map<String, String> mapCountryCodes = new HashMap<>();
 
mapCountryCodes.put("1", "USA");
mapCountryCodes.put("44", "United Kingdom");
mapCountryCodes.put("33", "France");
mapCountryCodes.put("81", "Japan");
 
Set<String> setCodes = mapCountryCodes.keySet();
Iterator<String> iterator = setCodes.iterator();
 
while (iterator.hasNext()) {
    String code = iterator.next();
    String country = mapCountryCodes.get(code);
 
    System.out.println(code + " => " + country);
}

Método values() con el cual obtenemos una colección:

Collection<String> countries = mapCountryCodes.values();
 
for (String country : countries) {
    System.out.println(country);
}

El método entrySet(), nos devuelve un Set para recorrerlo es necesario Map.Entry:

Set<Map.Entry<String, String>> entries = mapCountryCodes.entrySet();
 
for (Map.Entry<String, String> entry : entries) {
    String code = entry.getKey();
    String country = entry.getValue();
 
    System.out.println(code + " => " + country);
}

Y por último con lambda y el forEach es muy cómodo:

mapCountryCodes.forEach(
    (code, country) -> System.out.println(code + " => " + country));

Ejemplo:

import java.util.*;
class MapaDemo2 {
    public static void main(String[] args) {
    
    LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
    map.put(1, "uno"); map.put(2, "dos"); map.put(3, "tres");
    System.out.println("Claves = " + map.keySet()); // set-vista de claves
    System.out.println("Valores = " + map.values()); // set-vista de valores
    System.out.println("Entradas = " + map.entrySet()); // set-vista de entradas
 
    Set<Integer> claves = map.keySet();
    for(Integer k: claves) { 
        System.out.println("clave: " + k + "-> val: " + map.get(k)); }
        Set<Map.Entry<Integer, String>> entradas = map.entrySet();
    
    for(Map.Entry<Integer, String> e: entradas) {
        System.out.println("clave: " + e.getKey() + "-> val: " + e.getValue());
     }
 }
}

Ejercicio 1

Tenemos la siguiente tabla de latitudes/longitudes de ciudades nacionales e internacionales:

CIUDAD LATITUD LONGITUD
LUGO 43.01 N 7.33 O
BARCELONA 41.23 N 2.11 E
MADRID 40.24 N 3.41 O
LIMA 12.03 S 77.03 0

Las coordenadas son obligatoriamente objetos de la siguiente clase, a la que añadirás los métodos necesarios class Coordenadas{ private latitud; private longitud; } Los datos de la tabla anterior se almacena obligatoriamente en un hashmap cuya clave es la capital, y el objeto que almacena es la coordenada. Tu programa debe:

  • Insertar los datos.
  • Recorrer el mapa (prueba de varias maneras).
  • Consultar y eliminar objetos.

Ejercicio 2

Crea una pequeña guía telefónica usando TreeMap. Los valores serán nombre y telefono. Funcionalidades:

  • Obtener el número a partir del nombre.
  • Actualizar número.
  • Mostrar todas las entradas de la guía.

Ejercicio 3

Tenemos almacenados en un TreeMap unos alumnos (nombre y nota media), la clave del mapa es el nombre del alumno. Nos piden que se almacene los alumnos por orden natural alfabético del alumno. Más tarde nos piden justo el orden inverso (prueba Collections.reverseOrder()) en el constructor del Treemap. También prueba los métodos firstkey(), lastkey() y headMap(), ¿que hacen?.

Genéricos

Introducción

Cuando hablamos de genérico nos referimos a “tipo genérico”. Es decir tipaje de objetos libre, no es necesario decir el tipo de clase que es o el tipo de dato que va a devolver un método. Con esta implicación se pueden crear Clases genéricas, métodos genéricos o incluso iterfaces genéricas. La idea principal de usar genéricos es ahorrar código y también generar código más flexible. La representación de algo genérico se expresa entre , la letra puede ser cualquiera y se suele representar en mayúsculas. Veamos un ejemplo:

Supongamos que deseamos definir un método que nos permita imprimir arrays de diferentes tipos de datos: enteros, caracteres, cadenas,… Hasta ahora, esto lo conseguíamos mediante la sobrecarga de métodos. Por cada tipo de dato, añadíamos a nuestra clase una nueva versión del método que esencialmente era la misma que las otras, con la salvedad de que el dato sobre el que operaba era diferente:

public class DemoGen1 {
 public static void imprimeArray(int[] arr) {
  for (int val: arr) System.out.printf("%s ", val);
    System.out.println();
 }
 public static void imprimeArray(double[] arr) {
  for (double val: arr) System.out.printf("%s ", val);
    System.out.println();
 }
 public static void main(String[] args) {
  double[] a1 = { 3.5, 2.0, 4, -1.67 };
  int[] a2 = { 5, 0, 4, -1 };
  imprimeArray(a1);
  imprimeArray(a2);
  }
}

La idea fundamental es, en lugar de indicar el tipo de dato, indicamos un tipo “genérico” (le llamaremos T) y realizaremos la implementación con él. Durante la ejecución, será como si dicho tipo genérico se “sustituyera” por el tipo correcto correspondiente:

public class DemoGen2 {
 
 public static <T> void imprimeArray(T[] arr) {
    for (T val: arr) System.out.printf("%s ", val);
        System.out.println();
}

 public static void main(String[] args) {
    Double[] a1 = { 3.5, 2.0, 4, -1.67 };
    Integer[] a2 = { 5, 0, 4, -1 };
    String[] a3 = { "mi", "casa, ", "teléfono " };
    imprimeArray(a1);
    imprimeArray(a2);
    imprimeArray(a3);
 }
}

Acabéis de ver un método genérico para imprimir un array, vamos ahora a por clases genéricas.

Nota importante: al usar métodos o clases con tipos de datos genéricos si se necesita usar tipos de datos primitivos se tiene que utilizar los wrappers correspondientes.

Clases genéricas

En la definición de una clase a la derecha del nombre de la clase con < > se indicará uno o varios tipos parametrizados (una lista de letras en mayúscula): [modif] class nombreClase<lista_tipos_param> [extends …] [implements …] { … }

public class ClaseGen<T> {
 T miVal; //se declara una variable tipo parametrizado

 //tipo T lo podemos usar en métodos sin problemas
 public ClaseGen(T val) {
 this.miVal = val;
 }
 public T getMiVal() {
 return this .miVal;
 }
 public String toString() {
 return "ClaseGen<" + this.miVal.getClass().getName() + ">: miVal = " + this.miVal;
 }
}
// En ejecución hay que indicar que tipos vamos a usar con esa clase genérica

public class DemoGen3 {
 public static void main(String[] args) {
    ClaseGen<Double> obj1 = new ClaseGen<Double>(23.75);
    double v1 = obj1.getMiVal();
    System.out.println("obj1 miVal = " + v1);
    ClaseGen<String> obj2 = new ClaseGen<String>("Hello!");
    String v2 = obj2.getMiVal();
    System.out.println("obj2 miVal = " + v2);
    System.out.println("obj1 = " + obj1);
    System.out.println("obj2 = " + obj2);
 }
}

Fíjate que al declarar una nueva variable de la clase genérica ClaseGen, debemos indicar el tipo “real” que reemplazará, en esa variable concreta, al tipo parametrizado (en este caso, Double). Al crear el objeto, todas las referencias internas de la clase en atributos, variables, métodos,… al tipo parametrizado T serán sustituidas por referencias al tipo indicado. No se pueden usar tipos primitivos.

Varios tipos parametrizados

Si por ejemplo, quisiésemos que el par de elementos fuesen de dos tipos, independientes el uno del otro, podríamos redefinir una clase Pair como:

class Pair<T, V> {
 private T first;
 private V second;
 public T getFirst() {
 return first;
 }
 public V getSecond() {
 return second;
 }
 public void setFirst(T first) {
 this.first = first;
 }
 public void setSecond(V second) {
 this.second = second;
 }
}

Ejercicio 1

Utilizando la definición de clase anterior, crea un main que pruebe estas posibilidades:

  • Un par de Integer y Float.
  • Un par de String y array de enteros.
  • Un par de Persona y Persona. Crea esa clase Persona.

Tipos acotados

Podemos restringir o acotar el tipo genérico en nuestras clases genéricas con el uso de herencia (extends): Por ejemplo, supón que quieres crear una clase genérica que almacene valores numéricos y sea capaz de realizar diferentes operaciones sobre ellos (calcular el inverso,… ). ¿Cómo podríamos restringir que sólo se pudieran crear objetos utilizando clases como Integer, Float o Double? Para ello usaremos una superclase, clase o interface que debe extender el tipo. En este ejemplo Number:

public class ClaseGen<T extends Number> {
   T miVal;
   public ClaseGen(T val) { this.miVal = val; }
   public T getMiVal() { return this.miVal; }
   public double getDoubleVal() {
      return this .miVal.doubleValue();
   }
   public double getMiValInverse() {
      return 1/this.miVal.doubleValue();
   }
   public String toString() {
      return "ClaseGen<" + this.miVal.getClass().getName() + ">: miVal = " + this.miVal;
 }
}

Hemos podido usar el método doubleValue() porque es un método de Number.

Métodos y constructores genéricos

Los métodos también pueden ser genéricos sin necesidad de que los tipos genéricos esten definidos al nivel de clase. Por ejemplo, imagina una clase Util que tiene muchos métodos. Sólo uno de esos métodos, el método imprimirArray() necesita un tipo genérico, no es necesario ni apropiado definir el tipo genérico a nivel de clase, se hace directamente en el método:

class Util {
    static <T> void imprimirArray (T[] t) {
        for(int i=0;i<t.length;i++)
            System.out.println(t[i].toString());
    }
}

De la misma forma que los métodos… los constructores pueden usar los tipos genéricos de la clase:

public class Pareja<K, V> {
   private K clave;
   private V valor;
   public Pareja(K clave, V valor) {
      this.clave = clave;
      this.valor = valor;
   }
 public K getClave() { return this.clave; }
 public T getValor() { return this.valor; }
 public void setClave(K clave) { this.clave = clave; }
 public void setValor(V valor) { this.valor = valor; }
 public String toString() { return "Pareja [" + this.clave + ", " + this.valor + "]"; }
}

Ejercicio 2

Añade a la clase genérica Pair un constructor de forma que al crear los pares se pueda pasar al constructor los valores de first y second.

Ejercicio 3

Crea un main para utiliza el método anterior (imprimirArray) de forma que imprime un array de Integer, un array de Double y un array de personas. Crea tu tres arrays sencillitos de las clases anteriores e invoca al método.

Ejercicio 4

Partiendo de la siguiente implementación de lista enlazada que debes modificarla para que trabaje con cualquier tipo de dato, no sólo entero. Pruébala desde un main y crea y recorre una listas de Persona y otra de Integer.

class Nodo{
  private Nodo sig;
  private int dato;
  public Nodo(int dato) {
    this.dato = dato;
    this.sig=null;
  }
  public Nodo(int dato, Nodo sig) {
    this.dato = dato;
    this.sig = sig;
  }
  public void setSiguiente(Nodo sig) {
    this.sig = sig;
  }
  public Nodo getSiguiente() {
    return sig;
  }
  public int getDato() {
    return dato;
  }
}
class MiListaEnlazada{
    private Nodo primero=null;
    public void insertar(int dato){
        if(primero==null){
            primero=new Nodo(dato);
        }else{
            Nodo temp= new Nodo(dato,primero);
            primero=temp;
        }
            
    }
    public int tamano(){
        int i=0;
        Nodo temp=primero;
        while(temp!=null){
            i++;
            temp=temp.getSiguiente();
        }
        return i;
    }
    public int obtener(int indice){
        Nodo temp=primero;
        int i=0;
        while(i<indice){
            temp=temp.getSiguiente();
            i++;
        }
            
        return temp.getDato();
    }
}

Inmutabilidad Extra

Definición de inmutabilidad

En programación, la inmutabilidad se refiere a la propiedad de un objeto cuyo estado no puede ser modificado una vez que ha sido creado. En Java, esto se logra haciendo que las instancias de una clase sean “inmutables”, lo que significa que sus campos no pueden cambiar después de que se hayan inicializado.

Clase inmutable

Una clase inmutable es simplemente aquella cuyas instancias no pueden ser modificadas una vez que su información ha sido definida. No habrá ninguna modificación a la misma durante su ciclo de vida. Para ello se usa el modificador final, y estos pasos a seguir:

  • No proporcionar ningún método que permita modificar el objeto (como p. ej. los setter).
  • Usar visibilidad privada para los atributos
  • Definir los atributos como final Aunque puede no ser estrictamente necesario, es conveniente, ya que también previene la modificación interna.
  • Declarar la clase como final.

Ejemplos:

public final class Persona {
    private final String nombre;
    private final int edad;

    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }

    public String getNombre() {
        return nombre;
    }

    public int getEdad() {
        return edad;
    }
}

final class Circulo { 

  final private Punto origen;
  final private double radio; 

  public Circulo(Punto p, double r) {
    origen = new Punto(p); 
    radio=r; 
  }
  public Punto getOrigen() {
    return new Punto(origen);
  }
  public double getRadio() { return radio; }
  
}

Métodos estáticos de Colecciones

  • List.of(…), Set.of(…) y Map.of(…).
  • Por su parte Java 10 añadió un nuevo método estático copyOf dentro de las interfaces List, Set y Map para facilitar la creación de colecciones inmutables a partir de una colección dada:
List.copyOf(lista);
Set.copyOf(conjunto);
Map.copyOf(mapa);

Usando Arrays.asList()

Arrays.asList() El método devuelve una lista de tamaño fijo respaldada por la array especificada. Dado que una array no se puede modificar estructuralmente, es imposible agregar elementos a la lista o eliminar elementos de ella. La lista arrojará un UnsupportedOperationException si se realiza alguna operación de cambio de tamaño en él. Sin embargo los objetos de la lista si que se pueden modificar. Ejemplo:

import java.util.Arrays;
import java.util.List;

class Main
{
	public static void main(String[] args)
	{
		String[] lang = new String[] { "C", "C++", "Java" };

		List<String> fixedLengthList = Arrays.asList(lang);

		try {
			// inmutable -> UnsupportedOperationException
			fixedLengthList.add("C#");

			System.out.println("List  : " + fixedLengthList);
		}
		catch (UnsupportedOperationException ex) {
			System.out.println("java.lang.UnsupportedOperationException");
		}

		// los objetos si se pueden modificar
		fixedLengthList.set(1, "Go");

		lang[2] = "JS";

        // cualquier cambio se ve reflejado, estamos con referencias
		System.out.println("Array : " + Arrays.toString(lang));
		// lambda
		fixedLengthList.forEach(e ->System.out.print(e.toString()+ " "));
		
	}
}

Crear un String si es inmutable

Una variable tipo String es inmutable. Por si queréis saber la razón -> enlace Es decir siempre que se crea uno y luego se modifica implica crear uno nuevo. Es decir lo que hace realmente hace Java es crear un String nuevo. Hay que pensar que un String es un array de caracteres, y como bien sabéis un array es una estructura estática.

String saludo = "Hola"; // Creamos la cadena con un contenido

saludo = saludo + " mundo"; // y le agregamos después una subcadena

Implica la creación de un nuevo objeto String como resultado. Cuando en la segunda línea extendemos el contenido original de la variable saludo, lo que ocurre es que se libera el objeto String original, el creado en la primera sentencia, y se crea otro nuevo para alojar el resultado de la operación. Dado que el tipo String es inmutable (no podemos modificar su contenido), cualquier operación de modificación sobre una variable de este tipo, como puede ser concatenar una cadena a otra o usar métodos como toUpperCase(), replace() o similares, implica la creación de un nuevo objeto String como resultado.

Si queremos crear programas más eficientes (en el caso en que se usen muchos Strings), la mejor solución es usar un “generador” de Strings mutable. Una posibilidad es usar StringBuilder:

StringBuilder saludo = new StringBuilder("Hola");
saludo.append(" mundo");

Si el programa va a tener concurrencia, lo más seguro será utilizar StringBuffer: enlace

No modificable

No modificable no nos permiten agregar, eliminar o cambiar objetos de una colección, pero cada objeto puede cambiar. Es decir no implica inmutabilidad

Se han agregado en Collections, metodos que nos aporta colecciones no modificables por ejemplo: Collections.unmodifiableSet() o Collections.unmodifiableCollection()

Ejemplo:

class Persona {
    private String nombre;
    private int edad;

    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }

    public String getNombre() {
        return nombre;
    }

    public int getEdad() {
        return edad;
    }

    public void setEdad(int edad) {
        this.edad = edad;
    }
}

public class Main {
    public static void main(String[] args) {
        // Crear un conjunto mutable
        Set<Persona> conjuntoMutable = new HashSet<>();
        conjuntoMutable.add(new Persona("Juan", 30));
        conjuntoMutable.add(new Persona("María", 25));
        
        // Crear un conjunto no modificable a partir del conjunto mutable
        Set<Persona> conjuntoNoModificable = Collections.unmodifiableSet(conjuntoMutable);

        // Intentar modificar un objeto en el conjunto no modifiable
        Persona juan = conjuntoNoModificable.iterator().next();
        juan.setEdad(40); // Esto modifica el objeto dentro del conjunto

        // Imprimir el conjunto no modifiable para mostrar que refleja los cambios en el objeto
        System.out.println("Contenido del conjunto no modifiable después de modificar un objeto:");
        for (Persona persona : conjuntoNoModificable) {
            System.out.println(persona.getNombre() + " - " + persona.getEdad());
        }

         // Intentar agregar un elemento al conjunto no modifiable
        try {
        	conjuntoNoModificable.add(new Persona("Carlos", 22)); // Esto lanzará UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("No se puede agregar elementos al conjunto no modifiable.");
        }
    }
}

Pura inmutabilidad

Crear clases inmutables + Estructura de datos no modificable

Diferencia entre no modificable e inmutable

Es sútil pero importante:

  • Una colección No modificable es como una vista, por tanto indirectamente puede ser modificada por otra refencia a la misma colección modicable (tal como habéis podido comprobar en el anterior ejemplo). Es simplemente una vista de sólo lectura de una colección. No se puede alterar directamente, pero si en el origen cambia también se verá alterado.

  • Sin embargo, una colección Inmutable, se considera una copia de sólo lectura de otra colección y no se puede modificar. Si la colección de origen cambia, la copia inmutable no se verá afectada.

Ejemplo:

ArrayList<String> arrayInicial = new ArrayList();
arrayInicial.add("Hola");
arrayInicial.add("Adios");
arrayInicial.add("Ciao");

List<String> arr = List.copyOf(arrayInicial);
// Coleccion inmutable 
arr.forEach(e -> System.out.println(e));
// Modificamos la colecion original
arrayInicial.add("Adeus");
System.out.println("*".repeat(5));
// Volvemos a recorrer la coleccion y no hay ningún cambio
arr.forEach(e -> System.out.println(e));
// La original
System.out.println("*".repeat(5));
arrayInicial.forEach(e -> System.out.println(e));

Conclusión

En muchos casos necesitaremos “elementos” que conserven los datos, por ejemplo mejorar la eficiencia. Situaciones como:

  • Patrones de diseño.
  • Programación funcional.
  • Programación concurrente (multihilo).

Bibliografía

https://luizcostatech.medium.com/java-lists-unmodifiable-vs-immutable-d670afc58106 https://www.baeldung.com/java-stream-immutable-collection https://gelopfalcon.medium.com/programaci%C3%B3n-funcional-con-java-inmutabilidad-ddab9d40df4a https://javaconceptoftheday.com/java-9-immutable-collections/ https://www.techiedelight.com/es/mutable-unmodifiable-immutable-empty-list-java/

Conclusión

Tomar la decisión de que colección elegir para tu arquitectura es difícil, y hay más factores de los que aparece en el diagrama pero puede servir de guía y es un buen resumen: Diagrama resumen

08.Ficheros

Ficheros

Introducción a los Ficheros

Introdución os ficheiros

Na programación adoitan usarse variables para almacenar información que queremos tratar durante a execución: os datos de entrada, resultados e valores intermedios Con todo, a información almacenada nas variables é volátil, desaparecendo cando remata a execución do programa. Cando queremos almacenar de xeito permanente, fálase de persistencia, sendo dúas as formas máis habituais de facelo:

  • Organizado a información nun ou varios ficheiros nun sistema de almacenamento persistente

  • Facendo uso das facilidades que nos permiten as bases de datos Neste capítulo introduciremos a primeira das opcións; e para iso, comezaremos abordando a súa definición desde o punto de vista de máis baixo nivel:

  • Un ficheiro é un conxunto de bits almacenado nun dispositivo accesible a través dunha ruta (pathname) que o identifica.

Basicamente, unha secuencia de 1s e 0s que son almacenados fóra da memoria volátil do equipo (nun disco duro ou nun DVD, por exemplo). Desde un nivel máis alto, e para o que nos interesa desde o punto de vista da programación, esta é a información que debemos ter en conta.

Tipos de ficheiros

E posible facer moitas clasificacións, que iremos detallando a continuación.

Segundo a súa estrutura de almacenamento e contido

Podemos atopar coma tipos básicos de ficheiros os de texto plano e os binarios.

Os de texto plano almacenan secuencias de caracteres correspondentes a unha codificación determinada (ASCII, Unicode, EBCDIC, etc.). Son lexibles mediante un software de edición de texto coma o Bloc de Notas de Windows ou os editores Vi ou nano de Linux.

Exemplos:

  • os ficheiros de texto con extensión .txt
  • os .csv de valores separados por comas
  • os .htm y .html correspondentes a páxinas web
  • os de linguaxes de marcas .xml ou .rss.

O sistema de codificación de caracteres máis popular é o código ASCII (American Standard Code for Information Interchange, código estándar estadounidense para intercambio de información), que define 256 caracteres distintos (todas as combinacións de 8 bits,dous elevado a 8 posibilidades). Algúns deles, chamados caracteres de control, encárganse de definir accións coma o borrado, o salto de liña ou o tabulador e non representan símbolos concretos. Podes consultar na wikipedia máis información.

Os binarios conteñen información codificada en binario para o seu procesamento por parte de aplicacións. O seu contido resulta ilexible nun editor de texto. Exemplos: executables (.exe) documentos de aplicacións (.pdf, .docx, .xlsx, .pptx) ficheiros de imaxe, audio ou vídeo (.jpg, .gif, .mp3, .avi, .mkv) librerías de sistema (.dll).

Por organización interna

Cando se utilizan ficheiros de texto plano para almacenar información pódense clasificar conforme a súa ‘‘organización interna’’ en secuenciais, de acceso directo ou aleatorio, ou indexados. Nos secuenciais a información escríbese en posicións fisicamente contiguas. Para acceder a un dato hai que recorrer todos os anteriores. Exemplo de ficheiro secuencial con información sobre clientes: 00789521T#Alba#Blanco#González#613524647$11111111H#Xosé Ramon#García#Pemán#613423824$38546998X#Xurxo#Cazás#Otero#666332245$09653801B#Antón#Resines#Pardo#619335241%

Por cada contacto estruturouse a información en cinco datos independentes: NIF, nome, primeiro apelido, segundo apelido e número de teléfono. Neste caso o programador decidiu utilizar o grade (#) como separador de datos, o dólar ($) como separador de contactos e o tanto por cento (%) coma marca de fin de ficheiro. Nos de acceso directo ou aleatorio cada liña de contido organízase cuns tamaños fixos de dato. Pódese acceder directamente ó principio de cada liña.

Nesta ocasión cada contacto ocupa unha liña do ficheiro (o final de cada unha o sistema operativo incluirá un ou dos caracteres de salto de liña invisibles para o usuario), e cada dato utiliza un número de caracteres fixo, aínda que non o ocupe totalmente (no exemplo reservanse 15 caracteres para o nome, aínda que no caso de Alba só se utilicen catro).

Finalmente, nos indexados, xeralmente nun ficheiro de acceso aleatorio a información almacénase na orde en que se da de alta. Aínda que se conseguira introducir a información de acordo a algún criterio de ordenación concreto, nalgunhas ocasións é útil poder ordenala por varios criterios distintos. No exemplo anterior é posible que necesitemos un listado de clientes ordenado por NIF e outro polo apelido. Para dar solución a este problema creouse a organización indexada, que consiste na existencia de un ou varios arquivos adxuntos que ordenan o dato (chamado clave) polo que se desexa ordenar o ficheiro e o relacionan coa localización da liña correspondente.

Por tipos de soporte de almacenamento

De “acordo á organización física dos datos”, diferenciamos entre dous tipos de soportes Nos secuenciais para acceder a un dato hai que recorrer todo o contido do soporte previo ó devandito dato (por exemplo, as cintas magnéticas). Nos direccionables pódese acceder directamente a un dato sen ter que recorrer todos os anteriores (por exemplo un disco duro). Nun soporte direccionable pódese implementar un acceso secuencial, directo ou indexado, mentres que nun soporte secuencial só se poderá implementar un acceso secuencial.

Por xerarquía de memorias

Os ficheiros adoitan almacenarse en diferentes memorias, tendo en conta que hai que atopar un compromiso entre a velocidade de acceso desexada, a capacidade requirida e o custo por bit. diagrama

E/S con fluxos

En Java defínese a abstracción de stream (fluxo) para tratar a comunicación de información entre o programa e o exterior. Entre unha fonte e un destino flúe unha secuencia de datos: os fluxos, que actúan coma interfaz co dispositivo ou clase asociada. Así permiten:

  • Operación independente do tipo de datos e do dispositivo
  • Maior flexibilidade (redirección, combinación, …)
  • Diversidade de dispositivos (ficheiro, pantallas, teclado, rede, …)
  • Diversidade de formas de comunicación (con modo de acceso secuencial ou aleatorio, e información intercambiada que pode ser binaria, caracteres ou liñas)

Analoxía entre UNIX e Java

Hai unha certa analoxía entre o funcionamento en Unix e en Java:

Unix Java
Entrada estándar Habitualmente teclado System.in
Saída estándar Habitualmente a consola System.out
Saída de erro Habitualmente a consola System.err

En Java accedese á E/S estándar a través dos campos estáticos da clase java.lang.System indicados.

Utilización dos fluxos

Lectura

  1. Abrir un fluxo a unha fonte de datos (creación do obxecto stream)
  • Teclado
  • Fichero
  • Socket remoto
  1. Mentres existan datos dispoñibles
  • Ler datos
  1. Cerrar o fluxo (método close)

Escritura

  1. Abrir un fluxo a unha fonte de datos (creación do obxecto stream)
  • Pantalla
  • Ficheiro
  • Socket local
  1. Mentres existan datos dispoñibles
  • Escribir datos
  1. Cerrar o fluxo (método close) A ter en conta: o sistema xa se encarga de abrir e pechar os fluxos estándar un fallo en calquera punto produce a excepción IOException Para o que estamos a tratar, serán de interese os File streams para escribir e ler datos en ficheiros

Exemplo de entrada de texto desde un ficheiro

 try {
    BufferedReader reader = new BufferedReader(new FileReader(nomeficheiro"));
    String linea = reader.readLine();
    while(linea != null) {
        // procesar o texto da liña
        linea = reader.readLine();
    }
    reader.close();
  }
  catch(FileNotFoundException e) {
  // non se atopou o ficheiro
  }
  catch(IOException e) {
  // algo foi mal ó ler ou pechar o ficheiro
  }

De xeito xeral, pódese comprender o funcionamento do código… pero para mellorar o seu entendemento, explicaremos que son as excepcións e como facer uso das mesmas.

Uso de finally:

import java.io.*;
 public class SimpleExceptionHandling {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("C:\\UnFicheiroTemporal");
        } catch (FileNotFoundException e) {
            System.out.println("Aconteceu a excepción  :: " + e.getMessage());
            System.out.println("Necesitamos pechar FileStream!");
        } finally {
            System.out.println("Afortunadamente pecharase neste bloque");
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
            } catch (IOException e) {
            }
        }
    }
 }

try-with-resources

Mecanismo que automaticamente pecha recursos (pode ser máis de un) cando se acabou o seu traballo con eles. Por exemplo:

private static void printFile() throws IOException {
    try(FileInputStream input = new FileInputStream("ficheiro.txt")) {
        int data = input.read();
        while(data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
 }

Neste exemplo ábrese un FileInputStream dentro dun bloque try-with-resources, lee datos do FileInputStream, e pechao automaticamente cando a execución sae do bloque. Isto último e posible porque FileInputStream implementa a interface java.lang.AutoCloseable

Exercicio 1

Partindo do seguinte código, simplificalo co uso de try-with-resources.

Scanner scanner = null;
try {
    scanner = new Scanner(new File("test.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

Clase File

Podes consultar na web de Oracle a documentación de la biblioteca estándar de clases de java.

Constructores

  • File(String ruta)
  • File(String ruta, String nombre)
  • File(File directorio, String nombre)

Metodos

  • canRead() e canWrite() : comproba se o ficheiro se pode ler/escribir
  • delete(): borra o ficheiro
  • exists(): devolve boolean si existe el fichero
  • getPath(): devolve a ruta do ficheiro
  • mkdir(): crea un directorio coa ruta do obxecto que o recibe. Hay una versión que es mkdirs() que se usa para crear una jerarquía de directorios
  • isDirectory() comproba se o ficheiro é un directorio

Pódese usar rutas absolutas como no siguinte exemplo pero es totalmente recomendable usar rutas relativas:

        File prueba = new File("c:\\midirectorio\\prueba.txt");
        if(!prueba.exists())
            System.out.println("c:\\midirectorio\\prueba.txt NO existe");
        if(!new File("c:\\midirectorio").exists())
            System.out.println("c:\\midirectorio NO existe");
        if(!new File("m:").exists())
            System.out.println("m: NO existe");
        
        if(new File("l:\\midirectorio\\prueba.txt").exists())
            System.out.println("l:\\midirectorio\\prueba.txt existe");

Exemplo de mkdirs:

        String directorio = "l:/micarpeta";
        String varios = "carpeta1/carpeta2/carpeta3";

        // Crear un directorio
        boolean exito = (new File(directorio)).mkdir();
        if (exito)
            System.out.println("Directorio: " + directorio + " creado");
        // Crear varios directorios
        exito = (new File(directorio+"/"+varios)).mkdirs();
        if (exito)
            System.out.println("Directorios: " + varios + " creados");

Podes atopar na coñecida web de referencia w3schools exemplos explicativos con código de manexo de ficheiros con estes métodos.
Con todo, dende Java SE 7, existe un xeito mellor de xestionar os ficheiros: java.nio.
Entón, se existe unha forma máis axeitada de xestionar os ficheiros en Java… por que aprendemos a utilizar esta? Porque debido o enorme número de aplicacións que usan a antigua API (o que se chame legacy), Oracle non ten plan de eliminar ou desaconsellar o seu uso nas vindeiras versións de Java. E por iso, porque podes atopar un montón de código en funcionamento o que lle tes que dar mantemento (ou facer migración), e que non vai desaparecer de xeito inminente, e bo coñecer esta API.

java.nio para xestión de ficheiros e directorios

Agora que xa coñecemos esta nova posibilidade de traballar con ficheiros, que permite contar con toda a funcionalidade que se acadaba antes, indagaremos sobre cales son as melloras que trae e como traballar coa novaAPI.

Melloras de java.nio

Cales son esas melloras na xestión? Por unha banda, permite facer unha mellor xestión de erros. Poñamos este código de exemplo

File file = new File("probas/titorial.txt");
boolean result = file.delete()

Compila e pode funcionar mais no caso de fallar… como saberemos se é porque o ficheiro non existe, ou é porque non temos permisos para borrar?
Reescribindo o código coa nova NIO2 API:

Path path = Paths.get("probas/titorial.txt");
Files.delete(path);

Agora será preciso xestionar unha IOException, que dará os detalles de que foi o que aconteceu (por exemplo, non ter permisos de borrado).
A nova versión tamén mellora o soporte ós metadatos e trae melloras de rendemento evitando problemas de insuficiencia de memoria.

Diferencias con file e como facer uso de java.nio

A primeira diferencia que salta rápido a vista dun programador é o cambio no nome do paquete e da clase (observables en letra grosa os cambios). Antes construíamos un obxecto file vía o construtor:

java.io.File file = new java.io.File("probas/titorial.txt");

Agora obteremos un Path usando un método static (get):

java.nio.file.Path path = java.nio.file.Paths.get("probas/titorial.txt");

Para crear ficheiros usaremos os métodos createNewFile() e Files.createFile():

boolean result = file.createNewFile();
Path newPath = Files.createFile(path);
//Para crear un directorio, mkdir() ou Files.createDirectory():
boolean result = file.mkdir();
File newPath = Files.createDirectory(path);

Hai variantes para incluir subdirectorios que non existan con mkdirs() e Files.createDirectories():

boolean result = file.mkdirs();
File newPath = Files.createDirectories(path);

Para renomear ou mover un ficheiro, precisamos crear outra instancia do obxecto e usar renameTo() ou Files.move():

boolean result = file.renameTo(new File("baeldung/tutorial2.txt"));
Path newPath = Files.move(path, Paths.get("baeldung/tutorial2.txt"));

Finalmente para borrar, usaremos delete() ou Files.delete():

boolean result = file.delete();
Files.delete(Paths.get(path));

Lembra que os métodos legacy devolven un flag cun result set a falso en caso de erro; e os métodos NIO2 devolve unha instancia de Path instance (agás a operación de borrado que lanza unha IOException en caso de erro).

Exercicio 1

Cal sería o código para saber se o indicado no path é u ficheiro? E se é posible escribir no mesmo? Amosa o código de xeito tradicional e coa nova API NIO.

Exercicio 2

En args[0] indicas un directorio e o programa imprime seu contido (no recursivo). Exemplo: imaxe funcionamiento Faino con File e NIO

Exercicio 3

Como no anterior pero agora o programa imprime o contido de forma recursiva. Hai varias maneiras unha é directamente coa clase File e utilizando o método utilidad fileList().

Creando un ficheiro

Podemos crear un ficheiro baleiro cos atributos que desexemos co método: createFile(Path, FileAttribute); Se non se especifican os atributos, será creado con atributos por defecto. E de xa existir o ficheiro indicado na ruta, lanzarase unha excepción. O seguinte código crea un ficheiro con atributos por defecto:

Path file = ...;
try {
    // Create the empty file with default permissions, etc.
    Files.createFile(file);
} catch (FileAlreadyExistsException x) {
    System.err.format("O ficheiro chamado  %s" +  " xa existe%n", file);
} catch (IOException x) {
    // Outro tipo de fallo, coma os relacionados con permisos.
    System.err.format("createFile da erro: %s%n", x);
}

Creando un directorio

Pódese facer usando o método:
createDirectory(Path, FileAttribute);
Se non se especifica ningún atributo, terá os que correspondan por defecto. Un exemplo:

Path dir = ...;
Files.createDirectory(path);

Mostrando o contido dun directorio

Pódese facer usando o método: newDirectoryStream(Path);
Este método devolve un obxecto que implementa a interfaz DirectoryStream, que a súa vez implementa Iterable.
Recorda que o devolto DirectoryStream é un fluxo, e que se non o chamas desde un try-with-resources debes pechar o fluxo no bloque finally.
A continuación un exemplo que amosa os contidos dun directorio:

Path dir = ...;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path file: stream) {
        System.out.println(file.getFileName());
    }
} catch (IOException | DirectoryIteratorException x) {
    // IOException nunca será lazanda pola iteracion neste exemplo
    // so pode ser lanzado por newDirectoryStream.
    System.err.println(x);
}

Borrando un ficheiro, enlace simbólico ou directorio

Debes ter en conta que para borrar un directorio este non debe ter contido, ou a operación de borrado fallará. Cando o que se borra é un enlace simbólico, o destino do enlace non será borrado.
A clase File facilita dous métodos para o borrado.
O método delete(Path) → borra o ficheiro ou lanza unha excepción se a operación falla (por exemplo, non existe a excepción ou non se gozan dos permisos requiridos.
Por exemplo:

try {
    Files.delete(path);
} catch (NoSuchFileException x) {
    System.err.format("%s: no such" + " file or directory%n", path);
} catch (DirectoryNotEmptyException x) {
    System.err.format("%s not empty%n", path);
} catch (IOException x) {
    // File permission problems are caught here.
    System.err.println(x);
}

O método deleteIfExists(Path) → a diferencia do anterior, non lanzará excepción se o ficheiro non existe (o cal pode ser útil cando se traballa con varios fíos).

Copiando un ficheiro ou directorio

Para copiar un ficheiro ou directorio existe o método: copy(Path, Path, CopyOption…);
A copia falla se o destino existe agás a opción REPLACE_EXISTING se atope indicada.
Os directorios poden ser copiados, pero debese ter en contacta que os ficheiros contidos na ruta orixe non o serán: o novo directorio sempre será creado baleiro.
O seguinte código exemplifica o funcionamento do método copy (recorda que será preciso importar o paquete con import static java.nio.file.StandardCopyOption.*):

Files.copy(orixe, destino, StandardCopyOption.REPLACE_EXISTING);
Existen outros métodos da clase Files que permiten facer a copia entre un ficheiro e un fluxo:

  • copy(InputStream, Path, CopyOptions…) → todos os bytes dun dun fluxo de entrada a un ficheiro
  • copy(Path, OutputStream) → todos os bytes dun ficheiro a un fluxo de saída

Movendo un ficheiro ou directorio

É posible mover un ficheiro e directorio usando move(Path, Path, CopyOption…)
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Neste exemplo empregouse a opción REPLACE_EXISTING que sobreescribe de xa existir na ruta de destino o ficheiro ou directorio

Exercicio 4

Investiga la interfaz Path para obtener estos datos de un ruta:

  • Ruta do directorio pai
  • Número de elementos que compoñe a ruta
  • Obter o nome do arquivo (se o ten)

Partindo doutra ruta para o destino, copia un fichero de una carpeta a outra. Finalmente borra o path orixe.

Extra: relación y conversión entre Path y File

Entre a Interface Path e a classe File existen mecanismos para obter unha representación dun tipo o outro. No JDK7 non é necesario realizar operacións de conversión complexas soamemte é necesario recurrir ó método .toFile de cada un de eles:

  • path.toFile(): Retorna un obxecto File representando sua ruta.
  • file.toPath(): Retorna un obxecto de tipo Path construido dende unha ruta abstracta. O obxecto Path resultante encontrase asociado co sistema de arquivos por defecto.

Ficheros de Texto

Java con ficheiros de caracteres

Como indica no primeiro apartado introdutorio os ficheiros de caracteres ou tamén chamados de texto están compostos de caracteres alfanuméricos nun formato estándar (ASCII, UNICODE etc…).

Lectura

Para ler caracteres necesitamos a clase FileReader, que se obtén a partir dun obxecto File.
A clase FileReader proporciona diversos métodos de lectura:

  • int read(): lee un carácter e o devolve. clase que define as operacións básicas de entrada de bytes con ficheiros.
  • int read(char [] buf): lee ata buf.lenght caracteres de datos dunha matriz de caracteres (buf). Os caracteres lidos do ficheiro vanse almacenando no buf.
  • int read(char[] buf, int desprazamento, int n): lee ata n caracteres de datos da matriz buf, comezando por buf[ desprazamento ] e devolve o número lido de caracteres. Estes métodos devolven -1 se chegou o final do ficheiro. Exemplo:
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class LerCaracteres {    
    public static void main(String args[])throws IOException {     
        File doc = new File("Demo.txt");    
        FileReader freader = new FileReader(doc);
        //segunda forma do metodo
        char [] i = new char[100];
        freader.read(i);
        for(char j : i)
            System.out.print(j);
        freader.close();
    }}

Todo estes métodos de lectura son de carácter a carácter ou agregando un desprazamento (int a int), pero … é a maneira máis práctica? Non, hai outras alternativas que detallamos a continuación.

Usando a clase BufferedReader

Forma máis cómoda de facer lectura de ficheiro de liña a liña.
Podes consultar máis información na API de Java SE 11
A clase BufferedReader ten un método que é readLine(). Este método devolve unha liña completa en String, ata o final do ficheiro que devolve null.
Usando esta clase temos opcións de máis alto nivel de abstracción, que facilitan a vida ó programador.
Para crear un obxecto BufferedReader hai que crealo a partires de FileReader:

File doc = new File("C:\\Drive\\Learn.txt");
BufferedReader obj = new BufferedReader(new FileReader(doc));

Fluxo de comunicacion Exemplo:

public class LerLineas {   
    public static void main(String[] args) throws FileNotFoundException, IOException {
     
        File doc = new File("prueba.txt");
        //Uso do try with resources, non fai falta usar o close 
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(doc))){
        String line = bufferedReader.readLine();
            while(line != null) {
                System.out.println(line);
                line = bufferedReader.readLine();
            }
}}}

Usando a clase Scanner

Esta clase permítenos ler ficheiros dunha maneira moi práctica (ademais do seu uso moi común de recoller datos do teclado).
Podes consultar máis información na API de Java SE 11.
O que hai que ter en conta é que necesitamos un obxecto tipo File para o construtor de Scanner. Existen outras posibilidades como crear un obxecto Scanner con InputStream. Seguiremos coa primeira forma pero coma sempre en programación… hai múltiples opcións!

File doc = new File(path);
Scanner obj = new Scanner(doc);

A clase Scanner ten dous métodos clave:

  • boolean hasNext(): que devolve verdadeiro se non chega o final do ficheiro, falso en caso contrario.
  • String hasNextLine(): obtemos a liña actual do ficheiro en cadea alfanumérica.

Exemplo:

public class LerLineasScanner {
    public static void main(String[] args) { 
        //obxecto file para Scanner
        File f = new File("demo.txt");
        String cadena;
        Scanner entrada = null;
        try {
            entrada = new Scanner(f);         //crease un Scanner asociado o ficheiro
            while (entrada.hasNext()) {       //mentres non se alcance o final do ficheiro                       
                cadena = entrada.nextLine();  //lese unha liña do ficheiro
                System.out.println(cadena);   //mostrase por pantalla                                           
            }
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } finally{
            entrada.close();                                                                                      
        }}}

Escritura

En Java unha das opción que nos permite escribir nun ficheiro é coa clase FileWriter.
Podes consultar máis información na API de Java SE 11
Esta clase nos proporciona unha serie de métodos:

  • void write(int c): escribe un carácter clase que define as operacións básicas de entrada de bytes con ficheiros.
  • void write(char[] buf): escribe un array de caracteres.
  • void write(char[] buf, int desplazamento, int n): escribe n caracteres de datos da matriz buf e comezando por buf[ desplazamento ].
  • void write(String str): escribe unha cadea de caracteres.
  • append(char c): engade un carácter a un ficheiro. Para crear obxectos tipo FileWriter é necesario ter un obxecto File, como pasa con FileReader:
File ficheiro = new File(fich.txt);
FileWriter fic = new FileWriter(ficheiro);

Ademais de coñecer os anteriores métodos é necesario ter en conta o seguinte: o obxecto tipo FileWriter por defecto vai sempre sobrescribir o contido. Se o que queremos é engadir debemos poñer no construtor un segundo parámetro a true:

FileWriter fic = new FileWriter (ficheiro, true);

Exemplo:

     //try with resource acepta multiples fontes de datos
try(FileWriter fw=new FileWriter("ficheiro1.txt"); FileReader fr=new FileReader("ficheiro1.txt")){  
            //Escribimos no ficheiro un String, un caracter 97 (a)
            // e array de caracteres
            fw.write("Esto es una prueb");
            fw.write(97);
            char[] buf = {'1','2','1'}; 
            fw.write(buf);
            //Gardamos os cambios do ficheiro, método que forza o guardado
            //para que se poida ler antes de pechar
            fw.flush();
            //Leemos o ficheiro e o amosamos por pantalla
            int valor=fr.read();
            while(valor!=-1){
                System.out.print((char)valor);
                valor=fr.read();
            }
        }catch(IOException e){
            System.out.println("Error E/S: "+e);
}}}

Nota: método flush() para gardar os cambios.

Usando a clase BufferedWriter

Como pasa coa lectura, Java ofrécenos clases para mellorar a eficiencia na escritura é métodos máis sinxelos para escribir liñas. Neste caso o BufferedWriter é unha clase que deriva da clase Write, e que se compón a partir de FileWriter.
Como estades vendo é unha serie de obxectos que “alimentan” o seguinte ata chegar o que precisamos.
Podes consultar máis información na API de Java SE 11.
Na clase BufferedWriter temos os seguintes métodos coma os máis interesantes:

  • void write(String cad): permitenos escribir cadea de alfanuméricos no buffer. Tamén acepta caracteres e arrays.
  • void newLine(): escribe un salto de liña no buffer.

Exemplo:

 // A partir de Java 9 hai unha mellora do try-with-resources
        // Desta forma cerrase o buffer cando termina, reducese a try
        BufferedWriter writer = new BufferedWriter(new FileWriter("test.txt"));
            try (writer) {         
                writer.write("Esto e unha linea");
                writer.newLine();
            }     
   }}

Usando a clase PrintWriter

Por último a clase PrintWriter é moi utilizada xa que ten un método chamado println() moi cómodo de utilizar:

  • void println(String cad): escribe a cadea de caracteres no ficheiro cun salto de liña. Outra das vantaxes desta clase que deriva de Write é que o construtor só precisa a ruta do ficheiro, polo tanto aforramos obxectos e liñas de código: Exemplo:
public class EscribirPrintWriter {    
    public static void main(String[] args) throws FileNotFoundException {      
        try {
            //se non existe se crea
            PrintWriter printWriter = new PrintWriter("test.txt");
            for (int i = 1; i < 4; i++) {
                //se sobreescribe o contido, de necesitar agregar debese usar apend
                printWriter.println("Esta e a linea numero: " + i);          
            }
            printWriter.close();
        } catch (Exception e) {
            e.printStackTrace();
    }}}

Esta clase tamén ten outros métodos como printf() (saída formateada), print() (escritura sen salto de liña) ou append() (para agregar contido).

Exercicio 1

Desenvolve un programa que amañe un ficheiro de texto, quitando espazos baleiros e poñendo a primeira letra de cada línea en maiúscula. Para esta labor crea un ficheiro temporal (investiga o uso do File.createTempFile), que logo sobrescribirá o ficheiro orixinal. Para detectar espazos y letras minúsculas pódese utilizar a clase Character.

Java NIO

Tal como vimos en anteriores apartados a API java.nio.file ofrécenos xeitos máis directos de poder traballar con ficheiros.
Podes consultar máis información na API de Java SE 11.
Neste caso con ficheiros de texto e sempre a partir da clase Files e sus métodos estáticos. Os máis usados son:

  • List < string > readAllLines(Path path, Charset ch): método estático que nos devolve un listado de strings que son as liñas do ficheiro (o charset é opcional). O sinxelo deste método e que péchase e libera o recurso automaticamente ó finalizar a lectura. (existe outra posibilidade con File.lines que amosaremos na sección de lambda)
  • Path write(Path path, Iterable lines, Charset cs, OpenOption… options): esta versión do método escribe liñas ó ficheiro. É necesario un obxecto iterable (cada elemento unha liña), a codificación que se vai usar é o modo de escritura (por defecto se non existe crease, e de existir vaise sobrescribir). Charset: o frecuente é StandardCharsets.UTF_8 OpenOption: para crear o ficheiro StandardOpenOption.CREATE (hai moitas posibilidades próbalas)

Exemplo:

public class EscribirLerNIO {    
    public static void main(String[] args) {      
        EscribirLerNIO eLN = new EscribirLerNIO();
        eLN.lerLineas();
        eLN.escribirLineas();
    }
    
    public void lerLineas(){ 
        Charset charset = Charset.forName("ISO-8859-1");
        try {
            //non nos preocupamos ni de abrir ni pechar ningun recurso
            List<String> lines = Files.readAllLines(Paths.get("prueba.txt"), charset);
            for (String line : lines) {
                System.out.println(line);
      }
    } catch (IOException e) {
        System.out.println(e);
    }}
      
    public void escribirLineas(){
        Iterable<String> iterable = Arrays.asList("line1", "line2");    
        try {
            Files.write(Paths.get("prueba_escribir.txt"), iterable);
            byte[] bytes = Files.readAllBytes(Paths.get("prueba_escribir.txt"));
            System.out.println(new String(bytes));
    } catch (IOException e) {
            System.out.println(e);
    }}}
String text = "Esto es una cadena de prueba";
Files.write(Paths.get("/examples/writeText.txt"), text.getBytes(StandardCharsets.UTF_8),
        StandardOpenOption.CREATE ...);

Ficheiros de acceso secuencial

O acceso secuencial é o máis frecuente cando se traballa con ficheiros, e xa coñecemos as características e vantaxes do acceso explicadas no primeiro apartado.
Neste tipo de acceso hai te que ter en conta que debemos coñecer o formato ou estrutura do ficheiro de antemán, e ir procesándoo.
Na labor de lectura ou escritura pódese traballar a nivel de bytes ou de caracteres. O seguinte exemplo amosa unha posible aplicación liña a liña de ficheiros de texto.
O exemplo é un pequeno programa de produtos e provedores dunha empresa.

Abrir/crear ficheiro

Xa detallado no apartado de ficheiros de texto.

String fichero = "produc.txt";
File f = new File(fichero);
FileWriter fw = new FileWriter(f);

Gardar

Unha vez aberto unha posibilidade para gardar é usar a clase PrintWriter. Podes consultar máis información na API de Java SE 11
O que temos que ter en conta é que o gardar creamos unha estrutura ríxida de información/rexistros, que logo vamos a utilizar para ler ou engadir máis rexistros.
Un exemplo de gardar os produtos:

PrintWriter escritura = new PrintWriter(fw);
for (int i = 0; i < menu.produtos.size(); i++) {
      escritura.println(menu.produtos.get(i).imprimeProduto());
}
escritura.close();

A información que temos que gardar está nunha colección de obxectos, e a orden de almacenamento dos datos está no método imprimeProduto():

public String imprimeProduto() {
     String saidaProducto = codProd + ":" + tipo + ":" + nome + ":" + prezo + ":" + pais + ":";
        for (int i = 0; i < this.getCodigoProveedor().size(); i++) {
            if (i != 0) {
                saidaProduto = saidaProduto.concat(",");
            }
       saidaProduto= saidaProduto.concat(this.getCodigoProveedor().get(i).getCodProv());
        }
        return saidaProduto;
    }

Ler en ficheiros

Amosaremos a posibilidade directamente sobre este exemplo no que se lee de maneira secuencial e envorca toda esta información en obxectos e estes a súa vez introdúcense nunha colección para xestionalos axeitadamente (neste caso ArrayList):

FileReader fr = null;
            BufferedReader br = null;
            fr = new FileReader("prov.txt");
            br = new BufferedReader(fr);
            String linea;
            String[] items;
            while ((linea = br.readLine()) != null) {
                items = linea.trim().split(":");
                menu.proveedores.add(new Proveedor(items[0], items[1], items[2].toUpperCase()));
            }
            br.close();

Exercicio 2

Disponse de dous ficheiros de texto “proveedores.txt” e “productos.txt”:

  • proveedores.txt: cada liña conten a información dun provedor. Esta componse dos seguintes campos separados por “:” codProv:nome:enderezo
  • productos.txt: cada liña conten a información dun produto. Esta componse dos seguintes campos separados por “:” codProd:tipo:nome:prezo[ :pais ]:codProv,codProv,codProv

Coma se pode observar, o quinto e o sexto campo da liña está formado a súa vez por unha lista que contén o código dos provedores que subministran o devandito produto, e estes códigos están separados polo carácter “,”.
Existen dous tipos de produtos: con “n” é nacional, e con “i” internacional. Se é internacional o producto terá un campo máis que é o país de orixe.
A partires destes ficheiros crear os obxectos precisos, cunha arquitectura O.O. correcta.
Crea un programa con estas funcionalidades:

  • Engadir un produto.
  • Obter un total do facturado por parte dun provedor.
  • Engadir provedor a un determinado produto.
  • Gardar os cambios nos correspondentes ficheiros. Posible interfaz:
void crearProducto(String codProd, String tipo, String nombre, double precio, String pais) throws Exception;
    
void imprimirProveedores();
    
void imprimirProductos();
    
boolean asignarProveedor(String codProd, String codProv) throws Exception;
    
Proveedor getProveedorByCod(String codProv);
    
void facturacion(String codProv) throws Exception; // muestra por pantalla la facturación total del proveedor

Ficheros de Bytes

Nesta actividade imos ver como traballar con un tipo de ficheiros, o de bytes de maneira máis clásica e logo usando Java NIO. Para a continuación ver o uso de datos primitivos de Java con ficheiros.

Java con Ficheiros de Bytes

O fluxo de bytes é a maneira máis eficiente de traballar con entrada e saída de bytes, e tamén a máis simple e de baixo nivel. Neste caso traballarase con bytes directamente que se almacenan nos ficheiros en formato binario.

imaxe1 imaxe2

Java ofrécenos o paquete básico de Entrada/Saída java.io. Partindo deste paquete temos as dúas clases para a manipulación de ficheiros con fluxo de bytes:

  • FileInputStream: clase que define as operacións básicas de entrada de bytes con ficheiros.
  • FileOutputStream: clase que define as operacións básicas de saída de bytes con ficheiros.

Java sempre crea un obxecto para asignarlle un fluxo de bytes. As anteriores clases herdan das clases abstractas InputStream e OutputStream respectivamente.

Operacións básicas:

  • Método read(): método da clase FileInputStream, que lee do fluxo de entrada un byte (8 bits) e devolve un int (tipo primitivo de java). Hai dúas posibilidades: Lectura normal: devolve un int (24 ceros á esquerda e 8 bits resultante da lectura). Lectura fin do stream: obtense o valor “-1”. int read(): Le un byte de data do fluxo de entrada.
  • Método write(): método da clase FileOutputStream, que escribe no ficheiro un byte pero sempre a partires dun int (cast forzoso quédase cos últimos 8 bits) void write (int b): o mesmo pero escribe o número de bytes ó fluxo de saída.
  • Método flush(): método que forza o gardado no stream o no writer de bytes. Exemplo de lectura e escritura:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FluxoBytes {
    public static void main(String[] args) {  
        //Obtéñese o directorio home do sistema onde almacenarase o ficheiro
        String directory = System.getProperty("user.home");
        String fileName = "sample.txt";
        String absolutePath = directory + File.separator + fileName;
         // escritura do ficheiro
        try(FileOutputStream fileOutputStream = new FileOutputStream(absolutePath)) {
         String fileContent = "This is a sample text.";
            //almacenamento de bytes
            fileOutputStream.write(fileContent.getBytes());
        } catch (FileNotFoundException e) {
            System.err.println("Ficheiro non encontrado");
        } catch (IOException e) {
            System.err.println("Error na E/S");
        }
        // lendo o ficheiro
        try(FileInputStream fileInputStream = new FileInputStream(absolutePath)) {
            int ch = fileInputStream.read();
            //ata a fin do stream
            while(ch != -1) {
                System.out.print((char)ch);
                ch = fileInputStream.read();
            }
        } catch (FileNotFoundException e) {
            System.err.println("Ficheiro non encontrado");
        } catch (IOException e) {
            System.err.println("Error na E/S");
        } }
}

Java NIO

A partires da versión 1.4 de Java existe un novo paquete de funcionalidades de entrada e saída que evita as deficiencias de java.io e fai máis sinxelo o uso de streams (métodos máis directos). Centrándonos en ficheiros, mellora o rendemento das operacións, aparecen habilidades asíncronas (chamadas sen bloqueo), etc… Operacións básicas:

  • Files.write: método estático de escritura de bytes. É necesario indicarlle a ruta, o contido a escribir e unha opción de escritura:clase que define as operacións básicas de entrada de bytes con ficheiros. Path Files.write(Path path,byte[] bytes, StandardOpenOption options) throws IOException Aspectos a ter en conta é que devolve a ruta do ficheiro a escribir. Para o terceiro argumento do método o máis frecuente é utilizar o enumerado StandardOpenOption, que ten múltiples opcións (enlace das opcións): hai que telas en conta para a forma de abrir o ficheiro.
  • Files.readAllBytes: método estático para ler desde un ficheiro e envorcalo a unha matriz de bytes. Necesita unha ruta, é cerra o ficheiro cando chega o final ou hai un erro de entrada ou saída. byte[] readAllBytes(Path path) throws IOException Exemplo do seu uso:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FluxoBytesNIO {
public static void main(String[] args) {
    String directory = System.getProperty("user.home");
    String fileName = "sample.txt";
    String content = "This is a sample text.";
    Path path = Paths.get(directory, fileName);
    try {
        Files.write(path, content.getBytes(), StandardOpenOption.CREATE);
    } catch (IOException e) {
        System.err.println("Error na E/S");
    }
    try { 
        byte[] data = Files.readAllBytes(path);
        System.out.println(new String(data));
        } catch (IOException e) {
        System.err.println("Error na E/S");
        }
}}

Interface Path e clase Paths: utilidades do paquete java.NIO para manexar rutas do sistema, no noso caso rutas de ficheiros. A clase Paths ten métodos estáticos que devolve a interface Path a partires dunha URI o cadea de caracteres. A interface Path é un argumento de diferentes métodos en java.NIO, que representa a fonte de información. Un posible uso co método readAllBytes() é usalo xunto co construtor de String. Se queremos obter unha cadea de alfanuméricos de todo o contido do ficheiro podes empregalo así:

String content = new String(Files.readAllBytes(Paths.get("readMe.txt")), StandardCharsets.UTF_8);

Manexo de datos primitivos en ficheiros

En ocasións temos a necesidade de utilizar datos primitivos en ficheiros; estes datos primitivos son os herdados de linguaxes máis clásicas e non se comportan como obxectos normais no paradigma orientación de obxectos. Son os seguintes:

  • byte
  • short
  • int
  • long
  • float
  • double
  • boolean
  • char

No uso con ficheiros cada tipo de dato primitivo ten a súa función de lectura e escritura, e é frecuente utilizar ficheiros binarios.

Escritura

O primeiro paso e obter o stream do ficheiro binario, do mesmo xeito que no apartado de “Java con ficheiros de bytes”: FileOutputStream fileOutputStream = new FileOutputStream(absolutePath);

Unha vez obtido o stream para a escritura (sexa por ruta de string ou obxecto da clase Path) é preciso crear un obxecto da clase DataOutputStream que recibe como parámetro un obxecto tipo FileOutputStream:

DataOutputStream salida = new DataOutputStream(fileOutputStream); Co obxecto DataOutputStream xa temos métodos de escritura por cada tipo de dato primitivo desta forma → writexXx() onde xXx é o nome do dato primitivo. Exemplo de escribir datos primitivos int (enteiro):

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;
public class DataOutputStreamInt {   
 public static void main(String[] args) {
        escribirInt();
    }
    //Uso de recursos e excepcións clásica sen try with resources
    private static void escribirInt() {
            
        Scanner sc = new Scanner(System.in);                                                                      
        FileOutputStream fos = null;
        DataOutputStream salida = null;
        int n;
        try {
            fos = new FileOutputStream("datos.dat");
            salida = new DataOutputStream(fos);
            System.out.print("Introduce número enteiro. -1 para rematar: ");
            n = sc.nextInt();
            while (n != -1) {
                salida.writeInt(n); //se escribe o número enteiro no ficheiro                                   
                System.out.print("Introduce número enteiro. -1 para rematar: ");
                n = sc.nextInt();
            }
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
                if (salida != null) {
                    salida.close();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());                                                               
            }
       }}
}

Lectura

Como pasa coa escritura, necesitamos dalgunha forma obter o Stream do ficheiro que imos ler: FileInputStream fileInputStream = new FileInputStream(String ruta);

Recordade que FileInputStream pódese obter tamén do obxecto File coma unha das formas máis usadas. A partires do obxecto anterior necesitamos crear un tipo DataInputStream, que é onde java nos ofrece os métodos de lectura. Un método de lectura por cada tipo de dato primitivo: readxXx() onde xXx é o nome do dato primitivo. Exemplo de ler datos primitivos int (enteiro):

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DataInputStreamInt {
      public static void main(String[] args) {
          lerInt();
    }
    //Uso de recursos e excepcións clásica sen try with resources
    public static void lerInt(){
        FileInputStream fis = null;
        DataInputStream entrada = null;
        int n;
        try {
            fis = new FileInputStream("datos.dat");
            entrada = new DataInputStream(fis);
            while (true) {   
                n = entrada.readInt();  //se lee  un enteiro do ficheiro                                           
                System.out.println(n);  //se mostra na pantalla
            }
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (EOFException e) {
            System.out.println("Fin de fichero");
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (entrada != null) {
                    entrada.close();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());                                                               
            }
        }
    }}

As vantaxes de utilizar ficheiros binarios con datos primitivos son a inmediatez e a non necesidade de conversión da datos (casting). Por outra banda non nos da a liberdade e flexibilidade dos ficheiros de texto. Temos que ter claro qué tipo de dato imos ler.

Exercicio 1

Desenvolve un programa en Java que realice unha copia byte a byte dun arquivo pasado por parámetros. O nome do novo ficheiro sería: proba.txt -> proba_Copia.txt

Exercicio 2

Desenvolve un programa en Java que crea unha matriz de elementos de tipo double e lee por teclado o valor dos seus elementos. A continuación escribe o contido da matriz nun ficheiro. O principio do ficheiro escríbense dous enteiros cos valores do número de filas e columnas da matriz.

Serialización

Nesta páxina imos ver como recuperar obxectos dun ficheiro e tamén como almacenalos nel.

¿Que é a serialización de obxectos?

A serialización é un proceso de transformación que nos permite codificar obxectos da nosa arquitectura orientada a obxectos. Esta codificación sírvenos para transmitir ou almacenar obxectos. No noso caso vamos a traballar nunha forma de almacenamento e recuperación de obxectos en ficheiros, pero a serialización tamén é usada en sockets, en modelos cliente servidor etc… e decir fluxos (stream).

Serializacion

A necesidade de serialización xurde no propio paradigma de programación orientada a obxectos. E moito máis cómodo e natural andar a manexar obxectos, que non datos primitivos ou cadeas de alfanuméricos obtidos de ficheiros. Entón Java permítenos gardar e recuperar obxectos en ficheiros binarios. Para a serialización de obxectos (que non é máis que escribir ou ler un obxecto completo) fai falla un obxecto serializable: unha secuencia de bytes que contén datos del propio obxecto e metadatos do mesmo. Estes metadatos son o tipo de obxecto que é e a sua arquitectura interna (datos primitivos ou referencias a outros obxectos). Para indicar a Java que un obxecto é serializable a sua clase ten que implementar a interfaz Serializable, desta forma:

Public class Agenda implements Serializable {
private String nombre;
private String p_Apellido;
private transient String s_Apellido;


public Agenda(String nombre, String p_Apellido, String s_Apellido){
this.nombre = nombre;
this.p_Apellido = p_Apellido;
this.s_Apellido = s_Apellido;
}}

Aspectos útiles a ter en conta na serialización:

  • Coa herdanza as clases fillo son tamén serializable se o son no pai.
  • Todo os atributos de clase tipo primitivo son serializables.
  • Se non se desexa serializar algún atributo úsase o modificador transient.
  • Unha interfaz non se pode serializar.
  • As colecciones de obxectos pódense serializar.
  • SerialVersionUID: é un identificador de clases cando serializamos os seus obxectos. Vai ser un atributo da clase de tipo long, estático e inmutable (final): private static final long serialVersionUID = 4L;

Para todo o proceso de serialización e deserialización temos dúas clases ObjectInputStream e ObjectOutputStream, que se expoñen a continuación.

Escritura e serialización de obxectos

Na escritura de un obxecto nun ficheiro necesitamos crear un fluxo de saída primeiro. O máis frecuente é coa clase FileOutputStream, a partires do fluxo xa podemos crear un obxecto ObjectOutputStream, que ten o método de escritura: void writeObject(Object o): método de escritura dun obxecto no fluxo que se lle pasa como parámetro. Exemplo:

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.NoSuchElementException;
import java.util.Scanner;
//Clase para serializar e gardar obxectos da clase Account
public class CreateSequentialFile
{
   private static ObjectOutputStream output; // obxecto con fluxo de saída   
   public static void main(String[] args)
   {
      openFile();
      addRecords();
      closeFile();
   }
   //método de abrir ficheiro
   public static void openFile()
   {
      try {
         output = new ObjectOutputStream(
            Files.newOutputStream(Paths.get("clients.ser")));
      }
      catch (IOException ioException)
      {
         System.err.println("Error na apertura de ficheiro. Pechando.");
         System.exit(1); 
      } 
   } 
   // método de agregar obxectos
   public static void addRecords()
   {
      
      boolean fin= false; 
      Scanner input = new Scanner(System.in);   
      System.out.println("Introduza o número de conta, primer nome, apelido e balance.");
      while (!fin) 
      {
         try 
         {                        
            // crease un novo obxecto; asúmese datos correctos
            Account record = new Account(input.nextInt(),
               input.next(), input.next(), input.nextDouble());
            // serializa e garda o obxecto no ficheiro
            output.writeObject(record); 
            System.out.println("Desexa continuar? S|N");
            String sair= input.next();
            if (sair.equalsIgnoreCase("N")) fin = true;   
         } 
         catch (NoSuchElementException elementException)
         {
            System.err.println("Datos incorrectos.");
            input.nextLine(); // descartamos datos de entrada 
         } 
         catch (IOException ioException)
         {
            System.err.println("Error na escritura. Pechando.");
            break;
         } 
      }
   } 
   //método de pechar ficheiro
   public static void closeFile() 
   {
      try 
      {
         if (output != null)
            output.close();
      } 
      catch (IOException ioException)
      {
         System.err.println("Erro pechando ficheiro. Pechando.");
      } } } 

Pojo do exemplo:

import java.io.Serializable;
// clase na que os seus obxectos gardaranse nun ficheiro
public class Account implements Serializable
{
   private int account;
   private String firstName;
   private String lastName;
   private double balance;   
   // inicializacion do obxecto cos valores por defecto
   public Account() 
   {
      this(0, "", "", 0.0);
   }   
   public Account(int account, String firstName, 
      String lastName, double balance)
   {
      this.account = account;
      this.firstName = firstName;
      this.lastName = lastName;
      this.balance = balance;
   }
   public void setAccount(int acct)
   {
      this.account = account;
   } 
   public int getAccount() 
   { 
      return account; 
   }    
   public void setFirstName(String firstName)
   {
      this.firstName = firstName;
   } 
   public String getFirstName() 
   { 
      return firstName; 
   }    
   public void setLastName(String lastName)
   {
      this.lastName = lastName;
   } 
   public String getLastName() 
   {
      return lastName; 
   }    
   public void setBalance(double balance)
   {
      this.balance = balance;
   } 
   public double getBalance() 
   { 
      return balance; 
   } 
} 

Lectura e deserialización de obxectos

A lectura é moi semellante a escritura; é necesario crear un obxecto de lectura de fluxo co FileInputStream. Despois xeramos ObjectInputStream que ten o método de lectura: Object readObject(): obtense un obxecto do ficheiro e son deserializados, de maneira secuencial, un detrás doutro. Exemplo:

import java.io.EOFException;     
import java.io.IOException;      
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
//Clase para deserializar e ler obxectos clase Account do ficheiro
public class ReadSequentialFile
{
   private static ObjectInputStream input;
   public static void main(String[] args)
   {
      openFile();
      readRecords();
      closeFile();
   } 

   // abrir ficheiro
   public static void openFile()
   {
      try 
      {
         input = new ObjectInputStream(          
            Files.newInputStream(Paths.get("clients.ser")));
      } 
      catch (IOException ioException)
      {
         System.err.println("Erro abrindo ficheiro.");
         System.exit(1);
      } 
   }
   // método de ler obxectos
   public static void readRecords()
   {
      System.out.printf("%-10s%-12s%-12s%10s%n", "Conta",
         "Primer nome", "Apelidos", "Balance");
      try 
      {
         while (true) // bucle ata EOFException
         {
            // lectura e cast
            Account record = (Account) input.readObject();
            // mostramos os datos
            System.out.printf("%-10d%-12s%-12s%10.2f%n",  
               record.getAccount(), record.getFirstName(), 
               record.getLastName(), record.getBalance());
         } 
      }
      catch (EOFException endOfFileException)
      {
         System.out.printf("%nNon hai mais datos.%n");
      } 
      // Excepcion que xorde cando nos chega un obxecto que non esperamos
      catch (ClassNotFoundException classNotFoundException)
      {
         System.err.println("Tipo de obxecto inválido.");
      } 
      catch (IOException ioException)
      {
         System.err.println("Erro lendo o ficheiro.");
      } 
   } 
   // método de peche
   public static void closeFile()
   {
      try
      {
         if (input != null)
            input.close();
      } 
      catch (IOException ioException)
      {
         System.err.println("Error closing file. Terminating.");
         System.exit(1);
      } 
   } } 

Problema na sobrescritura de ficheiros na serialización

Se intentas abrir e agregar novos obxectos nun ficheiro xa utilizado, é decir agregar obxectos, o máis probable é que cando queras ler che apareza o seguinte erro: Error → IOException: invalid type code: AC É provocado porque se crea unha cabeceira extra sempre que se abre o ficheiro en modo escritura. Esa cabeceira ten a información dos metadatos dos obxectos almacenados e non pode estar duplicada. Para arranxar este problema hai que programar un propio ObjectOutputStream e sobrescribir o método writeStreamHeader:

import java.io.*;
//clase de proba
class Persona implements Serializable{
    String nombre;
    int edad;
 
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
}
// Creamos o noso ObjectOutputStream
class MiObjectOutputStream extends ObjectOutputStream{
    /** Constructor que recibe OutputStream */
    public MiObjectOutputStream(FileOutputStream out) throws IOException{
        super(out);
    }
    //Re definición do método de escribir a cabeceira para que non faga nada. */
    @Override
    public void writeStreamHeader() throws IOException{
    }
   }
class SobreEscrituraSer{ 
    public static void main(String[] args) throws IOException, ClassNotFoundException{      
        //Primeira escritura normal
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("persoas.dat"));
        Persona p;
        p = new Persona("Juan Lopez", 30);
        oos.writeObject(p);
        oos.close();     
        //Aa segunda e de adelante usamos noso MiobjectOutputStream
        //Parametro true para a agregación de objetos
         MiObjectOutputStream moos = new MiObjectOutputStream(new FileOutputStream("persoas.dat",true));
        p = new Persona("Jose Fernandez", 28);
        moos.writeObject(p);
        moos.close();    
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("persoas.dat"));
        p=(Persona)ois.readObject();
        System.out.println(p.nombre+", "+p.edad);
        p=(Persona)ois.readObject();
        System.out.println(p.nombre+", "+p.edad);
        ois.close();
 }
}

Aspectos importantes a ter en conta na serialización

  • Os datos primitivos sempre son serializables, non fai falta implementar a interfaz.
  • Si hai composición de clases, todas as clases implicadas deben ser serializables:
import java.io.*;
class Punto{
    int x;
    int y;

    public Punto(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
class Rectangulo implements Serializable{
    Punto origen;
    int base;
    int altura;
    Rectangulo(int x, int y, int base, int altura){
        origen=new Punto(x,y);
        this.base=base;
        this.altura=altura;
    }
    public String toString(){
        return origen.x+","+origen.y+","+base+","+altura;
    }

}
class Unidad6{
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangulos.dat"));
        Rectangulo r;
        r = new Rectangulo(0,0,2,3);
        oos.writeObject(r);
        r = new Rectangulo(2,3,10,10);
        oos.writeObject(r);
        oos.close();
        
    }
}

Xenera este error: Exception in thread “main” java.io.NotSerializableException: Punto

  • En herencia é suficiente que a clase pai ou base sexa serializable para que os fillos o sexan.
  • Menos as interfaces todas as coleccións típicas son serializables.
  • Si un atributo da clase non desexas que se serialice (motivos de seguridade por exemplo), palabra reservada transient.
  • Podemos xenerar un id para a serialización das clases. Chámase SerialVersionUID é o mellor que sexa tipo long, static e final. Proba a xeneralo dende o IDE ou podes asignalo a man así:
private static final long serialVersionUID = 1L;

Exercicio 1

Crea un programa que almacene nunha lista obxectos Employee e se almacene nun ficheiro. Logo debes recuperar esa lista de obxectos do ficheiro. Employee.java:

import java.io.Serializable;
// Sempre aínda que se introduza nunha colección
public class Employee implements Serializable {
    String id;
    String firstName;
    String lastName; 
    public Employee(String id, String firstName, String lastName) {
        super();
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Override
    public String toString() {
        return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]";
    }
}

Exercicio 2

Partindo de este exercicio de mapas enlace Serializa o mapa cos datos e gárdaos nun ficheiro. A continuación deserealiza dende o ficheiro e lee o mapa.

Exercicio 3

Temos que gardar obxectos dunha clase persoa co serialVersionUID, password e nome. Pero nos dicen que por motivos de seguridad o campo password no se pode gardar. Fai a proba de gardar e despois ler do ficheiro.

Exercicio extra (podeis usar coleccións)

Quérese gardar información sobre animais para realizar un xogo infantil. Os animais a mostrar no xogo son cans ou gatos.

A información que se quere gardar é o nome e do animal e a sua idade.

No caso dos cans, quérese saber se teñen o rabo longo ou non, e no caso dos gatos se teñen bigotes longos ou non.

Tamén se desexa saber a forma de expresarse de cada un. Os gatos farán “MIAU” e os cans “GUAU”.

Vamos a necesitar un método (getTipo) que nos indique que clase de animal é (can ou gato) xa que haberá momentos no que traballaremos con animais.

Unha vez definidas as clases para gardar a información anterior, crea una clase de nome OperacionesAnimales: Dicha clase poderá gardar información sobre cinco Cans e cinco gatos. Debes facer uso de arrays estáticos de tamaño 5. Disporá dun método addPerro(Perro perro) que permitirá agregar un perro (dos cinco posibles). Necesitarás un contador que che indique en qué posición do array debes de añadir o perro novo. Dicho método devolverá un boolean indicando se o can foi engadido correctamente. Se intentamos engadir un novo can e o array está ocupado, devolverá false.

O mesmo para o caso dos gatos método (addGato(Gato gato)):

  • Disporá dun método gardarAnimaisADisco(String fichero) no que se gardarán todos os obxectos que se encontren no array (perros / gatos). Escribiremos no ficheiro, antes dos obxectos, o número de obxectos total que imos a escribir.
  • Disporá dun método lerAnimaisDisco(String fichero) que lerá de disco os obxectos gardados do paso anterior. Primeiro leremos o número de animais gardados no disco para dar memoria o array que debe devolver o método. Despois leremos todos os animais que iranse gardando no array que temos que devolver. Se actualizarán os datos da clase (o array que gardan os gatos e os cans).

Lambda con Ficheros

En cada nova versión de Java amplíase o uso de lambda e expresións de programación funcional: actualízanse así as posibilidades de Java para ser unha linguaxe máis directa e menos “verbosa”: resumindo, menos liñas de programación para acadar o mesmo.

icono lambda

Coa API java.NIO temos a clase Files, que se pode utilizar con expresións lambda. A clase Files xunto coa clase Stream crean un binomio moi útil para operacións con ficheiros.

Ler un ficheiro de maneira secuencial

Path file = Paths.get("readFile.txt");
try(Stream<String>lines = Files.lines(file)
    .onClose(() -> System.out.println("Fin de lectura"))) {
        lines.forEach(System.out::println); }

Unha vez que se obtén a ruta do ficheiro utilizamos a método estático lines() de Files. Este método devólvenos un fluxo de traballo (Stream), que ten unha directiva cando se pecha o fluxo (onClose) e un método para percorrer todas as liñas (forEach). A clase Stream pódese “alimentar” de moitas formas: se pretendemos unha maior eficiencia un obxecto BufferedReader debe ser o elixido. Files ten un método no que se obtén un buffer de lectura (newBufferedReader):

try(BufferedReader br = Files.newBufferedReader(file);
    Stream<String> lines = br.lines()
        .onClose(() -> System.out.println("Fin de lectura de ficheiro."));){
            lines.forEach(System.out::println);
}

Ler con procesamento paralelo

Outra opción que nos ofrece Stream e onde non nos ten que importar o tratamento secuencial do ficheiro é o predicado parallel(). O que fai esta opción é crear fíos de execución sobre o contido do Stream (neste caso o ficheiro), mellorando o rendemento:

try(Stream<String>lines = Files.lines(file)
        .parallel()
        .onClose(() -> System.out.println("Fin de lectura de ficheiro."))) {
        //modificamos esta parte para poder observar os fíos que se xeran
        lines.forEach(s ->{
        System.out.println(s +" " + Thread.currentThread().getName());});

Exemplo:

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream; 
public class LerFicheiroLambda { 
    public static void main(String[] args) throws IOException {    
        Path file = Paths.get("probas.txt");  
        // Uso do try-with-resources con lambda
        try(Stream<String>lines = Files.lines(file)
                        .onClose(() -> System.out.println("Fin de lectura de ficheiro."))) {
            lines.forEach(System.out::println);
        }  
        //versión más eficiente utilizando BufferedReader
        System.out.println("Version mais eficiente....");   
        try(BufferedReader br = Files.newBufferedReader(file);
            Stream<String> lines = br.lines()
                    .onClose(() -> System.out.println("Fin de lectura de ficheiro."));){  
            lines.forEach(System.out::println);
        }       
        //versión de procesamento pararelo
        System.out.println("Version con procesamento paralelo....");
         try(Stream<String>lines = Files.lines(file)
                        .parallel()
                        .onClose(() -> System.out.println("Fin de lectura de ficheiro."))) {    
            //modificamos esta parte para poder observar os fíos que se xeran
             lines.forEach(s ->{
                    System.out.println(s +" " + Thread.currentThread().getName());});
        }      
    }
}

Lectura condicional

Stream ten un predicado que é filter(): permítenos filtrar e quedar coas liñas que nos interesan do ficheiro. Ten múltiples opcións; a continuación amosamos un exemplo sinxelo:

try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {
    long i = lines.filter(line -> line.startsWith("T"))
    .count();

Filter necesita un predicado lóxico booleano, ou utilizar métodos de Stream coma os seguintes:

  • contentEquals(String st)
  • endsWith(String sufix)
  • startsWith(String prefix)
  • matches(String red): expresións regulares.
  • … Exemplo:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class LerFilter {        
    public static void main(String[] args) throws IOException {      
        //Exemplos do uso do filter     
        try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {
            long i = lines.filter(line ->line.startsWith("T"))
            .count();
        System.out.println("Numero de lineas que empezar por 'T' e " + i);
    }                
        try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {
            long i = lines.filter(line ->line.isEmpty())
            .count();
        System.out.println("Número de lineas vacias: " + i);
    }            
    try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {  
            lines.filter(line ->line.endsWith("11"))
            .forEach(System.out::println);
    }
}}

Uso de lambda para operacións de directorios e arquivos de sistema

Para listar os directorios dunha ruta, temos a directiva isDirectory de Files:

try (Stream<Path> paths = Files.list(Path.of(folderPath))) {
    paths.filter(Files::isDirectory)
    .forEach(System.out::println);
}

Fíxate que devolve un Stream de path é dicir rutas de ficheiros. Para listar os ficheiros dunha ruta:

Para percorrer todo a árbore de directorios

Coma se fora de xeito recursivo Files ten un método estático chamado walk() que devolve un stream con todos os elementos da carpeta, das subcartafoles etc. ata chegar a final da árbore. O stream estará formado por todos os camiños que forma esa árbore.

Stream<Path> miStream = Files.walk(Paths.get("./src"));
miStream.forEach(System.out::println);

Búsqueda de ficheiros

A clase Files ten un método de busca de ficheiros. O uso máis frecuente é con tres argumentos de entrada:

  • Path p: obxecto que representa a ruta onde se vai buscar.
  • int maxDepth: enteiro que representa que nivel de busca se precisa na árbore de directorios. Se indicamos Integer.Maxvalue sería ata o final da árbore de ficheiros, se indicamos “1” sería o primeiro nivel.
  • BiPredicate <Path, BasicFileAttributes> matcher: é unha interfaz funcional que serve para indicar as condicións de busca.
try (Stream<Path> pathStream = Files.find(path, Integer.MAX_VALUE,
    (p, basicFileAttributes) ->
    p.getFileName().toString().equalsIgnoreCase(fileName))

Exemplo:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class BusquedaFicheiroLambda { 
    public static void main(String[] args) throws IOException {    
        Path path = Paths.get("/Users/jmor/NetBeansProjects/AccesoDatos_licencia");
        List<Path> result = busquedaPorNombre(path, "pom.xml");
        result.forEach(x -> System.out.println(x));    
        List<Path> result2 = busquedaPorExtension(path, ".txt");
        result2.forEach(x -> System.out.println(x));     
    }   
    //Metodo de busca usando .find con Stream
    public static List<Path> busquedaPorNombre(Path path, String fileName)
            throws IOException {
        //listado que xenera a busqueda
        List<Path> result;    
        //find ten tres atributos de entrada
        //a interfaz funcional fai ignorar as maiúsculas y minúsculas
        try (Stream<Path> pathStream = Files.find(path,
                Integer.MAX_VALUE,
                (p, basicFileAttributes) ->
                        p.getFileName().toString().equalsIgnoreCase(fileName))
        ) {
            result = pathStream.collect(Collectors.toList());
        }
        return result;
    }   
    //método de busca por extensión 
    public static List<Path> busquedaPorExtension(Path path, String ext)
            throws IOException {
        //listado que xera a busca
        List<Path> result;      
        //a interfaz funcional filtra por ficheiros (no directorios) y que
        // tena a extension
        try (Stream<Path> pathStream = Files.find(path,
                Integer.MAX_VALUE,
                (p, attr) -> 
                   attr.isRegularFile() && p.toString().endsWith(ext))
        )
        {
            result = pathStream.collect(Collectors.toList());
        }
        return result;
    }    
}

Exercicio 1

  • Crea un método de buscade de arquivos con Files.find a partires dunha ruta cun determinado tamaño (Files.size) usando expresións lambda. Mellora o teu programa detectando se un ficheiro é un directorio (Files.isDirectory), e devolvendo falso.
  • Crea un método que busque un ficheiro nun directorio e en todas os seus subcartafois (ata o final da árbore de directorios) usando lambda. Para ser máis eficiencia usa un filtro con Files::isRegularFile.

09.Bases de Datos

Bases de Datos

Introducción a JDBC

Hoy en día, la mayoría de aplicaciones informáticas necesitan almacenar y gestionar gran cantidad de datos.

Esos datos, se suelen guardar en bases de datos relacionales, ya que éstas son las más extendidas actualmente.

Las bases de datos relacionales permiten organizar los datos en tablas y esas tablas y datos se relacionan mediante campos clave. Además se trabaja con el lenguaje estándar conocido como SQL, para poder realizar las consultas que deseemos a la base de datos. Una base de datos relacional se puede definir de una manera simple como aquella que presenta la información en tablas con filas y columnas.

Una tabla es una serie de filas y columnas, en la que cada fila es un registro y cada columna es un campo. Un campo representa un dato de los elementos almacenados en la tabla (NSS, nombre, etc.) Cada registro representa un elemento de la tabla (el equipo Real Madrid, el equipo Real Murcia, etc.) No se permite que pueda aparecer dos o más veces el mismo registro, por lo que uno o más campos de la tabla forman lo que se conoce como clave primaria.

El sistema gestor de bases de datos, en inglés conocido como: Database Management System (DBMS), gestiona el modo en que los datos se almacenan, mantienen y recuperan. En el caso de una base de datos relacional, el sistema gestor de base de datos se denomina: Relational Database Management System (RDBMS). Tradicionalmente, la programación de bases de datos ha sido como una Torre de Babel: gran cantidad de productos de bases de datos en el mercado, y cada uno “hablando” en su lenguaje privado con las aplicaciones.

Java, mediante JDBC (Java Database Connectivity), permite simplificar el acceso a base de datos, proporcionando un lenguaje mediante el cual las aplicaciones pueden comunicarse con motores de bases de datos. Sun desarrolló este API para el acceso a bases de datos, con tres objetivos principales en mente:

  • Ser un API con soporte de SQL: poder construir sentencias SQL e insertarlas dentro de llamadas al API de Java.
  • Aprovechar la experiencia de los APIs de bases de datos existentes.
  • Ser sencillo.

JDBC

De JDBC podemos decir que:

  • Consta de un conjunto de clases e interfaces escritas en Java.
  • Proporciona un API estándar para desarrollar aplicaciones de bases de datos con un API Java pura.

Con JDBC, no hay que escribir un programa para acceder a una base de datos Access, otro programa distinto para acceder a una base de datos Oracle, etc., sino que podemos escribir un único programa con el API JDBC y el programa se encargará de enviar las sentencias SQL a la base de datos apropiada. Además, y como ya sabemos, una aplicación en Java puede ejecutarse en plataformas distintas.

En el desarrollo de JDBC, y debido a la confusión que hubo por la proliferación de API’s propietarios de acceso a datos, Sun buscó los aspectos de éxito de un API de este tipo, ODBC (Open Database Connectivity). ODBC se desarrolló con la idea de tener un estándar para el acceso a bases de datos en entorno Windows. Aunque la industria ha aceptado ODBC como medio principal para acceso a bases de datos en Windows, ODBC no se introduce bien en el mundo Java, debido a la complejidad que presenta ODBC, y que entre otras cosas ha impedido su transición fuera del entorno Windows. El nivel de abstracción al que trabaja JDBC es alto en comparación con ODBC, la intención de Sun fue que supusiera la base de partida para crear librerías de más alto nivel. JDBC intenta ser tan simple como sea posible, pero proporcionando a los desarrolladores la máxima flexibilidad.

jdbc

Establecimiento de conexiones

Conectores o Drivers

El API JDBC viene distribuido en dos paquetes:

  • java.sql, dentro de J2SE
  • javax.sql, extensión dentro de J2EE

Un conector o driver es un conjunto de clases encargadas de implementar las interfaces del API y acceder a la base de datos. Para poder conectarse a una base de datos y lanzar consultas, una aplicación necesita tener un conector adecuado. Un conector suele ser un fichero .jar que contiene una implementación de todas las interfaces del API JDBC. Cuando se construye una aplicación de base de datos, JDBC oculta los detalles específicos de cada base de datos, de modo que le programador se ocupe sólo de su aplicación. El conector lo proporciona el fabricante de la base de datos o bien un tercero. El código de nuestra aplicación no depende del driver, puesto que trabajamos contra los paquetesjava.sql y javax.sql. JDBC ofrece las clases e interfaces para:

  • Establecer una conexión a una base de datos.
  • Ejecutar una consulta.
  • Procesar los resultados.

Los cuatro pasos a seguir

  • Importar el paquete jar de JDBC que se necesite, en el caso de MariaDB se descarga aquí: website

    Este es el que he probado

  • Agregarlo a tu proyecto de Eclipse: enlace

  • Importar lo necesario en código fuente o registrar el driver de conexión con .forname():

try {
   Class.forName("oracle.jdbc.driver.OracleDriver");
}
catch(ClassNotFoundException ex) {
   System.out.println("Error: unable to load driver class!");
   System.exit(1);
}
  • Crear el objeto de conexión –> se va más adelante.

Componer la URL de conexión

Antes de obtener el objeto de conexión necesitamos la información necesaria para la conexión. Para la url de la conexión a la base de datos necesitamos crear un String con las siguientes partes:

  • Protocolo: jdbc
  • Tipo de sistema gestor de base de datos: oracle, mysql, mariadb, db2 etc…
  • Servidor o hostname: localhost si estáis en local.
  • Puerto.
  • Base de datos a conectarse.

Ejemplo:

  // url para un sgbd de mysql en local utilizando el puerto 3306 y conectandose a la bd hola
  String sURL = "jdbc:mysql://localhost:3306/hola";

Hay más opciones podéis verlas en este ejemplo

Obtener objeto de conexión -> conectarse

Una vez que tenemos lista la URL a partir del método estático getConnection() de la clase DriveManager podemos obtener el objeto que utilizaremos para conectarnos a la base de datos. Tiene tres firmas el método:

  • getConnection(String url): dentro de la url viene incluido el usuario y contraseña.
  • getConnection(String url, Properties prop): a partir de un objeto Properties podemos agregar los datos necesarios.
  • getConnection(String url, String user, String password): la versión que vamos a utilizar.

Datos para usar el servidor del cesacle

  • URL: jdbc:mariadb://dbalumnos.sanclemente.local:3319
  • User: alumno
  • Password: abc123..

Indicaciones para crear tu base de datos: nombre_usuario_nombre_base_de_datos

Cerrar conexiones

Si no usamos try-with-resources la clase Connection tiene un método para cerrar close()

Comprobación

A veces es necesario comprobar si el sistema gestor de base datos está online y si tenemos conexión:

if (connection != null && !connection.isClosed()) {
        // run sql statements
    } else {
        // handle closed connection path
  }

Otra forma es validar la conexión:

if (connection.isValid(5)) {
        // run sql statements
    }
    else {
        // handle invalid connection
    }

En el anterior ejemplo espera 5 segundos de respuesta.

O realizar una consulta de prueba:

public static boolean isConnectionValid(Connection connection)
{
    try {
        if (connection != null && !connection.isClosed()) {
            // Running a simple validation query
            connection.prepareStatement("SELECT 1");
            return true;
        }
    }
    catch (SQLException e) {
        // log some useful data here
    }
    return false;
}

Ejemplo clásico

Connection con = null;
  String sURL = "jdbc:mariadb://dbalumnos.sanclemente.local:3319/test";
  try {

      con = (Connection) DriverManager.getConnection(sURL,"xxxx","xxxx");
      System.out.println ("¡Conexión exitosa!");

        } catch (Exception e) { 
     System.out.println("Error en la conexión:" + e.toString() );
  } finally {
  try {
      // Cerramos posibles conexiones abiertas
      if (con!=null) con.close();    
  } catch (Exception e) {
      System.out.println("Error cerrando conexiones: "
        + e.toString());
  } 
}

Ejemplo con try-with-resources y url con múltiples opciones

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class SQLDatabaseConnection {
    // Connect to your database.
    // Replace server name, username, and password with your credentials
    public static void main(String[] args) {
        String connectionUrl =
                "jdbc:sqlserver://yourserver.database.windows.net:1433;"
                        + "database=AdventureWorks;"
                        + "user=yourusername@yourserver;"
                        + "password=yourpassword;"
                        + "encrypt=true;"
                        + "trustServerCertificate=false;"
                        + "loginTimeout=30;";

        try (Connection connection = DriverManager.getConnection(connectionUrl);) {
            // Code here.
        }
        // Handle any errors that may have occurred.
        catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Conexión a base de datos embebidas (SQLite, H2 etc…)

Introducción

Las bases de datos embebidas, también conocidas como bases de datos integradas o bases de datos en memoria, son sistemas de gestión de datos diseñados para funcionar directamente dentro de una aplicación, sin necesidad de una instalación o configuración separada. Estas bases de datos se integran directamente en la aplicación que las utiliza y son especialmente útiles en situaciones donde se necesita un almacenamiento ligero, eficiente y de fácil acceso. Aquí hay algunas características y conceptos básicos de las bases de datos embebidas:

  • Sin servidor independiente: A diferencia de las bases de datos tradicionales que se ejecutan como servicios independientes y requieren instalación y configuración por separado, las bases de datos embebidas se incorporan directamente en la aplicación y se ejecutan dentro del mismo proceso.

  • Ligereza: Estas bases de datos están diseñadas para ser ligeras y tener una huella de memoria mínima. Esto las hace ideales para aplicaciones donde los recursos son limitados, como aplicaciones móviles o sistemas embebidos.

  • Acceso rápido: Al integrarse directamente en la aplicación, las bases de datos embebidas ofrecen un acceso muy rápido a los datos, ya que no hay necesidad de realizar operaciones de red para interactuar con un servidor externo.

  • Uso de memoria compartida: En muchos casos, las bases de datos embebidas utilizan la misma memoria que la aplicación que las utiliza, lo que puede aumentar la eficiencia y el rendimiento al eliminar la necesidad de comunicación entre procesos.

  • Persistencia de datos: A pesar de estar integradas en la aplicación, las bases de datos embebidas suelen ofrecer mecanismos para persistir los datos en el disco, lo que significa que los datos pueden sobrevivir a los reinicios de la aplicación o del sistema.

  • Soporte para múltiples modelos de datos: Aunque algunas bases de datos embebidas están diseñadas para un modelo de datos específico, como clave-valor o documentos, otras ofrecen soporte para modelos más complejos, como relaciones entre tablas.

Ejemplos comunes: Algunos ejemplos de bases de datos embebidas populares incluyen SQLite, Berkeley DB y Realm.

Conexión con SQLite

Cómo con las base de datos más clásicas necesitamos un objeto Connection con JDBC. La URL es similar, ejemplo:

Connection conex = DriverManager.getConnection("jdbc:sqlite:/Users/juan/Documents/SQLite");

Para usar en tu proyecto Maven estas dependencias en el pom.xml:

<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.45.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.12</version>
</dependency>
</dependencies>

Para crear una base de dato para SQLite puedes usar el DBeaver, es muy sencillo.

Conexión con H2

La URL necesaria con esta base de datos es:

Connection conex = DriverManager.getConnection("jdbc:h2:/Users/juan/Documents/h2/Ult");

Dependencia con Maven:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

Al crearla con DBeaver debes tener cuidado con la versión de los drivers que usa el gestor. Usando los mismos en tu proyecto Maven.

Extra: comparativa de tres sistemas de base de datos embebidas: enlace

Recuperación de información (Consultas)

Partiendo de una conexión con la base de datos (Clase Connection), ya podemos realizar consultas contra ella

Antes de nada importa esta pequeña base de datos para que practiquéis: bd.sql bd_paraDbeaver.sql

Statement (Consultas sin parámetros)

La clase Statement se obtiene del método createStament() de Connection:

try (Statement stmt = con.createStatement()) {
    // use stmt here
}

A partir de Statement tenemos la posibilidad de ejecutar consultas simples con el método executeQuery(), de esta forma:

String selectSql = "SELECT * FROM employees"; 
try (ResultSet resultSet = stmt.executeQuery(selectSql)) {
    // use resultSet here
}

Como observáis se obtiene un objeto Resulset que veremos más adelante.

PreparedStament (Consultas parametrizadas)

Esta clase nos sirve para realizar consultas más complejas. Que reciban datos o parámetros desde “fuera”, es decir desde nuestro programa. Para obtener dicho objecto se necesita el método prepareStament() de Connection, y se pasa la consulta sql parametrizada:

String consulta = "select * from Persona where nombre = ? ";
PreparedStatement sentencia= conexion.prepareStatement(consulta); 

La consulta sql parametrizada tiene “?” (1 o más), que representa los parámetros de entrada desde nuestro programa a la consulta. Teniendo listo el PreparedStament necesitamos asignar el o los parámetros antes de la ejecución, este objeto tiene los sets necesarios para asignar:

sentencia.setString(1,"Juan");
sentencia.executeQuery(); //Nos devolverá un Resultset

Como puedes advinina el primer parámetro debe ser un número que indica a que “?” (o parámetro) de la sentencia sql quiere asignar. Ojo –> siempre empieza en 1 El segundo parámetro de los sets es el propio valor o variable que quieres pasar a la consulta. Debes consultar todos los set que te ofrece el jdbc en correlación a los tipos de datos del sistema gestor de base de datos.

Extra: hay otra forma para poder agregar parámetros a tu consulta SQL:

Statement sentencia = conexion.createStatement(); 
String nombre="pepe"; 
String consulta = "select * from Persona where nombre='"+nombre+"'";
ResultSet rs=sentencia.executeQuery(consulta);

Pero resulta muy peligrosa por el problema de seguridad SQL Injection.

ResultSet

Esta interfaz nos permitirá obtener toda la información que devuelve nuestras instrucciones sql. Como si fuera un cursor o iterable. El método más importante es el next(), que recorre el resultado de la consulta linea a linea hasta que devuelve false: Base de datos a importar

// Pojo de empleado
public class Employee {
    private int id;
    private String name;
    private String position;
    private double salary;
 
    // standard constructor, getters, setters
}

String selectSql = "SELECT emp_id,name, posicion, salary  FROM employees"; 
try (ResultSet resultSet = stmt.executeQuery(selectSql)) {
    List<Employee> employees = new ArrayList<>(); 
    while (resultSet.next()) { 
        Employee emp = new Employee(); 
        emp.setId(resultSet.getInt("emp_id")); 
        emp.setName(resultSet.getString("name")); 
        emp.setPosition(resultSet.getString("posicion")); 
        emp.setSalary(resultSet.getDouble(4)); 
        employees.add(emp); 
    }
}

Y tiene los correspondientes gets para ir obteniendo campo a campo la información, siempre en consonacia con los parámetros del “Select” de la consulta slq a ejecutar. Los gets tienen dos formas:

  • resultSet.getInt(int indice_Columna): recibe un número que corresponde al orden en la clausula Select.
  • resultSet.getInt(String etiqueta_Columna): recibe el nombre de la columna en la clausula Select.

Base de datos de prueba: empresa.sql

Conversión JDBC a Java

imagen

Ejercicio 1

Partiendo del ejemplo anterior, nos piden una pequeña aplicación que nos permita conocer el salario y su posición a partir del id del empleado (se recoge por teclado). Resultado por pantalla.

Posteriormente el total del gasto en salario de la empresa.

Y por último cuantos empleados tienen más que un salario (se pide por teclado) y una posición (también se pide por teclado).

Altas, bajas y modificaciones de información

Lo primero obtenemos un objeto Statement a partir de Connection:

try (Statement stmt = con.createStatement()) {
    // use stmt here
}

El Statement tiene tres métodos para ejecutar:

  • executeQuery(): para SELECT instrucciones. Visto en el anterior ejercicio.
  • executeUpdate(): para actualizar información o estructura de base de datos.
  • execute(): se puede usar para ambos casos. Más utilizada para crear o eliminar estructura de base de datos

Actualización de información

Respecto a las consultas de actualización, con un objeto de Connection tenemos el método executeUpdate(), retornan el número de registros insertados, registros actualizados o eliminados, dependiendo del tipo de consulta que se trate. Supongamos que tenemos varios registros en la tabla Cliente, de una base de datos. Si quisiéramos actualizar el teléfono del tercer registro, que tiene idCLIENTE=3 y ponerle como nuevo teléfono el 968610009 tendríamos que hacer:

String connectionUrl = "jdbc:mysql://localhost/notarbd?" + "user=root&password=admin";
// Obtener la conexión
Connection con = DriverManager.getConnection(connectionUrl);
// Preparamos la consulta y la ejecutamos
Statement s = con.createStatement();
int registros_afectados = s.executeUpdate("UPDATE CLIENTE SET teléfono='968610009' WHERE idCLIENTE=3");
// Cerramos la conexión a la base de datos.
con.close();

Creación


String tableSql = "CREATE TABLE IF NOT EXISTS employees" 
  + "(emp_id int PRIMARY KEY AUTO_INCREMENT, name varchar(30),"
  + "position varchar(30), salary double)";
stmt.execute(tableSql);

Inserciones de información

Usamos excuteUpdate():

Statement s = con.createStatement();
s.executeUpdate( "INSERT INTO CLIENTE" +
" (idCLIENTE, NIF, NOMBRE, APELLIDOS, DIRECCIÓN, CPOSTAL, TELÉFONO, CORREOELEC)" + " VALUES (4, '66778998T', 'Alfredo', 'Gates Gates', 'C/
Pirata 23','20400', '891222112', 'prueba@eresmas.es' )") ;

Borrado de información

Lo mismo:

Statement s = con.createStatement();
numReg = s.executeUpdate( "DELETE FROM CLIENTE WHERE NIF= '66778998T' " );
// Informamos del número de registros borrados 
System.out.println ("\nSe borró " + numReg + " registro\n") ;

Nota: Si usamos execute en vez de executeUpdate, tenemos un método que nos devuelve el número de filas afectadas tras la ejecución de la sentencia sql: stmt.getUpdateCount()

PreparedStatement

En este tipo de sentencias sql también necesitamos parámetros de entrada y se trabaja de la misma forma que lo explicado en el anterior recurso

Autoguardado

Por defecto en MariaDB cualquier operación que implica alta, baja o modificación de datos tiene el autoguardado activo (autocommit true). Es decir se ejecuta la sentencia SQL y se guardan los cambios. Pero a veces nos interesa que no se así. Por ejemplo operaciones sobre la base de datos de todas o ninguna, se llaman transaciones. Si tenemos dudas de que valor está la variable autocommit podemos asignarla a verdadero:

connection.setAutoCommit(true);

Si desabilitamos el autoguardado luego podemos ejecutar el método commit para forzar el guardado de operaciones:

connection.setAutoCommit(false);
// ejecutamos las sentencias importantes ....
connection.commit();

Ejercicio 1

Partiendo del Pojo:

public class Person {

    private Integer id;
    private String name;
    private String lastName;
    private Integer age;

    // standard constructor, getters, and setters
}

Desarrolla estas funcionalidades a partir de la interface:

public interface IPerson {
    public  Connection openConnection() throws SQLException;
    public  int insertPerson(Connection connection, Person person) throws SQLException;
    public  void updatePersonAgeById(Connection connection, int id, int newAge) throws SQLException;
    public  List selectAllPeople(Connection connection) throws SQLException;
    public int deletePerson(Connection connection, int id) throws SQLException;
    public Person maxAgePerson(Connection connection) throws SQLException;
}

Pool de Conexiones

Como ya sabéis Java Database Connectivity (JDBC) es una API que proporciona una interfaz común para acceder a bases de datos relacionales desde Java. JDBC utiliza un conjunto de clases e interfaces para conectarse a una base de datos, enviar consultas y recuperar resultados. Pero hasta ahora usabamos/ocupabámos una conexión del SGBD. Las conexiones de un SGBD son limitadas, por tanto hay formas de conectarse a la base de datos de manera más eficiente.

Pool de Conexiones

El uso de un pool de conexiones JDBC puede mejorar el rendimiento de una aplicación Java que requiere conectarse a una base de datos. En lugar de abrir y cerrar una conexión cada vez que se necesita acceder a la base de datos, el pool de conexiones permite reutilizar las conexiones existentes.

Lo primero que es necesario es agregar tres jars de Apache a tu proyecto (de la misma manera que el jar de MariaDB) para poder usar esta forma nueva de conectarse:

Para crear un pool básico de conexiones se crea un objeto de la clase BasicDataSource, a partir de ese objeto se obteniene las conexiones necesitadas. Y se le asigna todos los parámetros necesarios:

public class EjemploPoolConexionJDBC {

    public static void main(String[] args) throws SQLException {
        // Configurar el pool de conexiones
        BasicDataSource dataSource = new BasicDataSource();
        //dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/hola");
        dataSource.setUsername("xxx");
        dataSource.setPassword("xxxx");
        dataSource.setInitialSize(5);
        dataSource.setMaxTotal(10);
        
        // Obtener una conexión del pool
        Connection conn = dataSource.getConnection();
        
        // Usar la conexión para enviar consultas
        // ...
        
        // Devolver la conexión al pool
        conn.close();
    }
}

Hay más opciones de pool con diferentes clases DataSource, y también podemos configurarlo con determinados sets del DataSource tal como véis en el ejemplo. Extra:

  • setMaxActive() : Número máximo de conexiones que se pueden abrir simultáneamente.
  • setMinIdle() : Número mínimo de conexiones inactivas que queremos que haya. Si el número de conexiones baja de este número, se abriran más.
  • setMaxIdle() : Número máximo de conexiones inactivas que queremos que haya. Si hay más, se irán cerrando.
  • setInitialSize() : Número de conexiones que se quiere que se abran en cuanto el pool comienza a trabajar (se llama por primera vez a getConnection(), setLogwriter(), setLoginTimeout(), getLoginTimeout() o getLogWriter(). Debe llamarse a setInitialSize() antes de llamar a cualquiera de estos métodos y después no puede cambiarse el valor.

Rowset

Vamos a ver una interfaz de manipulación de datos que nos ofrece más posibilidades que lo hemos estado usando hasta ahora -> Resulset Dicha interfaz es RowSet, que se encuentra en el paquete javax.sql Descargar. Esta interfaz tiene muchos tipos: JdbcRowSet, CachedRowSet, WebRowSet, FilteredRowSet o JoinRowSet. Las que vamos a usar es JdbcRowSet.

Imagen Resulset

Si os fijáis RowSet hereda toda la funcionalidades de ResultSet, lo que nos aporta RowSet es mayor flexibilidad y posibilidades.

Ventajas y diferencias entres RowSet y ResultSet

RowSet ResultSet
paquete javax.sql paquete java.sql
puede estas desconectado de la BD siempre debe mantener la conexión con la BD
flexibilidad en la navegación siempre hacia delante (next())
puede ser serializable y transmitirlo no puede ser serializable
es un Java.Bean no es un Java.Bean se obtiene del método executeQuey()
se puede utilizar para actualizaciones sólo se utiliza de consulta

Creación y consulta

Una vez importado el jar de javax.sql podemos crear un objeto tipo JdbcRowSet, con un factory y asignarle lo necesario para conectarse a la base de datos:

JdbcRowSet rowSet;
try {
    //obtenemos el rowset de forma estatica
    rowSet = RowSetProvider.newFactory().createJdbcRowSet();
    rowSet.setUrl("jdbc:mysql://localhost:3306/Empresa");

    rowSet.setUsername("root");
    rowSet.setPassword("xxxxx");

    // Asignamos consulta y ejecutamos 
    rowSet.setCommand("select * from employees");
    rowSet.execute();

Los métodos setCommand() y execute() asigna consulta y la ejecuta.

Recorrer resultado y navegación

Como ya sabéis podemos usar el método next() para ir recorriendo fila a fila:

while (rowSet.next()) {
                System.out.println("Id: "  + rowSet.getInt(1));
                System.out.println("Name: " + rowSet.getString(2));
                System.out.println("Posicion: " + rowSet.getString(3));
            }

Método más interesantes de navegación:

  • previous(): hacia atrás.
  • absolute(int i): colocarse en una una posición concreta (empieza en 1).
  • first(): primer registro.
  • last(): último registro.

Actualizar registros

No sólo nos sirve para recorrer y navegar por los registros del resultado de una consulta, también podemos usarlo para actualizar información:

Donde se encuentra el cursor podemos actualizar un campo:

rset.updateString("PRODUCT", "HDMI CABLE");
rowSet.updateRow();

Con este método updateXXX() debes elegir el tipo de dato a actualizar. El primer argumento nombre de columna de la consulta o número (posición). Y ejecutar updateRow().

Realizar inserciones

RowSet también nos permite realizar inserciones usando el método moveToInsertRow() para colocarse en posición de inserción y usando los updates correspondientes. Es necesario ejecutar insertRow() para confirmar el nuevo registro:

rowSet.moveToInsertRow();
rowSet.updateInt("emp_id", 500);
rowSet.updateString("name", "Paco");
rowSet.updateString("posicion", "secretario");
rowSet.updateFloat("salary",10000);
rowSet.insertRow();

Borrado de filas

Usamos el método deleteRow() una vez te coloques en el registro que quieres eliminar.

Consultas parametrizadas

Exactamente igual que con ResultSet, necesitamos las “?” para indicar el parámetro de entrada, y existen los “set`s” correspondientes para asignar el valor al parámetro.

Ejercicio 1

Partiendo de la base de datos Empresa y la tabla Person realiza un programa que nos permita estas operaciones (usar RowSet en la mayoría de casos):

  • Mostrar todos los registros.
  • Mostrar el número de personas de la empresa.
  • Mostrar un registro por la posición. Una vez colocado que nos permita mostrar el anterior registro o el siguiente.
  • Actualizar una edad de una persona, indicando la posición.
  • Eliminar último registro.
  • Insertar una nueva persona en la empresa.

Extra: Por si queréis más

  • Típicos patrones de diseño con datos: DAO y Repository.
  • Frameworks de persistencia: Hibernate y JPA.
  • Uso de NoSQL con Java.
  • Transacciones y uso de objetos grandes (imágenes, ficheros etc…) Blob/Clob.

Dotenv

Una vez que vamos avanzando y creamos proyectos más complejos, surge la necesidad de gestionar variables de entorno de nuestro programa. Estas variables de entorno pueden contener desde parámetros de configuración, datos de conexión (server, base de datos, ficheros, repositorios) o de usuario, información del entorno de ejecución, parámetros etc…

Para gestionar y tener bien centralizado todos esos parámetros surge la librería dotenv.

Introdución a DotEnv

Dotenv es una herramienta que permite cargar variables de entorno desde un archivo de configuración en proyectos de desarrollo de software. Es comúnmente utilizado en entornos de desarrollo de aplicaciones web para cargar configuraciones sensibles, como credenciales de bases de datos o claves de API, sin necesidad de exponerlas directamente en el código fuente.

Dotenv se encarga de cargar estas variables de entorno cuando la aplicación se inicia en un entorno de desarrollo, haciendo que la configuración sea fácilmente ajustable y portátil entre diferentes sistemas y configuraciones. Esto promueve una práctica de desarrollo más segura y escalable al separar la configuración del código fuente y permitir una gestión más centralizada de las variables de entorno.

Instalación de DotEnv

A partir de una dependencia Maven en lo más cómodo:

<dependency>
    <groupId>io.github.cdimascio</groupId>
    <artifactId>dotenv-java</artifactId>
    <version>3.0.0</version>
</dependency>

Aquí os dejo también el jar que podéis agregar a un proyecto: jar

Uso de DotEnv

Lo primero es crear un fichero dentro de tu proyecto, con el nombre y extensión: “.env”

Este fichero contendrá por cada línea la clave de la variable y su valor, separado por un “=”. Ejemplo:

MY_ENV=MY_VALUE
...

Cuando se necesite usar dichas variables se debe cargar el .env desde el código fuente. La función load() de la clase Dotenv se encargará de ello, y la función get(String) para obtener el valor a partir de la clave. Ejemplo:

Dotenv dotenv = Dotenv.load();
System.out.println(dotenv.get("Mysql"));

También podemos obtener variables de sistema, a partir de su nombre. Y tienen preferencia con respecto a las del fichero .env.

Uso más avanzado

Ejemplo más avanzado, donde hay directrices de donde buscar el fichero, o si no está que no de error etc…

Dotenv dotenv = Dotenv.configure()
        .directory("./some/path")
        .ignoreIfMalformed()
        .ignoreIfMissing()
        .load();

Enlace con las opciones: enlace Portal de Github: enlace

Ejercicio

Cambia algun proyecto de base de datos, usando dotenv para todos los parámetros de conexión utilizados.

Conclusión

La idea es tener a mano todos esos parámetro que hay que cambiar cuando se pasa de un proyecto de desarrollo a producción, y tener mucho control sobre ese fichero. Son datos que no deberían estar esparcidos por el código. Una recomendación es incluir en el fichero .gitignore el fichero .env, para que no suba a ningún repositorio publico.

10.Lambda

Lambda

Introducción a lambda

Introdución a lambda

Las expresiones lambda surgen a partir de la versión 8 de Java, ante la necesidad de “modernizarse” contra otros lenguajes de programación. La idea es incluir programación funcional en Java, crear sentencias más directas o resolver el problema en menos líneas de código (menos verboso). Se busca la simplicidad. En cada nueva versión del jdk, se ha ido incluyendo más expresiones lambda y también cierta evolución. Por tanto no es un nuevo “Java” sino es un plus que debes agregar a tu programación.

icono lambda

Aspectos ya vistos

Expresiones con lambda

Estructura normal de una expresión lambda: (parámetros) -> cuerpo de la expresión

Lista de argumentos Operador Cuerpo
(x,y) -> { x + y }

Cuerpo de la expresión: Más de una sentencia requiere {} Parétesis para argumento más de uno.

Aspecto importante a tener en cuenta es que hay inferencia de tipos de datos, es decir en lo máximo posible no es necesario poner el tipo de dato. Esto implica expresiones más compactas y simples.

Debemos usar interfaces funcionales para usar expresiones lambda. Las más frecuentes entán en el paquete java.util.function.

Video explicativo de la sintáxis: enlace

Interfaces funcionales

Las interfaces funcionales son interfaces que tienen un método a implementar, es decir, un método abstracto. Esto significa que cada interfaz creada que respeta esta premisa se convierte automáticamente en una interfaz funcional. Toda expresión lambda debe convertirse o usar una interfaz funcional.

Interfaz Consumer

enlace

De las más sencillas recibe un argumento (genérico), pero no devuelve nada. Consumer representa una función con un argumento que no retorna nada. Tal y como su nombre indica, consume un valor y no genera nada. Un ejemplo de este tipo de funciones es el método de impresión por la salida estándar, que admite el texto a imprimir y no retorna nada. Ejemplo:

List<Integer> numbers = Arrays.asList(1,3,4,6);
numbers.forEach(number -> System.out.println(number));    

El forEach puede recibir un Consumer como parámetro. Existe la opción Biconsumer, que tiene dos argumentos de entrada.

Interfaz Supplier

enlace

Supplier representa una función sin argumentos que retorna un resultado. Tal y como su nombre indica, genera un valor. Un ejemplo de este tipo de funciones es el método de generación de números aleatoriosde Math, que no requiere argumentos:

Supplier<Integer> randomNumbersSupp=() -> new Random().nextInt(10);
Supplier<LocalDateTime> s = () -> LocalDateTime.now();
LocalDateTime time = s.get();
System.out.println(time);

Interfaz Predicate

enlace

Recibe un argumento y devuelve un booleano. Sirve para comprobar si una condición es verdadera o falsa.

Predicate<String> checker = a -> a.startsWith("M");
System.out.println(checker.test("Miguel"));

Lo utilizaros para aspectos condicionales dentro de una expresión lambda. También existe la versión con dos argumentos Bipredicate.

Interfaz Function

enlace

Recibe un parámetro y devuelve otro parámetro. Hay versiones con más opciones (Bifunction por ejemplo).

package com.arquitecturajava.functional;
 
public class Principal {
@FunctionalInterface
interface Matematicas {
public double operacion(double x, double y);
}
public static void main(String[] args) {
    Matematicas o = (x, y) -> x + y;
    System.out.println(o.operacion(2, 3));
 }
}

Similar a Function pero si queremos trabajar con siempre mismo tipo de datos podemos usar UnaryOperator. Por ejemplo:

UnaryOperator<Integer> multiplicarUnNumeroporDos = x -> x * 2;

Ejercicio 1

Crea una interfaz funcional, para usarla con lambda que devuelva el divisor más pequeño de un valor ( el parámetro de entrada).

Ejercicio 2

Crea una interfaz funcional, para usarla con lambda que recibe un parámetro tipo String y devuelve ese mismo String al revés. Entrada: Hola Mundo Salida: odnuM aloH

Ejercicio 3

Usa la interfaz supplier para “contener” un objeto de la clase:

public class Student {
    private int id;
    private String name;
    private String gender;
    private int age;

Uso de Stream y Colecciones

Api Stream

A partir de Java 8 ha aparecido esta api que facilita el trabajo con colecciones y flujo de datos/trabajo. Con Stream podemos usar todo lo visto con lambda, y de esta manera se exprime todas sus posibilidades.

Podemos obtener el stream de cualquier colección de esta manera:

Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();

Si se necesita trabajar con Array tenemos estas posibilidades:

Stream<String> streamOfArray = Stream.of("a", "b", "c");

String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);

Stream tiene muchas posibilidades como por ejemplo con String, Ficheros o generar contenido automático (enlace enlace2)

Uso con colecciones

Usarlo con Maps:

Map<String, Integer> map = new HashMap<>();
map.put("Pepe",5);
map.put("Fran", 8);
map.put("Jose", 7);

List<String> biggerThanSix = map.entrySet().stream()
  .filter(e -> 6 < e.getValue())
  .map(a -> a.getKey())
  .collect(Collectors.toList());

El método estático Stream.of puede ser muy útil para crear streams de datos primitivos o objetos, como en este ejemplo:

Stream<Person> userStream = Stream.of(
    new Person("Mahesh", 22),
    new Person("Krishn", 20),
    new Person("Suresh", 25)
);
userStream.forEach(u -> System.out.println(u.getNombre()));
//Uso con Strings
Stream<String> mystream = Stream.of("AA", "BB", "CC", "DD");
mystream.forEach(e -> System.out.println(e));

Operación forEach

La operación más usada, y que ya hemos usado. Implica un bucle que recorre todos los objetos del Stream. ForEach acepta expresiones lambda.

Operación Map

El uso de Java Stream map es una de las operaciones más comunes cuando trabajamos con un flujo de Streams. El método map nos permite realizar una transformación rápida de los datos y muy directa sobre el flujo original. Es decir “mapea” lo que necesitas de cada objeto de la colección:

Persona p1 = new Persona("pedro", 20, "perez");
Persona p2 = new Persona("juan", 25, "perez");
Persona p3 = new Persona("ana", 30, "perez");
List < Persona > lista = new ArrayList < Persona > ();
lista.add(p1);
lista.add(p2);
lista.add(p3);

lista
    .stream()
    .map(a -> a.edad )
    .forEach(a -> System.out.println(a));;

Es muy frecuente usar la operación .sum con el mapeo. Pruébalo!

Operación filter

Como el propio nombre indica filtra a partir de una condición los objetos que queremos, al más puro estilo “where” en una consulta SQL. Esta operación necesita una interfaz funcional Predicate. Por lógica algo que devuelva booleano. Si queremos rapidez en la instrucción lambda se debe colocar de primero.

Libro l = new Libro("El señor de los anillos", "fantasia", 1100);
Libro l2 = new Libro("El Juego de Ender", "ciencia ficcion", 500);
Libro l3 = new Libro("La fundacion", "ciencia ficcion", 500);
Libro l4 = new Libro("Los pilares de la tierra", "historica", 1200);
List <Libro> lista = Arrays.asList(l, l2, l3, l4);
lista.stream()
    .filter(libro -> libro.getPaginas() > 1000)
    .map(libro -> libro.getTitulo())
    .forEach(System.out::println);

Podemos crear predicados muy complejos y luego usarlos con Stream y filter cuando queramos:

public static boolean buenosLibros(Libro libro) {
        Predicate <Libro> p1 = (Libro l) -> l.getCategoria().equals("ciencia ficcion");
        Predicate <Libro> p2 = (Libro l) -> l.getCategoria().equals("fantasia");
        Predicate <Libro> p3 = (Libro l) -> l.getPaginas() > 1000;
        Predicate <Libro> ptotal = p1.or(p2).and(p3);
        return ptotal.test(libro);
    }

Con filter también es común usar count para obtener el número de elementos que cumple la condición:

cars.stream().filter(x -> x.getColor().equals("White")).count();

Operación Collect

Esta operación se utiliza para obtener una nueva colección a partir de operaciones con Stream:

List<Car> result = cars.stream().filter(x -> x.getColor().equals("White")).collect(Collectors.toList());

El anterior ejemplo obtenemos una nueva lista, pero hay opciones para mapas y conjuntos: Collectors.toSet() o Collectors.toMap.

Operación Sorted

También podemos ordenar un Stream, esta operación puede recibir varios tipos de argumentos:

  • Comparator.comparing():
  lista.stream()
    .sorted(Comparator.comparing( p -> p.getApellidos()))
    .forEach(System.out::println);
  • CompareTo con un BiPredicate:
List<Employee> employees = empList.stream()
      .sorted((e1, e2) -> e1.getName().compareTo(e2.getName()))
      .collect(Collectors.toList());

Operación distinct

Genera una lista de objectos descartando los iguales:

List<String> ids = cars.stream()
.filter(x -> x.getColor().equals("White"))
.map(car -> car.getBrand())
.distinct().collect(Collectors.toList());

Si mantenemos los objetos en el stream, la operación distinct usará el método equals de la clase correspondiente, que debemos sobreescribir.

Operación parallel

En el caso que necesitemos mejorar el rendimiento al trabajar con Stream, tenemos la operación parallel que genera varios hilos de ejecución. Tal como vimos con ficheros. Os dejo un enlace para que podáis probarlo: enlace

Las siguientes operaciones es necesario conocer la clase Optional

Operación findFirst

Esta operación nos devolverá el primer elememento del Stream, y los devolverá un objeto de la clase Optional, es decir el primer elemento o sino existe referencia a null.

@Test
public void createStream_whenFindFirstResultIsPresent_thenCorrect() {

    List<String> list = Arrays.asList("A", "B", "C", "D");

    Optional<String> resultFirst = list.stream().findFirst();

   	if (resultFirst.isPresent()){System.out.println(resultFirst.get());}

}

Sino no nos importa el orden y nos vale cualquier elemento podéis probar Stream.findAny().

Operación min max

Estas operaciones nos devolverá el mínimo o el máximo de un Stream a partir de recibir un Comparator (Compare, Comparing) de parámetro. Y nos devolverá un Optional como en la operación anterior. Ejemplos sencillos:

List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
Integer var = list.stream().min(Integer::compare).get();

List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
Optional<Integer> var = list.stream()
                    .min(Comparator.reverseOrder());

Ejercicio 1

Partiendo de un array de String, usa Stream y min para obtener el string más pequeño del array:

Ejercicio 2

Crea una lista de objetos tipo Persona, una vez lista la colección crea una instrucción lambda que nos devuelva la persona con menos edad.

Ejercicio 3

Genera una interfaz supplier que genera un valor entero (entre 1 y 10) al azar. Prueba Stream.generate para generar 5 numeros (.limit) y los muestre por pantalla.

Ejercicio 4

Partiendo de este pojo:

public class Factura  {
    private int numero;
    private String concepto;
    private double importe;
}

Implementa gets, sets, equals, hashCode, constructor por parámetros y toString. Crea una colección donde almacenes objetos factura. Usando lambda obtén una nueva colección filtrada con las facturas de importe mayor a 200. Muestra por pantalla el concepto de la factura. También nos piden una nueva colección con las facturas entre 200 y 300 (investiga el and de Predicate).

Operador "::"

Operador “::” también llamado “referencia del método” surgió a partir de Java 8, nos sirve para abreviar expresiones lambda entre otras funcionalidades. Por ejemplo:

Comparator x = (c1, c2) -> ((Computer) c1).getAge().compareTo(((Computer) c2).getAge());
		
Comparator c = Comparator.comparing(Computer::getAge);

//Expresiones identicas
a -> System.out.println(a)
System.out::println

Sintaxis: “::

Como podéis obsevar con este operador accedemos a un método de la clase ya implementado. Este operador ya lo hemos utilizado con ficheros con lambda por ejemplo accediendo a println.

Posibilidades de uso

Acceso a métodos estáticos de una clase

Clases de ejemplo para probar lo siguiente: Computer.java, ComputerUtils.java y OrdenadorHijo.java

Ejemplo:

List <Computer> inventory = Arrays.asList(
  new Computer( 2015, "white", 35), new Computer(2009, "black", 65));
inventory.forEach(ComputerUtils::repair);

Acceso a método normales de un objeto

Computer c1 = new Computer(2015, "white");
Computer c2 = new Computer(2009, "black");
Computer c3 = new Computer(2014, "black");
Arrays.asList(c1, c2, c3).forEach(System.out::print);

Uso con herencia

Método de la superclase:

public Double calculateValue(Double initialValue) {
    return initialValue/1.50;
}

Método sobreescrito en clase hija:

@Override
public Double calculateValue(Double initialValue){
    Function<Double, Double> function = super::calculateValue;
    Double pcValue = function.apply(initialValue);
    return pcValue + (initialValue/10) ;
}

Llamar al constructor

Con la palabra new podemos ejecutar el constructor de una clase:

class GFG {
  
    // Class constructor
    public GFG(String s)
    {
        System.out.println("Hello " + s);
    }
  
    // Driver code
    public static void main(String[] args)
    {
  
        List<String> list = new ArrayList<String>();
        list.add("Geeks");
        list.add("For");
        list.add("GEEKS");
  
        // call the class constructor
        // using double colon operator
        list.forEach(GFG::new);
    }
}

Ejercicio 1

Crea una lista de nombres, ordénalos (por longuitud) y muéstralos por pantalla usándo lambda y el operador ::

11.Novedades

Novedades

Clase Optional

La Clase Optional aparece a partir de Java 9, y simplemente hace referencia a un objeto o un valor null. Es decir hay situaciones en que ciertas sentencias nos podrá devolver algo o no. Siempre los “null” nos puede generar problemas, si usamos esta clase no. Ya que contempla la referencia a nada, intenta substituir a comprobaciones de tipo: “if (nota!=null) {}”

Resumen de cuatro métodos principales:

  • boolean isPresent(): devuelve si tiene valor o no.
  • get(): devuelve el objeto.
  • Optional.of(objeto): constructor de Optional estático (no acepta null).
  • Optional.ofNullable(objeto): mejor opción que la anterior ya que si acepta nulls.
  • boolean Optional.empty(): devuelve false si no existe el objeto.

Ejemplo:

public class Nota {

  private String asignatura;
  private double valor;
  public String getAsignatura() {
    return asignatura;
  }
  public void setAsignatura(String asignatura) {
    this.asignatura = asignatura;
  }
  public double getValor() {
    return valor;
  }
  public void setValor(double valor) {
    this.valor = valor;
  }
  public Nota(String asignatura, double valor) {
    super();
    this.asignatura = asignatura;
    this.valor = valor;
  }
}

public class Principal {

  public static void main(String[] args) {

    List<Nota> notas= new ArrayList<Nota>();
    notas.add(new Nota("matematicas",3));
    notas.add(new Nota("lengua",10));
    notas.add(new Nota("ingles",5));
    notas.add(new Nota("fisica",7));
    
    Nota nota= buscarNotaSobresaliente(notas);

    // Estas sentencias nos pueden generar el típico java.lang.NullPointerExcepcion
    System.out.println(nota.getValor());
    System.out.println(nota.getAsignatura());
  }

  public static Nota buscarNotaSobresaliente(List<Nota> notas) {

    Nota nota=null;
    for (Nota unaNota:notas) {

      if (unaNota.getValor()>=9) {
        nota= unaNota;
      }
    }
    
    return nota;
 }
}

Esto se soluciona con un !=null

if (nota!=null) {
      System.out.println(nota.getValor());
      System.out.println(nota.getAsignatura());
    }

O usando la clase Optional:

Optional<Nota> oNota= buscarNotaSobresaliente(notas);
    if (oNota.isPresent()) {
      Nota nota=oNota.get();
      System.out.println(nota.getValor());
      System.out.println(nota.getAsignatura());
    }
    
  }
  public static Optional<Nota> buscarNotaSobresaliente(List<Nota> notas) {
    
    for (Nota unaNota:notas) {
      
      
      if (unaNota.getValor()>=9) {
        return Optional.of(unaNota);
      }
    }
    return Optional.empty();
  
  }

Ejemplo con Optional.of():

public Optional<Capitulo> buscarCapitulo (String nombre)  {
        for (Capitulo c: capitulos) {
            if (c.getNombre().equals("nombre")) {   
                return Optional.of(c);
            }    
        }
        return Optional.empty();
    }

//main...

Optional<Capitulo> busqueda;
busqueda = l.buscarCapitulo("capitulo2");
busqueda.ifPresent((x)->System.out.println(x.getLongitud())); // lambda con ifPresent()

Recordemos que un Optional es un tipo que permite almacenar dos valores (valor concreto/valor nulo).

Entonces los Optionals nos permiten manejar valores que podrían no existir. Esto nos ayuda a reducir la cantidad de excepciones generadas por valores nulos (NullPointerExceptions). La clase Optional cuenta con varios métodos útiles que nos van a permitir manejar tanto la creación, la obtención y la verificación de los valores. Esta clase se ha ido ampliando con todas las posibilidades lógicas/booleanas posibles. más posibilidades

Ejemplo de un método que se utiliza con lambda

Un método muy cómodo de usar de Optional es ifPresentOrElse(), y cómo os imáginais es un if else dependiendo si tiene objeto el optional o no. Sintáxis con el uso de lambda:

public void ifPresentOrElse(Consumer<T> action,
                            Runnable emptyAction)

Ejemplo:

 Optional<Integer> op = Optional.ofNullable(9455); // creamos el Optional
 op.ifPresentOrElse( 
            (value) -> { System.out.println( 
                         "Value is present, its: "
                         + value); }, 
            () -> { System.out.println( 
                         "Value is empty"); }); 

Ejercicio 1

Crear un programa que simula un sistema de registro de usuarios (sin persistencia guardar en colecciones). El programa tendrá las siguientes funcionalidades:

  • Registrar un nuevo usuario con nombre, apellido y correo electrónico.
  • Buscar un usuario por su nombre y apellido.
  • Actualizar el correo electrónico de un usuario existente.
  • Eliminar un usuario existente. Para gestionar la ausencia de valores, utilizaremos la clase Optional.

Uso de Var

¿Para que sirve la palabra reservada var?

Crear objetos y variable tipo primitivo sin indicar el tipo. Es decir hay una inferencia de tipo, java internamente gestiona el tipo de dato. La sintaxis es la siguiente:

ArrayList<Persona> lista= new ArrayList<>();
lista.add(new Persona("pedro","perez",20));
// Pasamos a esto
var lista= new ArrayList<Persona>();
lista.add(new Persona("pedro","perez",20));

Condiciones

  • Siempre variable inicializadas.
  • Se puede usar con String, char, long, float, double, boolean y Objetos.
  • Con byte y short necesario Cast.
  • Null no es inferible, lo mismo con arrays.
  • Con lambda se puede usar, pero si tiene más de un argumento hay que usarlo en todos. Aspecto muy cambiante en las últimas versiones de Java:
(var s1, var s2) -> s1 + s2
// No permitido
(var s1, s2) -> s1 + s2
// Tampoco esto
(var s1, String s2) -> s1 + s2

Aclaración importante

Java sigue siendo fuertemente tipado en comparación a otros lenguajes de programación como JavaScript. Es decir una variable no puede cambiar de tipo al que referencia.

Ejemplo

//datos primitivos
var texto = "abc";
var caracter = '\n';
var largo = 42L;
var flotante = 3.14f;
var doble = 3.14d;
var logico = true;

//necesario cast con este tipo
var octeto = (byte)1;
var corto = (short)2;
var entero = 3;

var p=new Object();

var list = new ArrayList<String>(); // infiere ArrayList<String>

//Siempre algo inicializado
//var count=null;// Compilation error  

//Uso cómodo en bucles for
var numbers = List.of("a", "b", "c");
//for corto
for (var nr : numbers)
    System.out.print(nr + " ");
//for largo
for (var i = 0; i < numbers.size(); i++)
    System.out.print(numbers.get(i) + " ");

Clases Record

Las clases Record son un nuevo tipo de clases en el lenguaje Java, ayudan a modelar agregados de datos con menos ceremonia que las clases normales. Son clases que actúan como contenedores para datos inmutables, pueden ser considerados como tuplas. La declaración de un record mayormente consiste en la declaración de su estado. No es su objetivo resolver los problemas de las clases mutables que usan las convenciones de nombres de los JavaBeans. La palabra reservada es record y se usa en vez de class Un ejemplo de clase normal:

public class Point {
   private final int x;
   private final int y;

   Point(int x, int y) {
       this.x = x;
       this.y = y;
   }

   int x() { return x; }
   int y() { return y; }

   public boolean equals(Object o) {
       if (!(o instanceof Point)) return false;
       Point other = (Point) o;
       return other.x == x && other.y == y;
   }

   public int hashCode() {
       return Objects.hash(x, y);
   }

   public String toString() {
       return String.format("Point[x=%d, y=%d]", x, y);
   }
}

Ejemplo con record:

public record Point(int x, int y) {}

Aspectos relevantes

Las clases Record adquieren automáticamente los siguientes elementos:

  • Un campo privado y final por cada “atributo” o componente.
  • Constructor: implica un constructor por parámetros a partir de los argumentos establecidos. Este aspecto puede ser modificable.
public record Person (String name, String address) {}
Person person = new Person("John Doe", "100 Linda Ln.");
  • Getters y Setters: también existen pero se acceden con el nombre del atributo.
  • Equals y hashCode: se genera un método equals() normal, devuelve true si los objetos son del mismo tipo y los campos de los objetos son iguales.
  • toString: una implemetación de toString que incluye una representación de todos los componentes del registro con sus nombres.

Ejercicio 1

¿Qué ocurre con hashCode() y toString()? Compruébalo.

Método estáticos y variables

Permite crear variables estáticas en la declaración del record:

public record Person(String name, String address) {
    public static String UNKNOWN_ADDRESS = "Unknown";
}
Person.UNKNOWN_ADDRESS

Y por último método estáticos:

public record Person(String name, String address) {
    public static Person unnamed(String address) {
        return new Person("Unnamed", address);
    }
}
Person.unnamed("100 Linda Ln.");

## Limitaciones

  • No pueden extender ninguna otra clase y no pueden declarar campos que no sean los privados automáticos que corresponden a los componentes de la descripción del estado en la descripción.
  • Los registros son implícitamente final y no pueden ser abstract. Esto significa que no pueden ser mejorados por otra clase o registro.
  • Los componentes de un registro son implícitamente final. Esta restricción hace que sean inmutables.

Nota final: también acepta patern matching que veremos a continuación.

Patern Matching

Nota: es necesario un JDK 21 o superior. Sino queréis o podéis instalar una versióm última del JDK podéis usar este enlace: link.

Una de las características ampliadas con Java 9 es el Patern Matching que nos permite eliminar algunos cast explícitos, esto ya ha sido aplicado en expresiones como condiciones. Uso en el switch:

// Antes
Object o = ...; // any object
String formatted = null;
if (o instanceof Integer i) {
    formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
    formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
    formatted = String.format("double %f", d);
} else {
    formatted = String.format("Object %s", o.toString());
}

// Despues
Object o = ...; // any object
String formatter = switch(o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case Object o2  -> String.format("Object %s", o.toString());
};

Uso del patern matching con instance of: Versión clásica del equals:

public class Point {
    private int x;
    private int y;

    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    // constructor, hashCode method and accessors have been omitted
}

Posible forma con patern matching:

public boolean equals(Object o) {
    return o instanceof Point point &&
            x == point.x &&
            y == point.y;
}

Ahora también se puede usar con records Antes:

record Point(int x, int y) {}

static void printSum(Object o) {
    if (o instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x + y);
    }
}

Ahora:

void printSum(Object o) {
    if (o instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}

Al escribir la expresión de pattern matching para un record permite al mismo tiempo desestructurar los elementos y extraerlos a variables. La expresión resultante para desestructurar los elementos es muy verbosa pero hace más simple el acceso posterior a las variables de record.

Bloques de texto

En Java embeber en el código un trozo de código HTML, XML, SQL o JSON en un literal como un String requiere editarlo de forma significativa con caracteres de escape y concatenación para que el código compile. La cadena transformada resultante es poco legible y difícil de mantener.

Un bloque de texto HTML en código Java requiere de múltiples caracteres de escape y concatenaciones de cadenas:

String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world</p>\n" +
              "    </body>\n" +
              "</html>\n";

Usando bloques de texto se eliminan los caracteres de escape y las concatenaciones. El código resultante es mucho más legible y fácil de mantener. Debes iniciar la cadena de carácteres con triple dobles comillas:

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;

Ejercicio 1

Probar bloques de texto para json y xml por ejemplo.

Los bloque de texto nos permite identación (—), se escapan e interpretan los caracteres especiales, se normaliza independientente del sistema operativo, no tenemos que escapar etc… Ofrece una buena manera de trabajar con cadenas multilínea y con caracteres especiales.

Más información -> enlace enlace2 enlace3

Clases Sealed

Las clases Sealed están relacionadas con herencia de clases. Estas clases o interfaces selladas nos permiten restringir que otras clases e interfaces puede extender de ellas. El objetivo de las clases o interfaces sealed es proporcionar una forma de permitir que una clases sea ampliamente accessible pero a la vez no ampliamente extensible. Estas pueden ser extendidas o implementadas solamente por aquellas clases e interfaces que son explícitamente permitidas. Las clases sealed no tiene como objetivo ser un reemplazo a la palabra reservada final. Palabra reservada: sealed, non-sealed y permits

Ejemplo:

public abstract sealed class Shape
    permits Circle, Rectangle, Square, WeirdShape { ... }

public final class Circle extends Shape { ... }
public final class Rectangule extends Shape { ... }
public final class Square extends Shape { ... }
public final class WeirdShape extends Shape { ... }

Como podéis observar esta posibilidad nos permite controlar más la propagación de la herencia. Cada subclase que herede debe usar un modificador para describir como propagar el sellado iniciado por su super-clase:

  • Una sub-clase permitida puede aplicar el modificar final para evitar extenderse más.
  • Una sub-clase permitida puede aplicar el modificador sealed seguido de la cláusula permits para extender a otras sub-clases en su jerarquía.
  • Una sub-clase permitida puede aplicar el modificador non-sealed de manera que revierte en su propia jerarquía el “sellado” de la clase super clase, y abriendo la extensión a otros clases desconocidas por la super clase. Ejemplos:
public abstract sealed class Shape
    permits Circle, Rectangle, Square, WeirdShape { ... }

public final class Circle extends Shape { ... }

public sealed class Rectangle extends Shape 
    permits TransparentRectangle, FilledRectangle { ... }
public final class TransparentRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }

public final class Square extends Shape { ... }

public non-sealed class WeirdShape extends Shape { ... }

El modificador sealed también se pueden usar tanto para interfaces como para records.

Ejercicio 1

Haz un pequeño ejemplo donde pruebes sealed para interfaces como para records.

Librería lombok

Introdución

Lombok es una librería para Java que a partir de anotaciones nos permite ahorrar código, crear unas clases más limpias. La web oficial es esta -> web Es fácil de usar y agregar a nuestro IDE y proyectos.

Instalación en Eclipse

  • Descargarse el jar correspondiente. enlace
  • Ejecuta dicho jar desde consola con: java -jar
  • Este ejecutable detecta los IDEs que tengas instalados, se selecciona Eclipse y se instala el plugin.
  • Debes reiniciar el IDE, y si todo ha ido bien en About Eclipse debe aparecer. imagen Eclipse

Instalación en proyecto

Con maven:

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Agregandolo al classpath el jar de manera clásica: imagen classpath

Instalación en IntelliJ IDEA

Con este IDE podéis probarlo en los ordenadores del instituto. Agregáis al proyecto el jar necesario (como con Eclipse): Boton Derecho sobre proyecto -> Open Module Settings -> Libraries -> Y se agrega jar Es necesario agregar un pluging: pluging

Posibles anotaciones:

  • @Getter y @Setter: se colocan encima de la declaración de la clase o también en los atributos. Implica la generación automática de los getters y setters de todos los atributos.

  • @ToString y EqualsAndHashCode: es para la clase y se genera una sobrescrición del toString con los atributos de la clase, y se genera un equals en base al hash y el contenido de los atributos.

  • @AllArgsConstructor: automáticamente se genera un constructor con todos los parámetros de la clase, en el orden de declaración de los mismos. Campos variables

  • @NoArgsConstructor: lo mismo que lo anterior pero para el constructor por defecto.

  • @Data: equivale a todas las anotaciones de gets, sets, toString e equals y hashcode.

Hay más anotaciones, por ejemplo para pruebas, logs etc… Está aquí en enlace oficial con la api: api y con más anotaciones normales lombok.

Ejemplo:

Una vez hayas agregado la librería a tu proyecto ya puedes probarlo. Nota: activa la vista del Eclipse “Outline”, para ver en tiempo real lo que va generando las anotaciones.

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@EqualsAndHashCode
@ToString
@AllArgsConstructor
@NoArgsConstructor

public class Persona {

	private String nombre;
	private String apellidos;
	private int edad;
	
	
}
// Pruebalo a continuación

Persona p = new Persona("Juan","Fernandez",30); 
System.out.println(p.getNombre());
Persona p2 = new Persona();	
p2.setNombre("Juan");
p2.setApellidos("Fernandez");
p2.setEdad(30);
System.out.println("Funciona el equals:" + p.equals(p2));
System.out.println(p2);

Con el uso de @Data, aún se quita más codigo:

@Data
@AllArgsConstructor
public class Persona {

	private String nombre;
	private String apellidos;
	private int edad;
}

Por tanto es una librería de inyención de código en tiempo de compilación. Con las nuevas versiones de Java también podéis usar los records para motivos similares.

Consola JShell

Es una consola shell REPL (interactiva) para Java, al estilo a las que hay en Phython por ejemplo. Enlace -> JShell