Herencia en Java.

Herencia, Clases Abstractas, Clases Finales, Casting, operador instanceof

La herencia es una de las características fundamentales de la Programación Orientada a Ojetos.
Mediante la herencia podemos definir una clase a partir de otra ya existente.
La clase nueva se llama clase derivada o subclase y la clase existente se llama clase base o superclase.
En UML la herencia se representa con una flecha apuntando desde la clase derivada a la clase base.
La clase derivada hereda los componentes (atributos y métodos) de la clase base.
La finalidad de la herencia es:
-           Extender la funcionalidad de la clase base: en la clase derivada se pueden añadir atributos y métodos nuevos.
-           Especializar el comportamiento de la clase base: en la clase derivada se pueden modificar (sobrescribir, override) los métodos heredados para adaptarlos a sus necesidades.
La herencia permite la reutilización del código, ya que evita tener que reescribir de nuevo una clase existente cuando necesitamos ampliarla en cualquier sentido. Todas las clases derivadas pueden utilizar el código de la clase base sin tener que volver a definirlo en cada una de ellas.
Reutilización de código: El código se escribe una vez en la clase base y se utiliza en todas las clases derivadas.
Una clase base puede serlo de tantas derivadas como se desee: Un solo padre, varios hijos.

Herencia múltiple en Java: Java no soporta la herencia múltiple. Una clase derivada solo puede tener una clase base.
Diagrama UML de herencia múltiple no permitida en Java

La herencia expresa una relación “ES UN/UNA” entre la clase derivada y la clase base.
Esto significa que un objeto de una clase derivada es también un objeto de su clase base.
Al contrario NO es cierto. Un objeto de la clase base no es un objeto de la clase derivada.
Por ejemplo, supongamos una clase Vehiculo como la clase base de una clase Coche. Podemos decir que un Coche es un Vehiculo pero un Vehiculo no siempre es un Coche, puede ser una moto, un camión, etc.
Un objeto de una clase derivada es a su vez un objeto de su clase base, por lo tanto se puede utilizar en cualquier lugar donde aparezca un objeto de la clase base. Si esto no fuese posible entonces la herencia no está bien planteada.
Ejemplo de herencia bien planteada:
A partir de una clase Persona que tiene como atributos el nif y el nombre, podemos obtener una clase derivada Alumno. Un Alumno es una Persona que tendrá como atributos nif, nombre y curso.
Ejemplo de herencia mal planteada:
Supongamos una clase Punto que tiene como atributos coordenadaX y coordenadaY.
Se puede crear una clase Linea a partir de la clase Punto. Simplificando mucho para este ejemplo, podemos considerar una línea como un punto de origen y una longitud. En ese caso podemos crear la Clase Linea como derivada de la clase Punto, pero el planteamiento no es correcto ya que no se cumple la relación ES UN
Una Linea NO ES un Punto. En este caso no se debe utilizar la herencia.


Una clase derivada a su vez puede ser clase base en un nuevo proceso de derivación, formando de esta manera una Jerarquía de Clases.

Las clases más generales se sitúan en lo más alto de la jerarquía. Cuánto más arriba en la jerarquía, menor nivel de detalle.
Cada clase derivada debe implementar únicamente lo que la distingue de su clase base.
En java todas las clases derivan directa o indirectamente de la clase Object.
Object es la clase base de toda la jerarquía de clases Java.
Todos los objetos en un programa Java son Object.
CARACTERÍSTICAS DE LAS CLASES DERIVADAS
Una clase derivada hereda de la clase base sus componentes (atributos y métodos).
Los constructores no se heredan. Las clases derivadas deberán implementar sus propios constructores.
Una clase derivada puede acceder a los miembros públicos y protegidos de la clase base como si fuesen miembros propios.
Una clase derivada no tiene acceso a los miembros privados de la clase base. Deberá acceder a través de métodos heredados de la clase base.
Si se necesita tener acceso directo a los miembros privados de la clase base se deben declarar protected en lugar de private en la clase base.
Una clase derivada puede añadir a los miembros heredados, sus propios atributos y métodos (extender la funcionalidad de la clase).
También puede modificar los métodos heredados (especializar el comportamiento de la clase base).
-           Una clase derivada puede, a su vez, ser una clase base, dando lugar a una jerarquía de clases. LOS MODIFICADORES DE ACCESO
MODIFICADORES DE ACCESO JAVA

ACESO EN
MODIFICADOR
PROPIA CLASE
PACKAGE
CLASE DERIVADA
RESTO
private
SI
NO
NO
NO
<Sin modificador>
SI
SI
NO
NO
protected
SI
SI
SI
NO
public
SI
SI
SI
SI
HERENCIA EN JAVA. SINTAXIS
La herencia en Java se expresa mediante la palabra extends
Por ejemplo, para declarar una clase B que hereda de una clase A:
public class B extends A{
.
}
Ejemplo de herencia Java: Disponemos de una clase Persona con los atributos nif y nombre.
//Clase Persona. Clase Base
public class Persona {
    private String nif;
    private String nombre;
    public String getNif() {
        return nif;
    }
    public void setNif(String nif) {
        this.nif = nif;
    }
    public String getNombre() {
        return nombre;
    }
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
}
Queremos crear ahora una clase Alumno con los atributos nif, nombre y curso. Podemos crear la clase Alumno como derivada de la clase Persona. La clase Alumno contendrá solamente el atributo curso, el nombre y el nif son los heredados de Persona:
//Clase Alumno. Clase derivada de Persona
public class Alumno extends Persona{
    private String curso;
    public String getCurso() {
        return curso;
    }
    public void setCurso(String curso) {
        this.curso = curso;
    }  
}
La clase alumno hereda los atributos nombre y nif de la clase Persona y añade el atributo propio curso. Por lo tanto:
Los atributos de la clase Alumno son nif, nombre y curso.
Los métodos de la clase Alumno son: getNif(), setNif(String nif), getNombre(), setNombre(String nombre), getCurso(), setCurso(String curso).
La clase Alumno aunque hereda los atributos nif y nombre, no puede acceder a ellos de forma directa ya que son privados a la clase Persona. Se acceden a través de los métodos heredados de la clase base.
La clase Alumno puede utilizar los componentes public y protected de la clase Persona como si fueran propios.
Ejemplo de uso de la clase Alumno:

En una jerarquía de clases, cuando un objeto invoca a un método:
1. Se busca en su clase el método correspondiente.
2. Si no se encuentra, se busca en su clase base.
3. Si no se encuentra se sigue buscando hacia arriba en la jerarquía de clases hasta que el método se encuentra.
4. Si al llegar a la clase base raíz el método no se ha encontrado se producirá un error.
REDEFINIR MIEMBROS DE LA CLASE BASE EN LAS CLASES DERIVADAS
Redefinir o modificar métodos de la clase Base (Override)
Los métodos heredados de la clase base se pueden redefinir (también se le llama modificar o sobreescribir) en las clases derivadas.
El método en la clase derivada se debe escribir con el mismo nombre, el mismo número y tipo de parámetros y el mismo tipo de retorno que en la clase base. Si no fuera así estaríamos sobrecargando el método, no redefiniéndolo.
El método sobrescrito puede tener un modificador de acceso menos restrictivo que el de la clase base. Si por ejemplo el método heredado es protected se puede redefinir como public pero no como private porque sería un acceso más restrictivo que el que tiene en la clase base.
Cuando en una clase derivada se redefine un método de una clase base, se oculta el método de la clase base y todas las sobrecargas del mismo en la clase base.
Si queremos acceder al método de la clase base que ha quedado oculto en la clase derivada utilizamos:
super.metodo();
Si se quiere evitar que un método de la clase Base sea modificado en la clase derivada se debe declarar como método final. Por ejemplo:
public final void metodo(){
........
}
Ejemplo:
Vamos a añadir a la clase Persona un método leer() para introducir por teclado los valores a los atributos de la clase. La clase Persona queda así:
public class Persona {
    private String nif;
    private String nombre;
    public String getNif() {
        return nif;
    }
    public void setNif(String nif) {
        this.nif = nif;
    }
    public String getNombre() {
        return nombre;
    }
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
    public void leer(){
        Scanner sc = new Scanner(System.in);
        System.out.print("Nif: ");
        nif = sc.nextLine();
        System.out.print("Nombre: ");
        nombre = sc.nextLine();     
    }
}
La clase Alumno que es derivada de Persona, heredará este método leer() y lo puede usar como propio.
Podemos crear un objeto Alumno:
Alumno a = new Alumno();
y utilizar este método:
a.leer();
pero utilizando este método solo se leen por teclado el nif y el nombre. En la clase Alumno se debe sobreescribir o modificar este método heredado para que también se lea el curso. El método leer() de Alumno invocará al método leer() de Persona para leer el nif y nombre y a continuación se leerá el curso.
La clase Alumno con el método leer() modificado queda así:
//Clase Alumno. Clase derivada de Persona
public class Alumno extends Persona{
    private String curso;
    public String getCurso() {
        return curso;
    }
    public void setCurso(String curso) {
        this.curso = curso;
    }
    @Override  //indica que se modifica un método heredado
    public void leer(){
        Scanner sc = new Scanner(System.in);
        super.leer(); //se llama al método leer de Persona para leer nif y nombre
        System.out.print("Curso: ");
        curso = sc.nextLine(); //se lee el curso
    }  
}
Como se ha dicho antes, cuando en una clase derivada se redefine un método de una clase base, se oculta el método de la clase base y todas las sobrecargas del mismo en la clase base. Por eso para ejecutar el método leer() de Persona se debe escribir super.leer();
Redefinir atributos de la clase Base
Una clase derivada puede volver a declarar un atributo heredado (Atributo public o protected en la clase base). En este caso el atributo de la clase base queda oculto por el de la clase derivada.
Para acceder a un atributo de la clase base que ha quedado oculto en la clase derivada se escribe: super.atributo;
CONSTRUCTORES Y HERENCIA EN JAVA. CONSTRUCTORES EN CLASES DERIVADAS
Los constructores no se heredan. Cada clase derivada tendrá sus propios constructores.
La clase base es la encargada de inicializar sus atributos.
La clase derivada se encarga de inicializar solo los suyos.
Cuando se crea un objeto de una clase derivada se ejecutan los constructores en este orden:
1. Primero se ejecuta el constructor de la clase base
2. Después se ejecuta el constructor de la clase derivada.
Esto lo podemos comprobar si añadimos a las clases Persona y Alumno sus constructores por defecto y hacemos que cada constructor muestre un mensaje:
public class Persona {
    private String nif;
    private String nombre;
    public Persona() {
        System.out.println("Ejecutando el constructor de Persona");
    }
    /////// Resto de métodos
}

public class Alumno extends Persona{
    private String curso;
    public Alumno() {
        System.out.println("Ejecutando el constructor de Alumno");
    }
    /////// Resto de métodos
}
Si creamos un objeto Alumno:
Alumno a = new Alumno();
Se muestra por pantalla:
Ejecutando el constructor de Persona
Ejecutando el constructor de Alumno
Cuando se invoca al constructor de la clase Alumno se invoca automáticamente al constructor de la clase Persona y después continúa la ejecución del constructor de la clase Alumno.
El constructor por defecto de la clase derivada llama al constructor por defecto de la clase base.
La instrucción para invocar al constructor por defecto de la clase base es:   super();
Todos los constructores en las clases derivadas contienen de forma implícita la instrucción super() como primera instrucción.
public Alumno() {
        super(); //esta instrucción se ejecuta siempre. No es necesario escribirla
        System.out.println("Ejecutando el constructor de Alumno");
}  
Cuando se crea un objeto de la clase derivada y queremos asignarle valores a los atributos heredados de la clase base:
-           La clase derivada debe tener un constructor con parámetros adecuado que reciba los valores a asignar a los atributos de la clase base.
-           La clase base debe tener un constructor con parámetros adecuado.
-           El constructor de la clase derivada invoca al constructor con parámetros de la clase base y le envía los valores iniciales de los atributos. Debe ser la primera instrucción.
-           La clase base es la encargada de asignar valores iniciales a sus atributos.
-           A continuación el constructor de la clase derivada asigna valores a los atributos de su clase.
Ejemplo: en las clases Persona y Alumno anteriores añadimos constructores con parámetros:
Ahora se pueden crear objetos de tipo Alumno y asignarles valores iniciales. Por ejemplo:
Alumno a = new Alumno("12345678-Z","Eliseo Gonzáles Manzano","1DAW");
CLASES FINALES
Si queremos evitar que una clase tenga clases derivadas debe declararse con el modificador final delante de class:
public final class A{
         ........
}
Esto la convierte en clase final. Una clase final no se puede heredar.
Si intentamos crear una clase derivada de A se producirá un error de compilación:
public class B extends A{ //extends A producirá un error de compilación
         ........
}
CLASES ABSTRACTAS
Una clase abstracta es una clase que NO se puede instanciar, es decir, no se pueden crear objetos de esa clase.
Se diseñan solo para que otras clases hereden de ella.
La clase abstracta normalmente es la raíz de una jerarquía de clases y contendrá el comportamiento general que deben tener todas las subclases. En las clases derivadas se detalla la implementación.
Las clases abstractas:
- Pueden contener cero o más métodos abstractos.
- Pueden contener métodos no abstractos.                                      
- Pueden contener atributos.
Todas las clases que hereden de una clase abstracta deben implementar todos los métodos abstractos heredados.
Si una clase derivada de una clase abstracta no implementa algún método abstracto se convierte en abstracta y tendrá que declararse como tal (tanto la clase como los métodos que siguen siendo abstractos).
Aunque no se pueden crear objetos de una clase abstracta, sí pueden tener constructores para inicializar sus atributos que serán invocados cuando se creen objetos de clases derivadas.
La forma general de declarar una clase abstracta en Java es:
[modificador] abstract class nombreClase{
    ……….
}
Ejemplo de clase Abstracta en Java: Clase Polígono.
//Clase abstracta Poligono
public abstract class Poligono {
  
    private int numLados;

    public Poligono() {
    }

    public Poligono(int numLados) {
        this.numLados = numLados;
    }

    public int getNumLados() {
        return numLados;
    }

    public void setNumLados(int numLados) {
        this.numLados = numLados;
    }
    //Declaración del método abstracto area()
    public abstract double area();
}
La clase Poligono contiene un único atributo nublados. Es una clase abstracta porque contiene el método abstracto area().
A partir de la clase Poligono vamos a crear dos clases derivadas Rectangulo y Triangulo. Ambas deberán implementar el método area(). De lo contrario también serían clases abstractas.
El diagrama UML es el siguiente:


En UML las clases abstractas y métodos abstractos se escriben con su nombre en cursiva.

//Clase Rectangulo
public class Rectangulo extends Poligono{

    private double lado1;
    private double lado2;

    public Rectangulo() {
    }

    public Rectangulo(double lado1, double lado2) {
        super(2);
        this.lado1 = lado1;
        this.lado2 = lado2;
    }

    public double getLado1() {
        return lado1;
    }

    public void setLado1(double lado1) {
        this.lado1 = lado1;
    }

    public double getLado2() {
        return lado2;
    }

    public void setLado2(double lado2) {
        this.lado2 = lado2;
    }

    //Implementación del método abstracto area()
    //heredado de la clase Polígono
    @Override
    public double area(){
        return lado1 * lado2;
    }
}

//Clase Triangulo
public class Triangulo extends Poligono{

    private double lado1;
    private double lado2;
    private double lado3;

    public Triangulo() {
    }

    public Triangulo(double lado1, double lado2, double lado3) {
        super(3);
        this.lado1 = lado1;
        this.lado2 = lado2;
        this.lado3 = lado3;
    }


    public double getLado1() {
        return lado1;
    }

    public void setLado1(double lado1) {
        this.lado1 = lado1;
    }

    public double getLado2() {
        return lado2;
    }
    public void setLado2(double lado2) {
        this.lado2 = lado2;
    }

    public double getLado3() {
        return lado3;
    }

    public void setLado3(double lado3) {
        this.lado3 = lado3;
    }
   
    //Implementación del método abstracto area()
    //heredado de la clase Polígono
    @Override
    public double area(){
        double p = (lado1+lado2+lado3)/2;
        return Math.sqrt(p * (p-lado1) * (p-lado2) * (p-lado3));
    }
}
Ejemplo de uso de las clases:
public static void main(String[] args) {
        Triangulo t = new Triangulo(3.25,4.55,2.71);
        System.out.printf("Área del triángulo: %.2f %n" , t.area());
        Rectangulo r = new Rectangulo(5.70,2.29);
        System.out.printf("Área del rectángulo: %.2f %n" , r.area());
}
CASTING: CONVERSIONES ENTRE CLASES
UpCasting: Conversiones implícitas
La herencia establece una relación ES UN entre clases. Esto quiere decir que un objeto de una clase derivada es también un objeto de la clase base.
Por esta razón:
Se puede asignar de forma implícita una referencia a un objeto de una clase derivada a una referencia de la clase base. Son tipos compatibles.
También se llaman conversiones ascendentes o upcasting.
En el ejemplo anterior un triángulo en un Poligono y un cuadrado es un Poligono.
Poligono p;  
Triangulo t = new Triangulo(3,5,2);
Como un triángulo es un polígono, se puede hacer esta asignación:
p = t;
La variable p de tipo Poligono puede contener la referencia de un objeto Triangulo ya que son tipos compatibles.
Cuando manejamos un objeto a través de una referencia a una superclase (directa o indirecta) solo se pueden ejecutar métodos disponibles en la superclase.
En el ejemplo, la instrucción
p.getLado1();
provocará un error ya que p es de tipo Poligono y el método getLado1() no es un método de esa clase.
Cuando manejamos un objeto a través de una referencia a una superclase (directa o indirecta) y se invoca a un método que está redefinido en las subclases se ejecuta el método de la clase a la que pertenece el objeto no el de la referencia.
En el ejemplo, la instrucción
p.area();
ejecutará el método area() de Triángulo.
DownCasting: Conversiones explícitas
Se puede asignar una referencia de la clase base a una referencia de la clase derivada, siempre que la referencia de la clase base sea a un objeto de la misma clase derivada a la que se va a asignar o de una clase derivada de ésta.
También se llaman conversiones descendentes o downcasting.
Esta conversión debe hacerse mediante un casting.
Siguiendo con el ejemplo anterior:
Poligono p = new Triangulo(1,3,2);  //upcasting
Triangulo t;
t = (Triangulo) p; //downcasting
Esta asignación se puede hacer porque p contiene la referencia de un objeto triángulo.
Las siguientes instrucciones provocarán un error de ejecución del tipo ClassCastException:
Triangulo t;
Poligono p1 = new Rectangulo(3,2);
t = (Triangulo)p1;   //----> Error de ejecución
p1 contiene la referencia a un objeto Rectangulo y no se puede convertir en una referencia a un objeto Triangulo. No son tipos compatibles.
EL OPERADOR instanceof
Las operaciones entre clases y en particular el downcasting requieren que las clases sean de tipos compatibles. Para asegurarnos de ello podemos utilizar el operador instanceof.
instanceof devuelve true si el objeto es instancia de la clase y false en caso contrario.
La sintaxis es:
Objeto instanceof Clase
Ejemplo:
Triangulo t;
Poligono p1 = new Rectangulo(3,2);
if(p1 instanceof Triangulo)
   t = (Triangulo)p1;
else
   System.out.println("Objetos incompatibles");