Herencia y polimorfismo
Hasta ahora hemos aprendido a crear clases y objetos.
Por ejemplo, podíamos tener una clase Persona:
public class Persona {
String nombre;
int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public void saludar() {
System.out.println("Hola, soy " + nombre);
}
}
A medida que nuestros programas crecen, puede ocurrir que tengamos varias clases que compartan características.
Por ejemplo, imaginemos un programa para gestionar diferentes tipos de personas:
Persona
│
├── Alumno
│
└── Profesor
Tanto un Alumno como un Profesor son personas.
Por lo tanto, ambos podrían tener:
nombre
edad
saludar()
Pero además, cada uno podría tener sus propias características y comportamientos.
Un Alumno podría tener:
curso
estudiar()
Mientras que un Profesor podría tener:
asignatura
enseñar()
La herencia permite crear una clase nueva a partir de otra clase existente, reutilizando sus atributos y métodos.
El polimorfismo permite que diferentes clases respondan de manera diferente ante un mismo método.
Estos dos conceptos son fundamentales dentro de la Programación Orientada a Objetos.
Herencia
La herencia permite crear una clase nueva a partir de otra clase.
La clase nueva recibe los atributos y métodos de la clase original y puede añadir sus propios elementos.
Podemos imaginarlo de esta forma:
Persona
│
┌──────┴──────┐
↓ ↓
Alumno Profesor
Persona sería la clase padre.
Alumno y Profesor serían clases hijas.
También podemos encontrar otros nombres:
Clase padre
Clase base
Superclase
y:
Clase hija
Clase derivada
Subclase
Todos estos términos hacen referencia al mismo concepto.
extends
En Java utilizamos la palabra clave extends para indicar que una clase hereda de otra.
Por ejemplo:
public class Alumno extends Persona {
}
Esto significa:
Alumno
↓
hereda de
↓
Persona
La clase Alumno podrá utilizar los atributos y métodos heredados de Persona.
Por ejemplo, si tenemos:
public class Persona {
String nombre;
int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public void saludar() {
System.out.println("Hola, soy " + nombre);
}
}
Podemos crear:
public class Alumno extends Persona {
}
Ahora podemos crear un objeto Alumno:
Alumno alumno = new Alumno();
Y, si el constructor correspondiente está definido, podrá utilizar los elementos heredados de Persona.
La idea principal es:
Persona
nombre
edad
saludar()
│
│ herencia
↓
Alumno
nombre
edad
saludar()
No hemos tenido que volver a escribir nombre, edad ni saludar() dentro de Alumno, puesto que solo con el extends ya los ha heredado de Persona.
Añadir elementos propios
Una clase hija no solamente puede utilizar los elementos heredados.
También puede tener sus propios atributos y métodos.
Por ejemplo:
public class Alumno extends Persona {
String curso;
public void estudiar() {
System.out.println(nombre + " está estudiando");
}
}
Ahora Alumno tiene:
Elementos heredados:
nombre
edad
saludar()
Elementos propios:
curso
estudiar()
Podemos crear un alumno y utilizar ambos tipos de elementos:
Alumno alumno = new Alumno();
alumno.nombre = "Ana";
alumno.edad = 20;
alumno.curso = "Java";
alumno.saludar();
alumno.estudiar();
La clase Alumno puede utilizar saludar() porque lo ha heredado de Persona.
Constructor de una clase hija
Cuando una clase hija tiene un constructor, normalmente necesitamos inicializar también la parte heredada de la clase padre.
Nuestra clase Alumno podría tener:
public class Alumno extends Persona {
String curso;
public Alumno(String nombre, int edad, String curso) {
super(nombre, edad);
this.curso = curso;
}
}
Aquí aparece una nueva palabra clave en la primera línea del constructor:
super
La palabra clave super hace referencia a la clase padre.
Podemos utilizarla principalmente para acceder a elementos de la clase padre.
Uno de sus usos más habituales es llamar al constructor de la clase padre.
En:
public Alumno(String nombre, int edad, String curso) {
super(nombre, edad);
this.curso = curso;
}
La línea:
super(nombre, edad);
llama al constructor de Persona:
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
Podemos visualizarlo así:
Crear Alumno
↓
Constructor Alumno
│
├── super(nombre, edad)
│ ↓
│ Constructor Persona
│
└── this.curso = curso
Primero se inicializa la parte correspondiente a los atriburos del padre Persona utilizando super, y después se inicializa la parte específica de Alumno con el this habitual.
Llamar a métodos de la clase padre con super
También podemos utilizar super para llamar a un método de la clase padre.
Por ejemplo:
public class Persona {
public void saludar() {
System.out.println("Hola");
}
}
La clase Alumno podría hacer:
public class Alumno extends Persona {
public void presentarse() {
super.saludar();
System.out.println("Soy un alumno");
}
}
Cuando llamamos:
alumno.presentarse();
obtenemos:
Hola
Soy un alumno
La línea:
super.saludar();
indica que queremos ejecutar el método saludar() de la clase padre.
Sobreescritura de métodos
Una clase hija puede modificar el comportamiento de un método heredado.
Esto se conoce como sobreescritura de métodos o method overriding.
Imaginemos que todas las personas pueden saludar:
public class Persona {
public void saludar() {
System.out.println("Hola");
}
}
Un Alumno podría querer utilizar un saludo diferente:
public class Alumno extends Persona {
public void saludar() {
System.out.println("Hola, soy un alumno");
}
}
En este caso, Alumno tiene un método saludar(), llamado igual que el de su padre Persona.
Cuando utilizamos:
Alumno alumno = new Alumno();
alumno.saludar();
se ejecutará:
Hola, soy un alumno
y no:
Hola
El método de la clase hija ha sobreescrito el método de la clase padre.
@Override
Aunque Java permite escribir una sobreescritura simplemente definiendo el método, es recomendable utilizar la anotación:
@Override
Por ejemplo:
public class Alumno extends Persona {
@Override
public void saludar() {
System.out.println("Hola, soy un alumno");
}
}
@Override indica a Java que estamos intentando sobrescribir un método heredado.
Esto permite detectar errores.
Por ejemplo, si escribimos accidentalmente:
@Override
public void saludarAlumno() {
System.out.println("Hola");
}
Java mostrará un error porque saludarAlumno() no está sobrescribiendo ningún método de la clase padre.
Por lo tanto, @Override ayuda a evitar errores y hace que nuestro código sea más claro.
Utilizar el método original con super
Aunque hayamos sobrescrito un método, podemos seguir accediendo al método original de la clase padre utilizando super.
Por ejemplo:
public class Alumno extends Persona {
@Override
public void saludar() {
super.saludar();
System.out.println("Soy un alumno");
}
}
Ahora:
Alumno alumno = new Alumno();
alumno.saludar();
producirá:
Hola
Soy un alumno
Primero se ejecuta:
super.saludar();
que llama al método de Persona.
Después se ejecuta:
System.out.println("Soy un alumno");
que pertenece al método sobrescrito de Alumno.
Polimorfismo
El polimorfismo significa literalmente "muchas formas".
En programación, permite que un mismo método pueda tener comportamientos diferentes dependiendo del objeto que lo ejecute.
Imaginemos que tenemos:
Persona
│
├── Alumno
│
└── Profesor
Todas las personas pueden tener un método:
saludar()
Pero cada tipo de persona puede implementarlo de una manera diferente.
Por ejemplo:
public class Persona {
public void saludar() {
System.out.println("Hola");
}
}
public class Alumno extends Persona {
@Override
public void saludar() {
System.out.println("Hola, soy un alumno");
}
}
public class Profesor extends Persona {
@Override
public void saludar() {
System.out.println("Hola, soy un profesor");
}
}
Ahora podemos tener:
Alumno alumno = new Alumno();
Profesor profesor = new Profesor();
alumno.saludar();
profesor.saludar();
Salida:
Hola, soy un alumno
Hola, soy un profesor
Los dos objetos utilizan el mismo método:
saludar()
pero cada clase tiene un comportamiento diferente.
Esto es polimorfismo.
Referencias de la clase padre
Una de las características más importantes del polimorfismo es que podemos utilizar una variable del tipo de la clase padre para almacenar un objeto de una clase hija.
Por ejemplo:
Persona persona = new Alumno();
La variable es de tipo:
Persona
pero el objeto que hemos creado realmente es:
Alumno
También podemos hacer:
Persona persona1 = new Alumno();
Persona persona2 = new Profesor();
Tenemos:
Persona persona1
↓
Alumno
Persona persona2
↓
Profesor
Esto permite trabajar con diferentes tipos de objetos de una forma común.
Clases abstractas
En ocasiones queremos crear una clase que sirva como base para otras clases, pero que no tenga sentido crear objetos directamente de ella.
Por ejemplo, podemos tener:
Animal
│
├── Perro
└── Gato
Tiene sentido crear un Perro.
Tiene sentido crear un Gato.
Pero quizá no tenga sentido crear simplemente un objeto Animal.
Para estos casos podemos utilizar una clase abstracta.
Una clase abstracta se declara utilizando:
abstract
Por ejemplo:
public abstract class Animal {
}
Esta clase puede servir como base para otras clases:
public class Perro extends Animal {
}
public class Gato extends Animal {
}
Pero no podemos hacer:
Animal animal = new Animal();
Esto provocará un error.
Una clase abstract no puede ser instanciada directamente. Por lo que podemos verlo como crear una clase padre que sirva para que sus clases hijas hereden sus atributos y métodos, pero que no se puedan crear objetos directamente de ella.
Métodos abstractos
Una clase abstracta también puede tener métodos abstractos.
Un método abstracto es un método que se declara pero que no tiene implementación.
Por ejemplo:
public abstract class Animal {
public abstract void hacerSonido();
}
Observa que no tenemos:
{
...
}
Después del método.
Solamente indicamos:
public abstract void hacerSonido();
Estamos diciendo:
Todo
Animaldebe tener un métodohacerSonido(), pero cada tipo concreto de animal decidirá cómo implementarlo.
Implementar un método abstracto
Si una clase hereda de una clase abstracta que tiene un método abstracto, debe implementar dicho método.
Por ejemplo:
public class Perro extends Animal {
@Override
public void hacerSonido() {
System.out.println("Guau");
}
}
Y:
public class Gato extends Animal {
@Override
public void hacerSonido() {
System.out.println("Miau");
}
}
Ahora tenemos:
Animal
│
│ hacerSonido()
│
├── Perro
│ ↓
│ "Guau"
│
└── Gato
↓
"Miau"
La clase Animal establece que el método debe existir.
Las clases hijas deciden cómo funciona.
Diferencia entre herencia y polimorfismo
Aunque están muy relacionados, no son exactamente lo mismo.
La herencia permite que una clase reutilice y extienda otra clase.
Por ejemplo:
public class Perro extends Animal {
}
Estamos utilizando herencia.
El polimorfismo permite que diferentes clases utilicen una misma interfaz o método pero tengan comportamientos diferentes.
Por ejemplo:
Animal animal1 = new Perro();
Animal animal2 = new Gato();
animal1.hacerSonido();
animal2.hacerSonido();
Cada objeto ejecutará su propia versión de:
hacerSonido()
Errores frecuentes
Intentar crear un objeto de una clase abstracta
Incorrecto:
Animal animal = new Animal();
Si Animal es abstracta, no podemos crear objetos directamente de ella.
Correcto:
Animal animal = new Perro("Toby");
No implementar un método abstracto
Si tenemos:
public abstract class Animal {
public abstract void hacerSonido();
}
Una clase hija debe implementar el método:
public class Perro extends Animal {
@Override
public void hacerSonido() {
System.out.println("Guau");
}
}
Si no lo hacemos, tendremos un error, salvo que la propia clase hija también sea abstract.
Confundir extends con crear un objeto
Esta línea:
public class Perro extends Animal {
}
no crea ningún objeto.
Solamente indica que Perro hereda de Animal.
Para crear un objeto:
Perro perro = new Perro("Toby");
Olvidar super en el constructor
Si la clase padre necesita parámetros en su constructor:
public Animal(String nombre) {
this.nombre = nombre;
}
La clase hija puede utilizar:
public Perro(String nombre) {
super(nombre);
}
super(nombre) permite llamar al constructor de Animal.
Confundir super con this
Recordemos:
this
↓
Objeto actual
super
↓
Clase padre
Por ejemplo:
this.nombre
hace referencia al objeto actual.
Mientras que:
super.metodo()
hace referencia a la implementación del método en la clase padre.