Saltar al contenido principal

Herencia, super y overriding

La composición modela relaciones «tiene un». La herencia sirve para otra idea: una clase más específica es un tipo de otra clase y puede cumplir su mismo contrato.

De Persona a Alumno​

Un alumno es una persona. Ambas comparten nombre, pero el alumno añade un curso:

class Persona {
private String nombre;

public Persona(String nombre) {
this.nombre = nombre;
}

public String presentacion() {
return "Soy " + nombre;
}
}

class Alumno extends Persona {
private String curso;

public Alumno(String nombre, String curso) {
super(nombre);
this.curso = curso;
}
}

extends Persona establece la relación. Alumno hereda los miembros accesibles de Persona, pero no accede directamente a sus atributos private.

Construir primero la parte padre​

super(nombre) llama al constructor de la clase padre y debe ser la primera instrucción del constructor. Después se inicializa la parte específica del alumno.

No estamos creando dos objetos: existe un solo Alumno, construido con estado y comportamiento de ambos niveles.

Sobrescribir comportamiento​

El alumno puede adaptar un método heredado manteniendo su firma:

class Alumno extends Persona {
private String curso;

public Alumno(String nombre, String curso) {
super(nombre);
this.curso = curso;
}

@Override
public String presentacion() {
return super.presentacion() + " y estudio " + curso;
}
}
  • @Override pide al compilador que compruebe que realmente sobrescribimos un método.
  • super.presentacion() reutiliza la versión del padre.
  • La versión del hijo completa el comportamiento.

Si escribimos mal el nombre o cambiamos los parámetros, @Override evita que el error pase inadvertido.

Sobrecarga y sobrescritura no son lo mismo​

Sobrecargar (overload) significa mantener el nombre y cambiar los parámetros, normalmente dentro de una clase. Sobrescribir (override) significa que la clase hija redefine un método heredado con la misma firma.

// Sobrecarga
public String presentacion(String saludo) {
return saludo + ", " + presentacion();
}

La anotación @Override solo corresponde a la sobrescritura.

Ejemplo completo​

class Persona {
private String nombre;

public Persona(String nombre) {
this.nombre = nombre;
}

public String presentacion() {
return "Soy " + nombre;
}
}

class Alumno extends Persona {
private String curso;

public Alumno(String nombre, String curso) {
super(nombre);
this.curso = curso;
}

@Override
public String presentacion() {
return super.presentacion() + " y estudio " + curso;
}
}

public class HerenciaDemo {
public static void main(String[] args) {
Persona persona = new Persona("Ada");
Alumno alumno = new Alumno("Leo", "Java 2");

System.out.println(persona.presentacion());
System.out.println(alumno.presentacion());
}
}

Cuándo no usar herencia​

Una clase Coche no debería extender Motor: un coche tiene un motor, no es un motor.

class Motor {
public String arrancar() {
return "Motor arrancado";
}
}

class Coche {
private Motor motor;

public Coche(Motor motor) {
this.motor = motor;
}

public String arrancar() {
return motor.arrancar();
}
}

Aquí la composición refleja el modelo y permite cambiar el motor sin fingir una relación de tipos. Heredar solo para ahorrar unas líneas suele crear una relación difícil de sostener.

Límites saludables​

  • Mantén jerarquías pequeñas y fáciles de explicar.
  • Comprueba que cualquier objeto hijo pueda usarse razonablemente como uno del padre.
  • Conserva privados los detalles internos del padre.
  • Prefiere composición cuando una pieza solo usa o contiene a otra.

Errores habituales​

  • Olvidar super(...) cuando el padre no tiene constructor sin parámetros.
  • Intentar acceder desde el hijo a atributos private del padre.
  • Confundir sobrecarga con sobrescritura.
  • Quitar @Override para ocultar un error de firma.
  • Crear herencia entre clases que solo colaboran.

Practica: empleado y responsable​

Enunciado​

Crea Empleado con nombre y método descripcion(). Crea Responsable, que es un empleado y añade el nombre de su equipo. Sobrescribe descripcion() reutilizando la del padre.

Criterios​

  • Los atributos son privados.
  • El constructor hijo llama a super.
  • La sobrescritura lleva @Override.
  • No se duplican el nombre ni su inicialización.

Solución​

class Empleado {
private String nombre;

public Empleado(String nombre) {
this.nombre = nombre;
}

public String descripcion() {
return "Empleado: " + nombre;
}
}

class Responsable extends Empleado {
private String equipo;

public Responsable(String nombre, String equipo) {
super(nombre);
this.equipo = equipo;
}

@Override
public String descripcion() {
return super.descripcion() + " | equipo: " + equipo;
}
}

public class EmpleadosDemo {
public static void main(String[] args) {
Empleado empleado = new Empleado("Nuria");
Responsable responsable = new Responsable("María", "Backend");

System.out.println(empleado.descripcion());
System.out.println(responsable.descripcion());
}
}

La herencia es válida porque un responsable sigue siendo un empleado. En la siguiente unidad aprovecharemos esa relación para trabajar con distintos subtipos mediante una referencia común.