Herencia
Herencia
El paradigma orientado a objetos plantea el concepto de la herencia como una poderosa característica que otorga la capacidad de crear nuevas clases partiendo de otra definida previamente. Esto permite que la nueva clase herede sus atributos y métodos.
Esta característica nos permite reutilizar código, pero sobre todo, nos permite especializar las clases. Es decir, en lugar de crear una clase desde cero o modificar una clase existente, podemos crear una nueva clase que extienda las características y/o comportamientos de otra.
- La clase de la cual se hereda se conoce como clase padre, clase base o superclase.
- La clase que hereda se conoce como clase hija, clase derivada o subclase.
En Java, la herencia se define mediante la palabra clave extends
empleando la siguiente sintaxis:
1class <Clase Hija> extends <Clase Padre> {2 // ...3}
Como vemos, al definir una clase, podemos indicar que esta hereda de otra. Con lo cual todos los atributos y métodos de la clase padre, serán heredados por la clase hija también.
Para ver este concepto de manera más clara, estableceremos la superclase Vehicle
, de la cual heredaremos luego.
1class Vehicle {2 private String brand;3 private String model;4 private int year;5
6 public Vehicle(String brand, String model, int year) {7 this.brand = brand;8 this.model = model;9 this.year = year;10 }11
12 public String getBrand() {13 return brand;14 }15
16 public String getModel() {17 return model;18 }19
20 public int getYear() {21 return year;22 }23}
No obstante, podríamos tener la necesidad de crear nuevas clases que tienen en común los atributos y métodos de la clase Vehicle
, pero que además tienen atributos y/o métodos propios. Por ejemplo, podríamos tener la necesidad de crear una clase Car
.
1class Car extends Vehicle {2 private int doors;3
4 public Car(String brand, String model, int year, int doors) {5 super(brand, model, year);6 this.doors = doors;7 }8
9 public int getDoors() {10 return doors;11 }12}
En este ejemplo podemos notar, que al momento de definir el constructor de la clase Car
, estamos utilizando la palabra reservada super
.
En este caso, empleamos super
para acceder al constructor de la clase Vehicle
. De esta forma, delegamos la tarea de inicializar los atributos brand
, model
y year
a la clase padre, y nos concentramos solo en inicializar los atributos propios de la clase Car
.
De igual manera, podríamos usar super
para acceder otros métodos de la clase padre. Por ejemplo, dentro de la clase Car
, podríamos invocar el método getBrand()
de la clase Vehicle
de la siguiente forma:
1super.getBrand();
Así también, podríamos definir tantas clases hijas como necesitemos. Por ejemplo, podríamos definir la clase Motorcycle
de la siguiente forma:
1class Motorcycle extends Vehicle {2 private int cc;3
4 public Motorcycle(String brand, String model, int year, int cc) {5 super(brand, model, year);6 this.cc = cc;7 }8
9 public int getCc() {10 return cc;11 }12}
Sobrecarga y sobreescritura de métodos
La sobrecarga de métodos es una característica de la que hemos hablado anteriormente, y que nos permite definir varios métodos con el mismo nombre pero con diferentes firmas.
Por otro lado, en el contexto de la herencia, podemos encontrarnos con un concepto similar, conocido como sobreescritura de métodos.
A diferencia de la sobrecarga de métodos, la sobreescritura de métodos nos permite redefinir un método heredado de la clase padre. Es decir, estamos modificando un método heredado, para adaptarlo a las necesidades de la clase hija.
Establezcamos un nuevo ejemplo para ver este concepto de manera más clara. En este caso, definiremos la clase Character
, la cual representa un personaje de un videojuego.
1class Character {2 private String name;3 private int level;4 private int health;5
6 public Character(String name, int level, int health) {7 this.name = name;8 this.level = level;9 this.health = health;10 }11
12 public String getName() {13 return name;14 }15
16 public int getLevel() {17 return level;18 }19
20 public int getHealth() {21 return health;22 }23
24 public void attack() {25 System.out.println("Atacando...");26 System.out.println("Daño: 10");27 }28}
Ahora, definiremos la clase Warrior
, la cual representa un guerrero. Es decir, un personaje especializado en el combate cuerpo a cuerpo. Con esto en mente, podemos pensar que el método attack()
de la clase Character
no es suficiente para representar el ataque de un guerrero y nos gustaría que el daño que este realiza dependa de su nivel y de un nuevo atributo que definiremos, su fuerza.
Para esto nos veremos en la necesidad de sobreescribir el método attack()
, para adaptarlo a las necesidades de la clase Warrior
. En Java, podemos sobreescribir un método de la clase padre mediante la anotación @Override
.
1class Warrior extends Character {2 private int strength;3
4 public Warrior(String name, int level, int health, int strength) {5 super(name, level, health);6 this.strength = strength;7 }8
9 public int getStrength() {10 return strength;11 }12
13 @Override14 public void attack() {15 System.out.println("Atacando...");16 System.out.println("Daño: " + (10 * getLevel() + getStrength()));17 }18}
Como vemos, antes de definir el método attack()
, hemos agregado la anotación @Override
. Esta anotación nos permite indicarle al compilador que estamos sobreescribiendo un método de la clase padre. De esta forma, podremos realizar la misma operación tanto para objetos de la clase Character
como para objetos de la clase Warrior
.
De la misma manera, podemos sobreescibir métodos de la clase padre en cualquier otra clase hija. Por ejemplo, podríamos definir la clase Wizard
, la cual representa un mago. Es decir, un personaje especializado en el combate a distancia. Con esto en mente, nos gustaría que el daño que este realiza dependa de su nivel y de un nuevo atributo que definiremos, su mana
.
1class Wizard extends Character {2 private int mana;3
4 public Wizard(String name, int level, int health, int mana) {5 super(name, level, health);6 this.mana = mana;7 }8
9 public int getMana() {10 return mana;11 }12
13 public void setMana(int mana) {14 this.mana = mana;15 }16
17 @Override18 public void attack() {19 System.out.println("Atacando...");20 System.out.println("Daño: " + (10 * getLevel() + getMana()));21 setMana(getMana() - 10);22 }23}
Herencia múltiple
La herencia múltiple es una característica que permite que una clase herede de más de una clase padre. En Java, la herencia múltiple no es permitida, ya que puede generar conflictos en el código.
Sin embargo, existen alternativas para simular la herencia múltiple y, en la práctica, obtener el mismo resultado. Para ello se emplea el concepto de interfaces, el cual veremos más adelante. De momento, nos concentraremos en la herencia simple.
Jeraquía de clases
La herencia nos permite crear una jerarquía de clases, una estructura en la que una clase hija hereda de una clase padre, y esta a su vez hereda de otra clase padre, y así sucesivamente.
Siguiendo con el ejemplo planteado sobre los personajes de un videojuego, podríamos definir la clase Character
como una clase padre, y definir las clases Warrior
y Wizard
como clases hijas.
Además, esta última podría ser padre de otra clase. Por ejemplo, podríamos definir la clase AncientWizard
, la cual representa un mago mucho más poderoso que el mago común, y que agrega comportamientos y características propias. Por supuesto, esta clase herada de la clase Wizard
.
1class AncientWizard extends Wizard {2 private int wisdom;3
4 public AncientWizard(String name, int level, int health, int mana, int wisdom) {5 super(name, level, health, mana);6 this.wisdom = wisdom;7 }8
9 public int getWisdom() {10 return wisdom;11 }12
13 @Override14 public void attack() {15 System.out.println("Atacando...");16 System.out.println("Daño: " + (10 * getLevel() + getMana() + getWisdom()));17 }18}
Gráficamente podemos representar esta jerarquía mediante un árbol de herencia, en el cual cada nodo representa una clase, y cada rama representa una relación de herencia.
En Java, veremos que existen muchos de estos árboles de herencia para clases que ya están definidas. Podemos entender mejor el comportamiento de las mismas si analizamos su jerarquía de clases.
La herencia puede se interpretada mediante la analogía de es un. Por ejemplo, en la naturaleza podríamos decir que un perro es un animal, y que un gato es un animal. De la misma manera, podríamos decir que un perro es un mamífero, y que un gato es un mamífero. Si abstrajéramos este tipo de entidades a componentes de software veríamos que estas realaciones se mantienen y que podemos aplicarlas a la herencia.
Igualmente con nuestro ejemplo de personajes de videojuegos, podemos decir que un Warrior
es un Character
, y que un Wizard
es un Character
. De la misma manera, podemos decir que un AncientWizard
es un Wizard
.
Así podríamos seguir atrás en la jerarquía de clases, y nos daríamos cuenta que todas las clases, ya sea las que definimos nosotros o las que nos provee Java, heredan de una clase padre común, la clase Object
.
Es decir, cualquier instancia es un Object
.