java-herencia-extends-super-object

Herencia en Java: extends, super y Object

  • 4 min

La herencia permite definir una clase a partir de otra y especializar su comportamiento.

En programación intentamos evitar la duplicación innecesaria, aunque la herencia no es la única forma de reutilizar código.

Imagina que estás programando un videojuego. Creas una clase Enemigo que tiene vida y posición. Luego creas un Jugador que tiene… vida y posición. Y luego un NPC que tiene… vida y posición.

¿Vas a escribir int vida; int x; int y; tres veces? ¿Y si luego quieres cambiar cómo funciona la posición? ¿Vas a editar tres archivos?

La herencia nos permite crear una clase base y hacer que otras clases derivadas reciban sus miembros accesibles.

El concepto “es-un” (is-a)

La herencia establece una relación jerárquica.

  • Un Coche es un Vehículo.
  • Un Gato es un Animal.
  • Un Gerente es un Empleado.

Si no puedes decir la frase “es un”, probablemente no deberías usar herencia (quizás deberías usar composición, “tiene un”, pero eso es tema avanzado).

La palabra clave extends

En Java, para heredar usamos extends. Vamos a verlo con el clásico ejemplo de vehículos.

La superclase (padre)

Definimos lo que es común a todos.

// Archivo: Vehiculo.java
public class Vehiculo {
    protected String marca; // protected: Las hijas pueden tocarlo
    protected double velocidad;

    public Vehiculo(String marca) {
        this.marca = marca;
        this.velocidad = 0;
    }

    public void acelerar() {
        this.velocidad += 10;
        System.out.println("Vehículo acelerando... " + this.velocidad + " km/h");
    }
}
Copied!

La subclase (hija)

Ahora creamos un Coche. El Coche hereda todo lo que tiene el Vehículo, y añade sus cosas propias (número de puertas).

// Archivo: Coche.java
public class Coche extends Vehiculo { // Coche hereda de Vehiculo

    private int numeroPuertas;

    public Coche(String marca, int puertas) {
        super(marca); // Llamamos al constructor del padre (Ver abajo)
        this.numeroPuertas = puertas;
    }

    // Método propio de Coche
    public void ponerAireAcondicionado() {
        System.out.println("Aire puesto. Qué fresquito.");
    }
}
Copied!

Al usar extends, la clase Coche automáticamente tiene marca, velocidad y el método acelerar(), sin tener que escribirlos.

Coche miCoche = new Coche("Toyota", 5);
miCoche.acelerar(); // ¡Funciona! Heredado de Vehiculo.
miCoche.ponerAireAcondicionado(); // Propio de Coche.
Copied!

La palabra clave super

Cuando heredas, a veces necesitas llamar a “papá”. Para eso sirve super. Tiene dos usos principales:

En el constructor (super())

Cuando nace un hijo (Coche), primero tiene que nacer la parte de padre (Vehiculo). Java te obliga a llamar al constructor de la superclase.

  • Debe ser la primera línea del constructor del hijo.
  • Si no lo pones, Java intentará poner un super() invisible (llamando al constructor vacío del padre). Si el padre no tiene constructor vacío… ¡Error de compilación!

Accediendo a métodos ocultos

Si el hijo y el padre tienen un método con el mismo nombre (Sobrescritura, que veremos en el siguiente post), puedes usar super.metodo() para ejecutar la versión original del padre.

Java y la herencia simple

Aquí hay una diferencia gigante con C++. En Java, una clase solo puede tener UN padre.

  • public class Coche extends Vehiculo
  • public class CocheAmfibio extends Barco, Coche (Error)

Esto se hizo para evitar el temido Problema del Diamante (cuando dos padres tienen un método igual, ¿cuál hereda el hijo?). Java prefiere la simplicidad. Si necesitas comportamientos de múltiples fuentes, Java usa Interfaces (lo veremos pronto).

La clase Object: el padre de todos

En Java, hay una verdad universal: Todo es un Objeto.

Si tú creas una clase que no extiende de nada:

public class Perro { ... }
Copied!

Java, silenciosamente, hace esto por ti:

public class Perro extends Object { ... }
Copied!

La clase java.lang.Object es la raíz de toda la jerarquía de Java. Esto significa que CUALQUIER objeto que crees tiene automáticamente los métodos definidos en Object.

Los métodos que todos heredamos

Gracias a heredar de Object, cualquier clase tiene:

  1. toString(): Devuelve una representación textual. La implementación de Object combina el nombre de la clase con el hash en hexadecimal; no muestra una dirección de memoria.
  2. equals(Object obj): Compara si dos objetos son iguales. Por defecto compara referencias (==), pero solemos sobrescribirlo para comparar contenido (como IDs).
  3. hashCode(): Un número utilizado por colecciones hash como HashMap. No tiene que ser único: objetos distintos pueden producir el mismo valor.
  4. getClass(): Nos dice de qué tipo es el objeto en tiempo de ejecución.