¿Qué es la Herencia en programación orientada a objetos?

Cuando hablamos de herencia en programación no nos referimos precisamente a que algún familiar lejano nos ha podido dejar una fortuna, ya nos gustaría. En realidad se trata de uno de los pilares fundamentales de la programación orientada a objetos. Es el mecanismo por el cual una clase permite heredar las características (atributos y métodos) de otra clase.

La herencia permite que se puedan definir nuevas clases basadas de unas ya existentes a fin de reutilizar el código, generando así una jerarquía de clases dentro de una aplicación. Si una clase deriva de otra, esta hereda sus atributos y métodos y puede añadir nuevos atributos, métodos o redefinir los heredados.

Estoy seguro que cuando has leído “reutilizar” se te ha hecho la boca agua ¿verdad? No hay nada mejor en programación que poder usar el mismo código una y otra vez para hacer nuestro desarrollo más rápido y eficiente. El concepto de herencia ofrece mucho juego. Gracias a esto, lograremos un código mucho más limpio, estructurado y con menos líneas de código, lo que lo hace más legible.

En Java tenemos que tener claro cómo llamar a la clase principal de la que heredamos y aquella que hereda de ella, así, clase que se hereda se denomina superclase. La clase que hereda se llama subclase. Por lo tanto, una subclase es una versión especializada de una superclase. Hereda todas las variables y métodos definidos por la superclase y agrega sus propios elementos únicos.

 

Terminología importante:

  • Superclase: la clase cuyas características se heredan se conoce como superclase (o una clase base o una clase principal).

  • Subclase: la clase que hereda la otra clase se conoce como subclase (o una clase derivada, clase extendida o clase hija). La subclase puede agregar sus propios campos y métodos, además de los campos y métodos de la superclase.

  • Reutilización: la herencia respalda el concepto de “reutilización”, es decir, cuando queremos crear una clase nueva y ya hay una clase que incluye parte del código que queremos, podemos derivar nuestra nueva clase de la clase existente. Al hacer esto, estamos reutilizando los campos/atributos y métodos de la clase existente.

 

Declara una jerarquía de herencia

En Java, cada clase solo puede derivarse de otra clase. Esa clase se llama superclase, o clase padre. La clase derivada se llama subclase o clase secundaria.

Utiliza la palabra clave extends para identificar la clase que extiende su subclase. Si no declara una superclase, su clase amplía implícitamente la clase Object. El objeto es la raíz de todas las jerarquías de herencia; Es la única clase en Java que no se extiende de otra clase.

 

Ejemplo de cómo usar la herencia en Java

Para entender el concepto mejor, crearemos una superclase llamada DosDimensiones, que almacena el ancho y la altura de un objeto bidimensional, y una subclase llamada Triángulo que usaremos la palabra clave extends para crear esa subclase.

//Clase para objetos 
de dos dimensiones
class DosDimensiones{
    double base;
    double altura;

    void mostrarDimension(){
        System.out.println("La
        base y altura es: "+base+"
        y "+altura);
    }
}

 

//Una subclase de DosDimensiones
para Triangulo
class Triangulo 
extends DosDimensiones{
 String estilo;

    double area(){
     return base*altura/2;
    }

    void mostrarEstilo(){
     System.out.println
   ("Triangulo es: "+estilo);
  }
}

class Lados3{
  public static void 
  main(String[] args) {
   Triangulo t1=new Triangulo();
   Triangulo t2=new Triangulo();

   t1.base=4.0;
   t1.altura=4.0;
   t1.estilo="Estilo 1";

   t2.base=8.0;
   t2.altura=12.0;
   t2.estilo="Estilo 2";

    System.out.println("Información
   para T1: ");
    t1.mostrarEstilo();
    t1.mostrarDimension();
    System.out.println("Su área es:
     "+t1.area());

    System.out.println();

    System.out.println("Información
   para T2: ");
    t2.mostrarEstilo();
    t2.mostrarDimension();
    System.out.println("Su área es:
     "+t2.area());

    }
}

 

En el programa anterior, cuando se crea un objeto de clase Triangulo, una copia de todos los métodos y campos de la superclase adquiere memoria en este objeto. Es por eso que, al usar el objeto de la subclase, también podemos acceder a los miembros de una superclase.

 

Herencia y modificadores de acceso

Los modificadores de acceso definen qué clases pueden acceder a un atributo o método. esto podría servir por ejemplo para ser usados para proteger la información o mejor dicho definir cómo nuestro programa accede a ella. Es decir, los modificadores de acceso afectan a las entidades y los atributos a los que puede acceder dentro de una jerarquía de herencia.

Aunque así de pronto esto pueda parecer complejo lo mejor, para entenderlo, es resumir sus características en una descripción general rápida de los diferentes modificadores:

  • Solo se puede acceder a los atributos o métodos privados (private) dentro de la misma clase.
  • Se puede acceder a los atributos y métodos sin un modificador de acceso dentro de la misma clase, y por todas las demás clases dentro del mismo paquete.
  • Se puede acceder a los atributos o métodos protegidos (protected) dentro de la misma clase, por todas las clases dentro del mismo paquete y por todas las subclases.
  • Todas las clases pueden acceder a los atributos y métodos públicos.

Como puedes ver en esta lista, una subclase puede acceder a todos los atributos y métodos públicos y protegidos de la superclase. Siempre que la subclase y la superclase pertenecen al mismo paquete, la subclase también puede acceder a todos los atributos y métodos privados del paquete de la superclase.

La siguiente tabla muestra el acceso a los miembros permitido por cada modificador:

herencia en programacion

Por supuesto para verlo más claro lo mejor es siempre un ejemplo. En la red hay muchos pero creo que uno de los más claros y completos es el ejemplo del programa CoffeeMachine que puedes encontrar en el siguiente enlace en GitHub. Este código es además muy interesante porque aparecen otros conceptos como método sobrecargado, poliformismo y clases abstractas que veremos en futuros artículos. Así que si quieres te invito a descargar el código, analizarlo al completo y probar a compilarlo para ir avanzando.

package org.thoughts.on.java.coffee;
import java.util.HashMap;
import java.util.Map; 
 
public class BasicCoffeeMachine { 
    protected Map configMap; 
    protected Map beans; 
    protected Grinder grinder; 
    protected BrewingUnit brewingUnit; 
 
    public BasicCoffeeMachine(Map beans) { 
        this.beans = beans; 
        this.grinder = new Grinder(); 
        this.brewingUnit = new BrewingUnit(); 
 
        this.configMap = new HashMap(); 
        this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480)); 
    } 
 
    public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException { 
        switch (selection) { 
            case FILTER_COFFEE: 
                return brewFilterCoffee(); 
            default: 
                throw new CoffeeException("CoffeeSelection [" + selection + "] not supported!"); 
        } 

 } 
 
    private Coffee brewFilterCoffee() { 
        Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE); 
 
        // grind the coffee beans 
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee()); 
 
        // brew a filter coffee 
        return this.brewingUnit.brew(
            CoffeeSelection.FILTER_COFFEE, groundCoffee, config.getQuantityWater()); 
    } 
 
    public final void addBeans(CoffeeSelection sel, CoffeeBean newBeans)
        throws CoffeeException {
        CoffeeBean existingBeans = this.beans.get(sel);

        if (existingBeans != null) { 
            if (existingBeans.getName().equals(newBeans.getName())) { 
                existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity()); 
            } else { 
                throw new CoffeeException(
                    "Only one kind of beans supported for each CoffeeSelection."); 
            } 
        } else { 
            this.beans.put(sel, newBeans); 
        } 
    } 
}

En donde ahora la clase PremiumCoffeeMachine es una subclase de la clase BasicCoffeeMachine.

package org.thoughts.on.java.coffee; 
import java.util.Map; 
 
public class PremiumCoffeeMachine extends BasicCoffeeMachine { 
    public PremiumCoffeeMachine(Map beans) { 
        // call constructor in superclass 
        super(beans); 
 
       // add configuration to brew espresso 
         this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28)); 
    }  
 
    private Coffee brewEspresso() { 
        Configuration config = configMap.get(CoffeeSelection.ESPRESSO); 
 
        // grind the coffee beans 
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee()); 
 
        // brew an espresso 
        return this.brewingUnit.brew(
            CoffeeSelection.ESPRESSO, groundCoffee, config.getQuantityWater()); 
    } 
 
    public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException { 
        if (selection == CoffeeSelection.ESPRESSO) {
            return brewEspresso(); 
        } else {
            return super.brewCoffee(selection);
        } 
    } 
}

Un poco lío, ¿verdad? Pero si nos fijamos un poco podemos leer en el código:

public PremiumCoffeeMachine(Map beans) { 
    // call constructor in superclass 
        super(beans); 
 
    // add configuration to brew espresso 
    this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28)); 
}

Primero la palabra clave super sirve para llamar al constructor de la superclase. Como el constructor es público y la subclase puede acceder a él no hay problema en crear el objeto.

Nota: La palabra clave super puede ser usado para acceder a un atributo o para llamar a un método de la superclase que la subclase actual anula.

Pero configMap es un atributo protegido que solo puede definirse mediante la clase BasicCoffeeMachine. Al extender esa clase, el atributo también se convierte en parte de la clase PremiumCoffeeMachine, y puedo agregar la configuración que se requiere para preparar un café expreso en el Map.

 

Tipos de herencia en Java

Java, como la mayoría de lenguajes de programación modernos, dispone de diferentes tipos de herencia que podemos usar para hacer todavía más eficiente nuestro programa al añadir características o atributos procedentes de diferentes clases. Lo que sí debemos tener en cuenta es que solo puede existir una superclase, como veremos en los siguientes tipos de herencia:

  • Herencia única: donde las subclases heredan las características de solo una superclase.
  • Herencia Multinivel: una clase derivada heredará una clase base y, además, la clase derivada también actuará como la clase base de otra clase.
  • Herencia Jerárquica: una clase sirve como una superclase (clase base) para más de una subclase.
  • Herencia Múltiple (a través de interfaces): una clase puede tener más de una superclase y heredar características de todas las clases principales. Pero Java no admite herencia múltiple con clases, así que para lograrlo tenemos que usar Interfaces.
  • Herencia Híbrida (a través de Interfaces): Es una mezcla de dos o más tipos de herencia anteriores. Como Java no admite herencia múltiple con clases, la herencia híbrida tampoco es posible con clases, pero como en el ejemplo anterior, podemos lograr el mismo resultado a través de Interfaces.

En resumen, la herencia nos abre un mundo de posibilidades y combinaciones que nos ayudan a trabajar en desarrollos complejos con más eficiencia, mostrando código ordenado y estructurado que resultará no solo fácil de leer, sino también de mantener en un futuro dentro de su ciclo de desarrollo.

 

Guía de Posibilidades Profesionales en el Ecosistema de Java