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.
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:
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.
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
.
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
.
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
.
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:
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()
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.
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:
final
o static
.
Si incumplimos alguna de estas reglas y estamos usando el tag @Override
, el compilador nos lo indicará.
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.
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