Herramientas de usuario

Herramientas del sitio


bloque3:herencia

Herencia de Clases

En Java podemos tener clases que heredan de otras (extends, o extienden). Cuando una clase hereda de otra puede hacer uso de los miembros de su clase padre. Además, permite sobrescribir métodos en caso de que en una subclase queramos concretar su funcionamiento.

Debemos tener en cuenta que toda clase en Java hereda de la clase Object de forma directa o indirecta, y que por tanto hay una serie de métodos que heredan todas las clases.

¿Para qué sirve?

La herencia es un mecanismo mediante el cual aprovechamos la implementación de operaciones de una clase padre (reutilización de código) y creamos nuevas clases hijas a partir de esa clase:

  • Heredando y posiblemente modificando y/o añadiendo operaciones.
  • Heredando y posiblemente añadiendo atributos.

Debemos tener en cuenta que un método o atributo heredado mediante el mecanismo de herencia no puede ser suprimido en la subclase.

Podríamos decir que la herencia nos permite usar el código creado en una clase para no tener que volverlo a escribir en una subclase. Todas las subclases tienen las operaciones de la superclase, y así no tenemos que repetir el código en cada una.

Otro planteamiento seria, si me encuentro con la necesidad de tener dos clases cuyas operaciones (métodos) son iguales, pero que cada una tiene otras operaciones propias diferentes, debo concluir que esas dos clases tienen un parte en común, la cual debería definirse en una superclase, permitiendo a estas subclases implementar solo las operaciones específicas de cada una.

Declaración de una Subclase

Si entendemos que una clase son “los planos” que definen la creación de un ejemplar (objeto, instancia) de esa clase, podemos entender que cuando una clase hereda de otra, lo que está haciendo es usar los planos de la clase padre, y añadirle más información para crear más o diferentes funcionalidades de las que tenía.

Podemos llamar a las clases hijas subclases y a la clase padre, superclase. Para que una subclase herede de una superclase debe usarse extends en la definición de la subclase.

Ejemplo de superclase: Vehiculo. Hay muchos tipos de vehículos (avión, moto agua, coche, moto). Es una clase estándar como cualquier otra.

public class Vehiculo{
   private int km;
   private String matricula;
 
   public int getKm(){
      return km;
   }
 
   public void setKm(int km){
      this.km = km;
   }
 
   public String getMatricula(){
      return matricula;
   }
 
   public void setMatricula(String matricula){
      this.matricula = matricula;
   }
}

Ahora vemos su uso y vemos que aparecen los métodos getKm(), setKm(), getMatricula() y setMatricula().

Además del resto de métodos heredados de la clase Object.


Palabra clave Extends

Vamos a crear ahora la clase Coche que extiende las funcionalidades de la clase Vehiculo:

public class Coche extends Vehiculo{
 
   //Campo específico de Coche
   private int cilindrada;
 
   //Metodos específicos de Coche
   public void setCilindrada(int cilindrada){
      this.cilindrada = cilindrada;
   }
 
   public int getCilindrada(){
      return cilindrada;
   }
}

Aquí vemos como aparecen los métodos heredados de la superclase Vehiculo: getKm(), setKm(), getMatricula() y setMatricula().

Además tiene sus propios miembros específicos setCilindrada() y getCilindrada().

También están los miembros heredados de la clase Object, comunes a todas las clases. Ambas clases (Vehiculo y Coche) tienen el constructor estándar sin parámetros.


Una subclase solo puede acceder a los miembros de la superclase que sean public o protectec.

Impedir la herencia

Si queremos que una clase no permita extender su funcionalidad, o lo que es lo mismo, no permita ser heredada, definiremos dicha clase como final.

public final class Vehiculo{
   ...
}

La palabra clave final la hemos usado en otras ocasiones para crear variables que no se puedan modificar (constantes), y también se aplica a las clases y a los métodos con dicha finalidad.

Por ejemplo las clases String o Math no se pueden heredar, ya que están definidas como final.

Constructor de la subclase

Los constructores no se heredan. Siempre que se crea una instancia de una subclase, se crea primero una instancia de la superclase. Esta instanciación puede ser implícita, o explícita, o lo que es lo mismo, invisible a nivel de código, o visible.

En una clase se puede definir un constructor de forma explícita, o no definir ninguno. Cuando no definimos un constructor, Java crea implícitamente uno. No tiene parámetros, y no hace nada, pero sirve para crear instancias de la clase. Se conoce como constructor no-args, sin argumentos (parámetros).

Atendiendo a lo anterior, con los constructores de la subclase puede ocurrir 2 cosas:

  • La superclase tiene un constructor sin parámetros (ya sea el que define Java de forma implícita cuando no hemos definido ninguno, o uno definido por nosotros que no tiene parámetros). En este caso en la subclase podemos definir un constructor, o no definir ninguno (Java crea un constructor no-args implícitamente). Cada vez que creemos una instancia de la subclase, Java llamará automáticamente al constructor de la superclase de forma implícita, invisible.
  • La superclase no tiene un constructor sin parámetros (o sea, se ha definido uno con parámetros). En este caso Java no puede llamar de forma implícita al constructor de la superclase, ya que necesita saber qué valor dar a sus parámetros. Entonces estamos obligados a crear también un constructor en la subclase, cuya primera instrucción es una llamada al constructor de la superclase. Esto se hace usando la instrucción super().

Palabra clave super

Si en cualquier clase tenemos la palabra this que representa una referencia al objeto actual de la propia clase, en una subclase tenemos además la palabra super que contiene una referencia al objeto actual de la superclase.

Esto tiene su explicación en que, para crear una instancia de una subclase, obligatoriamente se debe crear primero una instancia de la superclase (explícita o implícitamente).

A la hora de crear un constructor para la subclase, es necesario llamar al constructor de la superclase. Cuando el constructor/es de la superclase recibe parámetros, no nos queda más remedio que llamarlo nosotros:

public class Vehiculo{
   ...
 
   //Constructor de la superclase
   public Vehiculo(int km, String matricula) {
      this.km = km;
      this.matricula = matricula;
   }
}
........
 
public class Coche extends Vehiculo{
   ...
 
   //Constructor de la subclase
   public Coche(int km, String matricula) {
      super(km, matricula);
   }
 
   //Ó podemos tener también otro constructor en la subclase
   public Coche(int km, String matricula, int cilindrada) {
      super(km, matricula);
      this.cilindrada = cilindrada; //Atributo específico de la subclase.
   }
}

Como vemos, si una clase hereda de otra, y la superclase solo tiene constructores con argumentos, la subclase debe llamar a dicho constructor con al menos el mismo número de argumentos, aunque puede usar más. Una vez vez que se crea el constructor de la superclase, podemos crear los constructores de la subclase.

Si una superclase no tiene contructor no-args, la subclase debe tener al menos los mismos constructores, ya que estos no se heredan.

La palabra super también me permite acceder a los miembros de una superclase → super.arrancar()

Sobrescritura de métodos: Override

Cada vez que una clase hereda un método, se tiene la posibilidad de sobrescribir el método. La sobrescritura de métodos se usa para definir el comportamiento específico de un método que tiene un comportamiento demasiado general en la superclase. Esto se denomina Override.

//Superclase Vehiculo
public class Vehiculo{
   ...
   public void arrancar(){
      System.out.println("Vehiculo arrancado...");
   }
}
 
//Subclase Coche
public class Coche extends Vehiculo{
   ...
   @Override
   public void arrancar(){
      //modificamos el comportamiento del método
      System.out.println("Coche arrancado...");
   }
}
 
//Clase con método main
...
public static void main(String[] args){
   Vehiculo miVehiculo = new Vehiculo();
   Coche miCoche = new Coche();
 
   miVehiculo.arrancar();  //Muestra: "Vehiculo arrancado..."
   miCoche.arrancar();     //Muestra: "Coche arrancado..."
}

Cuando se llama a un método, este se busca primero en su propia clase, y si no está, se buscará en la superclase, y así sucesivamente hasta llegar a la clase Object. Si no está en ninguna clase, el compilador nos lo indica como un error.

Etiqueta @Override

Cuando sobrescribimos un método es aconsejable (aunque no obligatorio) indicarle sobre su declaración, la etiqueta @Override:

@Override
public String toString(){
   return "Coche- matricula : " + matricula + " cilindrada: " + cilindrada;
}

Existen una serie de normas que se deben cumplir al sobrescribir un método, y la etiqueta @Override nos ayuda a cumplirlas:

  • Los parámetros del método sobrescrito deben ser exactamente iguales que los del original (tipo y cantidad). Si no, estaríamos sobrecargándolo (2 métodos distintos con el mismo nombre).
  • El tipo de retorno debe ser el mismo o un subtipo del tipo de retorno del método de la superclase.
  • El modificador de visibilidad no puede ser más restrictivo que el original, aunque si menos restrictivo.
  • No se puede sobrescribir un método marcado como final o static.

Si incumplimos alguna de estas reglas y estamos usando el tag @Override, el compilador nos lo indicará.

Impedir el Override de un método

Si quiero que un método no pueda ser sobrescrito (redefinido) en una subclase, debo indicarle en su definición el modificador final:

public class Vehiculo{
   public final void arrancar(){
      System.out.println("Vehiculo arrancado...");
   }
}

Ahora ninguna subclase de Vehiculo puede sobrescribir el método: está obligada a usar el método original de Vehículo sin poder modificarlo.

Modificador Protected

Mediante el mecanismo de herencia, una subclase solo puede acceder a los miembros de su superclase que tengan visibilidad public o protected.

El modificador de visibilidad protected actúa igual que el modificador private, pero permite que una subclase puede acceder a los miembros marcados como protected, incluso aunque estén en paquetes diferentes.

Un miembro protected permite ser accedido por la misma clase o por sus subclases, mientras que private solo permite por la propia clase.

public class Vehiculo{
   private int kilometros;  //Ninguna clase más tiene acceso a él
   protected int matricula; //La subclase tiene acceso
 
   protected void arrancar(){
      System.out.println("Vehiculo arrancado...");
   }
}
 
public class Coche extends Vehiculo{
 
   public void mostrarDatos(){
      //puedo acceder al atributo matricula
      System.out.println("La matricula del coche es: " + matricula);
 
      //No puedo acceder al atributo kilometros
      System.out.println("Los km del coche son: " + kilometros); //Error de compilación
   }
 
   //Sobrescribo el método arrancar y lo hago público
   @Override
   public void arrancar(){
      System.out.println("Coche arrancado...");
   }
 
 
//Clase con método main
public static void main(String[] args){
   Vehiculo miVehiculo = new Vehiculo();
   Coche miCoche = new Coche();
 
   miVehiculo.arrancar(); //Error de compilación: no tengo acceso
   miCoche.arrancar(); //Muestra: "Coche arrancado...");
   miCoche.mostrarDatos(); //Muestra la matrícula
}

© 2024 Fernando Valdeón

bloque3/herencia.txt · Última modificación: 16/09/2024 20:53 por 127.0.0.1