Java
Herencia y @Override
Cómo una clase hereda de otra con extends, qué hace super, qué significa exactamente @Override y en qué se diferencia sobrescribir de sobrecargar.
El problema: dos clases casi iguales
Imagina que el sistema del curso ahora necesita también profesores. Un profesor tiene nombre y edad, igual que un alumno, pero además tiene una materia:
public class Alumno {
private String nombre;
private int edad;
private double nota;
// constructor, getters, setters...
}
public class Profesor {
private String nombre;
private int edad;
private String materia;
// el MISMO constructor, los MISMOS getters, otra vez...
}nombre y edad, con su constructor y sus getters, están escritos dos veces. Si mañana se agrega un dni, hay que tocar los dos archivos y acordarse de no olvidar ninguno. La herencia permite escribir esa parte común una sola vez, en una clase “padre”, y que las otras dos partan de ahí.
extends: heredar de otra clase
extends se traduce literalmente como “extiende” o “amplía”, y describe bastante bien lo que pasa: la clase hija es la clase padre más lo suyo propio.
public class Persona {
protected String nombre;
protected int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() {
return nombre;
}
public String describir() {
return nombre + ", " + edad + " años";
}
}public class Alumno extends Persona {
private double nota;
public Alumno(String nombre, int edad, double nota) {
super(nombre, edad);
this.nota = nota;
}
}Alumno no declara nombre ni edad ni getNombre(), y sin embargo los tiene:
Alumno a = new Alumno("Ana", 20, 8.5);
System.out.println(a.getNombre());
System.out.println(a.describir());Ana
Ana, 20 añosEl vocabulario que vas a ver en cualquier explicación de Java:
| Término | Otros nombres | En el ejemplo |
|---|---|---|
| Clase padre | superclase, clase base | Persona |
| Clase hija | subclase, clase derivada | Alumno, Profesor |
| Heredar | extender | Alumno extends Persona |
Java tiene herencia simple: una clase puede escribir extends una sola vez. No existe class X extends A, B. Si necesitas que un objeto cumpla varios “contratos” a la vez, la herramienta son las interfaces, que sí se pueden combinar sin límite.
protected en vez de private
En Persona, nombre y edad están declarados protected, no private. Ese es el modificador de la tabla de Encapsulación que faltaba justificar: private haría que ni siquiera Alumno — que es una Persona — pudiera leerlos directamente.
protected es más abierto de lo que parece: además de las subclases, deja entrar a cualquier clase del mismo paquete. Si la subclase puede arreglárselas usando los getters públicos del padre, prefiere dejar los atributos en private: menos superficie abierta, menos formas de romper la clase base sin darse cuenta.
super: llamar al padre
super se traduce como “superior”, y se refiere a la clase de arriba. Tiene dos usos distintos que conviene no mezclar:
1. super(...) — llamar al constructor del padre. Solo se puede usar como primera línea del constructor de la hija:
public Alumno(String nombre, int edad, double nota) {
super(nombre, edad); // Persona se encarga de nombre y edad
this.nota = nota; // Alumno se encarga de lo suyo
}2. super.metodo() — llamar a la versión del padre de un método, que es lo que verás en la sección siguiente.
Si no escribes super(...), Java inserta una llamada implícita a super() sin argumentos. Eso funciona solo si el padre tiene un constructor sin parámetros; si Persona solo define Persona(String, int), olvidarte del super(...) explícito es un error de compilación (constructor Persona in class Persona cannot be applied to given types). Es uno de los errores más frecuentes al empezar con herencia.
Sobrescribir un método: @Override
describir() funciona para cualquier persona, pero para un alumno estaría bueno que además dijera la nota. La hija puede sobrescribir (override) el método: volver a declararlo con la misma firma, y su versión reemplaza a la del padre para los objetos de esa clase.
public class Alumno extends Persona {
private double nota;
public Alumno(String nombre, int edad, double nota) {
super(nombre, edad);
this.nota = nota;
}
@Override
public String describir() {
return super.describir() + " — nota: " + nota;
}
}Persona p = new Persona("Luis", 45);
Alumno a = new Alumno("Ana", 20, 8.5);
System.out.println(p.describir());
System.out.println(a.describir());Luis, 45 años
Ana, 20 años — nota: 8.5Dos cosas están pasando ahí:
describir()enAlumnoreemplaza al dePersonapara todo objetoAlumno.super.describir()llama a la versión original del padre desde dentro de la nueva. Sin esesuper., escribirdescribir()a secas se llamaría a sí mismo para siempre — una recursión infinita que termina enStackOverflowError.
Qué es realmente @Override
@Override es una anotación: una marca que se le pone al código para que el compilador (o una herramienta) la lea. Override se traduce como “anular” o “prevalecer sobre”.
Lo importante: @Override no hace que el método sobrescriba nada. La sobrescritura ocurre igual sin la anotación, solo por tener la misma firma que el método del padre. Lo que hace @Override es pedirle al compilador que verifique esa intención — y ahí está todo su valor:
public class Alumno extends Persona {
@Override
public String descrbir() { // ← typo: "descrbir"
return "...";
}
}Sin @Override, eso compila perfecto: Java entiende que estás declarando un método nuevo llamado descrbir(), y describir() sigue devolviendo la versión del padre. El programa corre y hace lo que no querías, sin un solo error. Con @Override, el compilador responde method does not override or implement a method from a supertype y el problema aparece al instante.
Escribe @Override siempre que quieras sobrescribir un método. Es opcional para el lenguaje, pero es la diferencia entre que un typo o un parámetro mal tipeado te lo diga el compilador o te lo diga el programa en producción, dos meses después.
Sobrescribir (override) no es sobrecargar (overload)
Los dos nombres se parecen muchísimo en español y en inglés, y significan cosas distintas. Ya viste la sobrecarga en Métodos:
| Sobrecarga (overload) | Sobrescritura (override) | |
|---|---|---|
| Dónde viven los métodos | En la misma clase | En clases distintas: padre e hija |
| Los parámetros | Distintos (es lo que los diferencia) | Idénticos (es lo que los relaciona) |
| El tipo de retorno | Puede ser distinto | Tiene que ser el mismo (o un subtipo) |
| Cuándo se decide cuál correr | Al compilar, según los argumentos | Al ejecutar, según el tipo real del objeto |
| Anotación | Ninguna | @Override |
static int sumar(int a, int b) { ... }
static double sumar(double a, double b) { ... }public class Persona { public String describir() { ... } }
public class Alumno extends Persona { @Override public String describir() { ... } }Un truco para no confundirlas: sobrecargar es cargar un mismo nombre con varios significados a la vez; sobrescribir es escribir encima del que ya había, tapándolo.
Reglas que el compilador hace cumplir
Al sobrescribir, la hija no puede hacer cualquier cosa:
| Regla | Por qué |
|---|---|
| La firma (nombre + parámetros) tiene que ser idéntica | Si cambian los parámetros, es una sobrecarga, no una sobrescritura |
| No se puede reducir la visibilidad | Un método public en el padre no puede volverse private en la hija: quien ya lo usaba a través del padre dejaría de poder |
| Sí se puede ampliar la visibilidad | De protected a public está permitido |
Un método final no se puede sobrescribir | final significa exactamente “esto no se toca” — ver static y final |
Un método static no se sobrescribe | Pertenece a la clase, no al objeto; redeclararlo en la hija lo oculta, que no es lo mismo |
Pon a prueba
public class Animal {
public String hablar() {
return "Sonido genérico";
}
}
public class Perro extends Animal {
@Override
public String hablar() {
return "Guau";
}
}
// en main:
Animal a = new Perro();
System.out.println(a.hablar());Empleado hereda de Persona, pasa nombre y edad al constructor del padre, y reescribe describir() añadiendo el puesto al final del texto original.
public class Empleado Persona {
private String puesto;
public Empleado(String nombre, int edad, String puesto) {
(nombre, edad);
this.puesto = puesto;
}
public String describir() {
return .describir() + " — " + puesto;
}
}Este código compila sin una sola advertencia, pero imprime Sonido genérico en vez de Miau. ¿Por qué, y cuál es el cambio de una línea que lo habría detectado al compilar?
public class Animal {
public String hablar() {
return "Sonido genérico";
}
}
public class Gato extends Animal {
public String hablar(String tono) {
return "Miau";
}
}
// en main:
Animal a = new Gato();
System.out.println(a.hablar());Gato.hablar(String tono) recibe un parámetro que Animal.hablar() no tiene, así que no es una sobrescritura sino una sobrecarga: Gato termina con dos métodos hablar distintos, el heredado sin parámetros y el nuevo con uno. La llamada a.hablar() sin argumentos encuentra el heredado, que devuelve "Sonido genérico".
El cambio de una línea es agregar @Override encima de hablar(String tono). Con esa anotación el compilador verifica que exista un método con esa firma exacta en la superclase, no lo encuentra, y falla con method does not override or implement a method from a supertype — el bug aparece antes de ejecutar nada.
Este es exactamente el escenario para el que se inventó la anotación, y por eso conviene ponerla siempre.
Ordena las líneas para definir una clase Cuadrado que hereda de Figura y sobrescribe area().
- public class Cuadrado extends Figura {
- return lado * lado;
- public Cuadrado(String nombre, double lado) {
- private double lado;
- @Override
- }
- this.lado = lado;
- }
- public double area() {
- }
- super(nombre);
Siguiente paso
Que Alumno y Profesor sean los dos una Persona habilita algo más potente que ahorrar código repetido: poder tratarlos a los dos como personas y que cada uno responda a su manera. Eso es el polimorfismo — ver Polimorfismo y clases abstractas.