Ir al contenido principal

Herencia en Java

Introducción

La herencia es uno de los conceptos fundamentales de la programación orientada a objetos en Java. Este mecanismo permite que una clase adquiera las propiedades y comportamientos de otra clase, estableciendo una relación jerárquica entre ellas. La herencia es esencial para reutilizar código, crear jerarquías de clases y establecer relaciones del tipo "es un" entre objetos. En este artículo, exploraremos cómo implementar y aprovechar las ventajas de la herencia en Java.

El concepto de herencia

La herencia en Java se basa en la idea de que una clase (denominada clase hija o subclase) puede heredar atributos y métodos de otra clase (denominada clase padre, superclase o clase base). Esto permite crear una estructura jerárquica de clases donde las subclases heredan características comunes de su superclase.

Para establecer una relación de herencia en Java, se utiliza la palabra clave extends:

public class ClasePadre {
    // Atributos y métodos de la clase padre
}

public class ClaseHija extends ClasePadre {
    // La clase hija hereda automáticamente los atributos y métodos
    // de la clase padre, y puede añadir sus propios elementos
}

Ventajas de la herencia

  • Reutilización de código: Evita duplicar código al heredar funcionalidades ya implementadas
  • Extensibilidad: Permite añadir nuevas características a clases existentes
  • Jerarquía de tipos: Establece una relación "es un" entre las clases
  • Polimorfismo: Posibilita tratar objetos de diferentes subclases a través de referencias de su superclase

La clase Object

En Java, todas las clases heredan automáticamente de la clase Object. Esta es la raíz de la jerarquía de clases en Java, y proporciona métodos comunes a todos los objetos como:

  • equals(): Compara si dos objetos son iguales
  • toString(): Devuelve una representación en texto del objeto
  • hashCode(): Genera un código hash para el objeto
  • getClass(): Devuelve el tipo de la clase en tiempo de ejecución
public class Ejemplo {
    public static void main(String[] args) {
        Ejemplo obj = new Ejemplo();
        
        // Estos métodos están disponibles gracias a heredar de Object
        System.out.println(obj.toString());
        System.out.println(obj.getClass().getName());
    }
}

Creando una jerarquía de clases

Veamos un ejemplo práctico de herencia con una jerarquía de vehículos:

// Clase base o superclase
public class Vehiculo {
    private String marca;
    private String modelo;
    private int año;
    
    public Vehiculo(String marca, String modelo, int año) {
        this.marca = marca;
        this.modelo = modelo;
        this.año = año;
    }
    
    public void arrancar() {
        System.out.println("El vehículo está arrancando");
    }
    
    public void detener() {
        System.out.println("El vehículo se está deteniendo");
    }
    
    // Getters y setters
    public String getMarca() {
        return marca;
    }
    
    public String getModelo() {
        return modelo;
    }
    
    public int getAño() {
        return año;
    }
}

// Subclase que hereda de Vehiculo
public class Coche extends Vehiculo {
    private int numeroPuertas;
    
    public Coche(String marca, String modelo, int año, int numeroPuertas) {
        // Llamamos al constructor de la clase padre
        super(marca, modelo, año);
        this.numeroPuertas = numeroPuertas;
    }
    
    public void abrirMaletero() {
        System.out.println("Abriendo maletero del coche");
    }
    
    public int getNumeroPuertas() {
        return numeroPuertas;
    }
}

// Otra subclase que hereda de Vehiculo
public class Motocicleta extends Vehiculo {
    private boolean tieneCaballete;
    
    public Motocicleta(String marca, String modelo, int año, boolean tieneCaballete) {
        super(marca, modelo, año);
        this.tieneCaballete = tieneCaballete;
    }
    
    public void hacerCaballito() {
        System.out.println("¡La moto está haciendo un caballito!");
    }
    
    public boolean getTieneCaballete() {
        return tieneCaballete;
    }
}

La palabra clave super

La palabra clave super se utiliza para acceder a elementos de la clase padre:

  1. Llamar al constructor de la superclase: super(parámetros);
  2. Acceder a métodos de la superclase: super.nombreMetodo();
  3. Acceder a atributos de la superclase: super.nombreAtributo;
public class Ejemplo {
    public static void main(String[] args) {
        Coche miCoche = new Coche("Seat", "Ibiza", 2020, 5);
        miCoche.arrancar(); // Método heredado de Vehiculo
        miCoche.abrirMaletero(); // Método propio de Coche
        
        System.out.println("Marca: " + miCoche.getMarca()); // Getter heredado
        System.out.println("Puertas: " + miCoche.getNumeroPuertas()); // Getter propio
    }
}

Sobrescritura de métodos

Una de las características más potentes de la herencia es la capacidad de sobrescribir (override) métodos de la clase padre en la clase hija. Esto permite que una subclase proporcione una implementación específica de un método ya definido en su superclase.

Para sobrescribir un método:

  1. La firma del método debe ser exactamente igual (nombre, parámetros y tipo de retorno)
  2. La visibilidad no puede ser más restrictiva que en la superclase
  3. Se recomienda usar la anotación @Override para indicar explícitamente la sobrescritura
public class Coche extends Vehiculo {
    // ... código anterior ...
    
    @Override
    public void arrancar() {
        System.out.println("El coche está arrancando el motor de combustión");
    }
}

public class CocheElectrico extends Coche {
    private int capacidadBateria;
    
    public CocheElectrico(String marca, String modelo, int año, int numeroPuertas, int capacidadBateria) {
        super(marca, modelo, año, numeroPuertas);
        this.capacidadBateria = capacidadBateria;
    }
    
    @Override
    public void arrancar() {
        System.out.println("El coche eléctrico está encendiendo el motor eléctrico");
    }
    
    public int getCapacidadBateria() {
        return capacidadBateria;
    }
}

Herencia y constructores

Los constructores no se heredan, pero siempre se llama implícita o explícitamente al constructor de la superclase:

  • Si no especificamos constructor en la subclase, Java llamará al constructor sin parámetros de la superclase
  • Si queremos llamar a un constructor específico de la superclase, debemos usar super() como primera instrucción del constructor de la subclase
public class Ejemplo {
    public static void main(String[] args) {
        // Creamos objetos de diferentes tipos
        Vehiculo vehiculo = new Vehiculo("Genérico", "Estándar", 2022);
        Coche coche = new Coche("Toyota", "Corolla", 2021, 4);
        CocheElectrico tesla = new CocheElectrico("Tesla", "Model 3", 2023, 4, 75);
        
        // Probamos los métodos sobreescritos
        System.out.println("Llamando a arrancar() en cada objeto:");
        vehiculo.arrancar(); // Usa la versión de Vehiculo
        coche.arrancar();    // Usa la versión sobreescrita en Coche
        tesla.arrancar();    // Usa la versión sobreescrita en CocheElectrico
    }
}

Limitaciones de la herencia en Java

En Java, hay algunas restricciones importantes sobre la herencia:

  1. Herencia simple: Una clase solo puede heredar directamente de una superclase (a diferencia de otros lenguajes que permiten herencia múltiple)
  2. Clases final: Una clase declarada como final no puede ser heredada
  3. Métodos final: Un método declarado como final no puede ser sobrescrito en subclases
// Esta clase no puede ser heredada
final class ClaseFinal {
    // Implementación...
}

class ClaseNormal {
    // Este método no puede ser sobreescrito
    final public void metodoFinal() {
        // Implementación...
    }
}

Herencia vs. Composición

Aunque la herencia es poderosa, no siempre es la mejor opción para reutilizar código. La composición (incluir objetos de otras clases como atributos) suele ser una alternativa más flexible:

// Enfoque de herencia
class CocheDeLujo extends Coche {
    // CocheDeLujo es un Coche
}

// Enfoque de composición
class CocheDeLujo {
    private Coche coche; // CocheDeLujo tiene un Coche
    private SistemaEntretenimiento entretenimiento;
    private AsientosCalefactables asientos;
    
    // Implementación...
}

Regla general: Usa herencia cuando existe una relación "es un" verdadera, y composición cuando la relación es "tiene un".

Resumen

La herencia es un pilar fundamental de la programación orientada a objetos en Java que permite a las clases heredar atributos y comportamientos de otras clases. A través de la palabra clave extends, podemos crear jerarquías de clases que comparten funcionalidades comunes, evitando así la duplicación de código. La sobrescritura de métodos nos permite especializar el comportamiento de las subclases, mientras que mecanismos como super nos facilitan acceder a elementos de la clase padre. Es importante recordar que Java solo admite herencia simple y considerar la composición como alternativa cuando la relación entre objetos no es estrictamente del tipo "es un".