1 Introducción a la herencia¶
Introducción¶
La herencia permite definir una clase tomando como base a otra clase ya existente. Dicha clase base se conoce como superclase o clase padre y la clase que hereda se denomina subclase o clase hija. Por lo tanto, una subclase es una versión especializada de una superclase, ya que hereda tanto los atributos como los métodos definidos por la superclase y además añade los suyos propios.
Esto es una de las bases de la reutilización de código ya que cuando se quiere crear una clase nueva y ya existe una clase que incluye parte del código que queremos, podemos heredar nuestra nueva clase de la clase existente reutilizando los atributos y métodos de la misma. La herencia facilita el trabajo del programador porque permite crear clases estándar y a partir de ellas crear nuestras propias clases personales. Esto es más cómodo que tener que crear todas las clases desde cero.
Por ejemplo, si quisiéramos realizar una aplicación de vehículos, definiríamos una superclase o clase padre con lo común a todos los vehículos y luego definiríamos una subclase o clase hija para cada tipo de vehículo donde se añadiría lo particular de cada uno.
En java, la herencia se especifica en la subclase añadiendo la palabra extends seguida del nombre de la superclase. Por ejemplo, así sería para indicar que Coche es hija de Vehículo:
En Java, solamente se puede tener un padre pero puede hacer varios niveles de herencia, es decir, clases hijas que a su vez son padres de otras clases. Por ejemplo, el Car es hija de Vehicle pero puede ser padre de otra clase, como por ejemplo TodoTerreno:
Si el padre tiene algún atributo estático, también lo pueden usar los hijos. ClasePadre.AtributoEstático
y ClaseHijo.AtributoEstático
acceden a la misma variable porque es el mismo atributo estático.
Modificadores de acceso¶
En el tema 4. Programación Orientada a Objetos vimos los modificadores de acceso y cómo afectaban a la visibilidad. En este tema vamos a incorporar el modificador de acceso protected que es el que está pensado para la herencia.
Los modificadores de acceso afectan a la visibilidad y también afectan a la herencia. Visibilidad es lo que una clase puede ver de otra clase y herencia es lo que una clase hereda de otra clase. He aquí dos tablas con los modificadores de acceso, una para la visibilidad y otra para la herencia
Tabla de visibilidad
Private | Friendly | Protected | Public | |
---|---|---|---|---|
Misma clase | x | x | x | x |
Mismo paquete | x | x | x | |
Otro paquete | x | |||
Subclase en el mismo paquete | x | x | x | |
Subclase en distinto paquete | x |
Tabla de herencia
Private | Friendly | Protected | Public | |
---|---|---|---|---|
Subclase en el mismo paquete | x | x | x | |
Subclase en distinto paquete | x |
Si las clases están en un subpaquete, a efectos de visibilidad y herencia se considera que están en otro paquete.
Las conclusiones que se pueden obtener a partir de las dos tablas son las siguientes:
- La visibilidad es la misma independientemente de que la clase sea hija o no.
- En herencia, siempre se hereda el protected independientemente del paquete donde se encuentre la clase hija. Por lo tanto, cuando diseñemos una clase que vaya a tener descendientes, es conveniente declarar sus atributos como protected.
Un atributo private no se hereda pero si los getters y setters del padre tienen un modificador distinto de private, sí puede el hijo utilizar dicho atributo a través de dichos métodos. Pero no es conveniente programar de esta manera, es más adecuado utilizar el modificador protected.
public class Vehicle {
protected int wheelCount;
protected double speed;
protected String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getWheelCount() {
return wheelCount;
}
public double getSpeed() {
return speed;
}
public void accelerate(double amount){
speed += amount;
}
public void brake(double amount){
speed -= amount;
}
}
public class Car extends Vehicle{
/*
* Car hereda de Vehicle los atributos wheelCount, speed y color porque
son protected
* También hereda todos los métodos de Vehicle, ya que son public
* Además, puede tener atributos y métodos propios
*/
private double gasoline;
public double getGasoline() {
return gasoline;
}
public void refuel(double liters) {
gasoline += liters;
}
}
public class MainProtected {
public void show(){
Car car = new Car();
car.accelerate(100);
System.out.printf("La velocidad del coche es %.2f km/h\n", car.getSpeed());
car.refuel(40.35);
System.out.printf("El coche tiene %.2f litros de gasolina\n", car.getGasoline());
}
public static void main(String[] args) {
new MainProtected().show();
}
}
Sobrecarga y anulación de métodos¶
Se puede sobrecargar un método heredado para proporcionar una versión del mismo adaptado a las necesidades de la subclase.
public class Overload {
public void show(){
Car car = new Car();
car.accelerate(100); // Heredado
System.out.printf("La velocidad del coche es %.2f km/h\n", car.getSpeed());
car.accelerate(); // sobrecargado
System.out.printf("La velocidad del coche es %.2f km/h\n", car.getSpeed());
}
public static void main(String[] args) {
new Overload().show();
}
}
Si la subclase define un método con la misma firma que un método heredado, entonces anula o sobrescribe el método de la superclase. Veamos un ejemplo donde el coche ha anulado el método heredado de acelerar para añadirle el consumo de gasolina:
public class MainOverride {
public void show(){
Car car = new Car();
car.refuel(40.35);
System.out.printf("El coche tiene %.2f litros de gasolina\n", car.getGasoline());
car.accelerate(100);
System.out.printf("El coche tiene %.2f litros de gasolina\n", car.getGasoline());
}
public static void main(String[] args) {
new MainOverride().show();
}
}
En la clase Car, se puede observar la anotación @Override antes de la firma del método accelerate.
Las anotaciones de Java comienzan con @ y permiten incrustar información suplementaria en un programa para que pueda ser utilizada por varias herramientas.
La anotación @Override le indica al compilador que el método debe sobrescribir un método de la superclase. Si no lo hace, el compilador generará un error. Se utiliza para asegurar que un método de superclase esté anulado, y no simplemente sobrecargado. Es una manera de comprobar en tiempo de compilación que se está anulando correctamente un método, y de este modo evitar errores en tiempo de ejecución los cuales serían mucho más difíciles de detectar.
La visibilidad de lo que se hereda es con respecto al paquete de la superclase, no con respecto al paquete de la subclase. Si no interesa, la subclase tendrá que sobrescribir lo heredado aunque no haga ningún cambio para que la visibilidad sea con respecto al paquete de la subclase.
Veamos un ejemplo donde Vehicle está en un paquete distinto que Car. Vehicle tiene el método accelerate como protected, por lo tanto Car lo hereda aunque esté en otro paquete como podemos observar en la tabla de herencia. La clase Main se encuentra en el mismo paquete que Car y quiere acceder al método accelerate del mismo. Pero la visibilidad de accelerate es con respecto al paquete de Vehicle ya que la visibilidad de lo que se hereda es con respecto al paquete de la superclase, no con respecto al paquete de la subclase. Dicho método es protected y si nos fijamos en la tabla de visibilidad, un protected no es visible desde otro paquete, por lo que no se le va a permitir dando un error de compilación:
public class Vehicle {
protected int wheelCount;
protected double speed;
protected String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getWheelCount() {
return wheelCount;
}
public double getSpeed() {
return speed;
}
protected void accelerate(double amount){
speed += amount;
}
public void brake(double amount){
speed -= amount;
}
}
public class Visibility {
public void show(){
exercises.bloque_ii.tema_07.modifier_protected.Car car = new Car();
car.accelerate(100); // Error de compilación porque no es visible
System.out.printf("La velocidad del coche es %.2f km/h\n", car.getSpeed());
car.refuel(40.35);
System.out.printf("El coche tiene %.2f litros de gasolina\n", car.getGasoline());
}
public static void main(String[] args) {
new Visibility().show();
}
}
La solución es que Car anule el método accelerate heredado de Vehicle para que la visibilidad de dicho método sea con respecto al paquete de Car. Car no va a realizar ningún cambio en dicho método, es decir, lo va a anular para dejarlo exactamente igual, pero de esta forma modifica el paquete para la visibilidad:
public class Car extends Vehicle{
// ...
@Override
protected void accelerate(double amount){
speed += amount;
}
}
Pero, ¿qué ocurriría si la clase Main estuviera en un paquete distinto a Car? Daría un error de compilación porque un protected no es visible desde otro paquete.
La solución sería que Car cambiara la visibilidad del método. Si una subclase quiere anular algún método de la superclase para cambiar la visibilidad, se permite únicamente si amplia la visibilidad, no si la reduce. La escala de valores de más restrictivo a menos es: private, friendly, protected y public. Por ejemplo, no se puede cambiar de protected a friendly pero sí al revés. Solucionemos el ejemplo anterior para que la clase Main pueda acceder al método accelerate de Car. Si nos fijamos en la tabla de visibilidad, el único modificador que nos permite visibilidad desde otro paquete es public. Entonces, tendríamos que cambiar el protected a public y se permite porque se amplia la visibilidad, no se reduce: