</>waridocu

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.

javaherenciaextendssuperoverridepoo

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:

Ejemplo: dos clases con el 80% del código repetidojava
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.

Ejemplo: una clase base y dos que la extiendenjava
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";
    }
}
javajava
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:

javajava
Alumno a = new Alumno("Ana", 20, 8.5);
System.out.println(a.getNombre());
System.out.println(a.describir());
texttext
Ana
Ana, 20 años

El vocabulario que vas a ver en cualquier explicación de Java:

TérminoOtros nombresEn el ejemplo
Clase padresuperclase, clase basePersona
Clase hijasubclase, clase derivadaAlumno, Profesor
HeredarextenderAlumno extends Persona
Nota

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.

Advertencia

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:

javajava
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.

Importante

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.

Ejemplo: la hija cambia el comportamiento heredadojava
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;
    }
}
javajava
Persona p = new Persona("Luis", 45);
Alumno a = new Alumno("Ana", 20, 8.5);
System.out.println(p.describir());
System.out.println(a.describir());
texttext
Luis, 45 años
Ana, 20 años — nota: 8.5

Dos cosas están pasando ahí:

  • describir() en Alumno reemplaza al de Persona para todo objeto Alumno.
  • super.describir() llama a la versión original del padre desde dentro de la nueva. Sin ese super., escribir describir() a secas se llamaría a sí mismo para siempre — una recursión infinita que termina en StackOverflowError.

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:

Ejemplo: el error que @Override atrapajava
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.

Importante

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étodosEn la misma claseEn clases distintas: padre e hija
Los parámetrosDistintos (es lo que los diferencia)Idénticos (es lo que los relaciona)
El tipo de retornoPuede ser distintoTiene que ser el mismo (o un subtipo)
Cuándo se decide cuál correrAl compilar, según los argumentosAl ejecutar, según el tipo real del objeto
AnotaciónNinguna@Override
Ejemplo: sobrecarga — dos métodos, misma clase, parámetros distintosjava
static int sumar(int a, int b) { ... }
static double sumar(double a, double b) { ... }
Ejemplo: sobrescritura — mismo método, clase hija, misma firma exactajava
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:

ReglaPor qué
La firma (nombre + parámetros) tiene que ser idénticaSi cambian los parámetros, es una sobrecarga, no una sobrescritura
No se puede reducir la visibilidadUn 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 visibilidadDe protected a public está permitido
Un método final no se puede sobrescribirfinal significa exactamente “esto no se toca” — ver static y final
Un método static no se sobrescribePertenece a la clase, no al objeto; redeclararlo en la hija lo oculta, que no es lo mismo

Pon a prueba

🎯 ¿Qué imprime y por qué?
javajava
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());
  • Exacto. Aunque la variable esté declarada como Animal, el objeto que existe en memoria es un Perro, y Java elige la versión del método según el objeto real, no según el tipo de la variable. Eso es polimorfismo, el tema de la página siguiente.

  • Es la respuesta intuitiva, pero al revés de cómo funciona Java. El tipo de la variable decide qué métodos se pueden llamar; el objeto real decide qué versión se ejecuta.

  • Guardar un Perro en una variable Animal no necesita cast: todo Perro es un Animal. El cast haría falta en el sentido contrario, al bajar de Animal a Perro.

  • Llamar a super.hablar() es opcional: sirve cuando quieres añadir algo al comportamiento del padre. Si lo reemplazas por completo, como aquí, no hace falta.

✏️ Completa la clase hija

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;
  }
}

💡 Encuentra el bug

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?

javajava
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 el código

Ordena las líneas para definir una clase Cuadrado que hereda de Figura y sobrescribe area().

  1. public class Cuadrado extends Figura {
  2. return lado * lado;
  3. public Cuadrado(String nombre, double lado) {
  4. private double lado;
  5. @Override
  6. }
  7. this.lado = lado;
  8. }
  9. public double area() {
  10. }
  11. 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.