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…

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).