Guía de herencia en Java

1. Información general

Uno de los principios fundamentales de la programación orientada a objetos, la herencia, nos permite reutilizar el código existente o ampliar un tipo existente.

En pocas palabras, en Java, una clase puede heredar otra clase y múltiples interfaces, mientras que una interfaz puede heredar otras interfaces.

En este artículo, comenzaremos con la necesidad de herencia, pasando a cómo funciona la herencia con clases e interfaces.

Luego, cubriremos cómo los nombres de variables / métodos y los modificadores de acceso afectan a los miembros que se heredan.

Y al final, veremos qué significa heredar un tipo.

2. La necesidad de la herencia

Imagine que, como fabricante de automóviles, ofrece varios modelos de automóviles a sus clientes. Aunque los diferentes modelos de automóviles pueden ofrecer diferentes características, como un techo corredizo o ventanas a prueba de balas, todos incluirían componentes y características comunes, como motor y ruedas.

Tiene sentido crear un diseño básico y extenderlo para crear sus versiones especializadas, en lugar de diseñar cada modelo de automóvil por separado, desde cero.

De manera similar, con la herencia, podemos crear una clase con características y comportamiento básicos y crear sus versiones especializadas, creando clases, que heredan esta clase base. De la misma manera, las interfaces pueden ampliar las interfaces existentes.

Notaremos el uso de varios términos para referirnos a un tipo que es heredado por otro tipo, específicamente:

  • un tipo base también se llama tipo super o padre
  • un tipo derivado se denomina extendido, secundario o tipo secundario

3. Herencia de clases

3.1. Ampliando una clase

Una clase puede heredar otra clase y definir miembros adicionales.

Comencemos por definir un auto de clase base :

public class Car { int wheels; String model; void start() { // Check essential parts } }

La clase ArmoredCar puede heredar los miembros de la clase Car usando la palabra clave extiende en su declaración :

public class ArmoredCar extends Car { int bulletProofWindows; void remoteStartCar() { // this vehicle can be started by using a remote control } }

Ahora podemos decir que la clase ArmoredCar es una subclase de Car, y la última es una superclase de ArmoredCar.

Las clases en Java admiten herencia única ; la clase ArmoredCar no puede extender varias clases.

Además, tenga en cuenta que, en ausencia de una palabra clave extendida , una clase hereda implícitamente la clase java.lang.Object .

Una clase de subclase hereda los miembros públicos y protegidos no estáticos de la clase de superclase. Además, los miembros con acceso predeterminado y al paquete se heredan si las dos clases están en el mismo paquete.

Por otro lado, los miembros privados y estáticos de una clase no se heredan.

3.2. Acceso a los miembros principales desde una clase secundaria

Para acceder a propiedades o métodos heredados, simplemente podemos usarlos directamente:

public class ArmoredCar extends Car { public String registerModel() { return model; } }

Tenga en cuenta que no necesitamos una referencia a la superclase para acceder a sus miembros.

4. Herencia de la interfaz

4.1. Implementación de múltiples interfaces

Aunque las clases pueden heredar solo una clase, pueden implementar múltiples interfaces.

Imagina que el ArmoredCar que definimos en la sección anterior es necesario para un súper espía. Entonces, la empresa de fabricación de automóviles pensó en agregar funcionalidad de vuelo y flotación:

public interface Floatable { void floatOnWater(); }
public interface Flyable { void fly(); }
public class ArmoredCar extends Car implements Floatable, Flyable{ public void floatOnWater() { System.out.println("I can float!"); } public void fly() { System.out.println("I can fly!"); } }

En el ejemplo anterior, notamos el uso de la palabra clave implements para heredar de una interfaz.

4.2. Problemas con la herencia múltiple

Java permite la herencia múltiple mediante interfaces.

Hasta Java 7, esto no era un problema. Las interfaces solo pueden definir métodos abstractos , es decir, métodos sin implementación. Entonces, si una clase implementó múltiples interfaces con la misma firma de método, no fue un problema. La clase de implementación finalmente tuvo solo un método para implementar.

Veamos cómo cambió esta sencilla ecuación con la introducción de métodos predeterminados en las interfaces, con Java 8.

A partir de Java 8, las interfaces pueden optar por definir implementaciones predeterminadas para sus métodos (una interfaz aún puede definir métodos abstractos ). Esto significa que si una clase implementa múltiples interfaces, que definen métodos con la misma firma, la clase secundaria heredaría implementaciones separadas. Esto suena complejo y no está permitido.

Java no permite la herencia de múltiples implementaciones de los mismos métodos, definidos en interfaces separadas.

He aquí un ejemplo:

public interface Floatable { default void repair() { System.out.println("Repairing Floatable object"); } }
public interface Flyable { default void repair() { System.out.println("Repairing Flyable object"); } }
public class ArmoredCar extends Car implements Floatable, Flyable { // this won't compile }

Si queremos implementar ambas interfaces, tendremos que anular el método repair () .

Si las interfaces en los ejemplos anteriores definen variables con el mismo nombre, digamos duración , no podemos acceder a ellas sin antes del nombre de la variable con el nombre de la interfaz:

public interface Floatable { int duration = 10; }
public interface Flyable { int duration = 20; }
public class ArmoredCar extends Car implements Floatable, Flyable { public void aMethod() { System.out.println(duration); // won't compile System.out.println(Floatable.duration); // outputs 10 System.out.println(Flyable.duration); // outputs 20 } }

4.3. Interfaces que amplían otras interfaces

Una interfaz puede extender múltiples interfaces. He aquí un ejemplo:

public interface Floatable { void floatOnWater(); }
interface interface Flyable { void fly(); }
public interface SpaceTraveller extends Floatable, Flyable { void remoteControl(); }

Una interfaz hereda otras interfaces mediante el uso de la palabra clave extiende . Las clases usan la palabra clave implements para heredar una interfaz.

5. Tipo de herencia

Cuando una clase hereda otra clase o interfaces, además de heredar sus miembros, también hereda su tipo. Esto también se aplica a una interfaz que hereda otras interfaces.

Este es un concepto muy poderoso, que permite a los desarrolladores programar en una interfaz (clase base o interfaz) , en lugar de programar en sus implementaciones.

Por ejemplo, imagine una condición en la que una organización mantiene una lista de los automóviles que poseen sus empleados. Por supuesto, todos los empleados pueden tener diferentes modelos de automóviles. Entonces, ¿cómo podemos referirnos a diferentes instancias de automóviles? Esta es la solucion:

public class Employee { private String name; private Car car; // standard constructor }

Debido a que todas las clases derivadas de Car heredan el tipo Car , las instancias de la clase derivada se pueden hacer referencia mediante una variable de la clase Car :

Employee e1 = new Employee("Shreya", new ArmoredCar()); Employee e2 = new Employee("Paul", new SpaceCar()); Employee e3 = new Employee("Pavni", new BMW());

6. Miembros ocultos de la clase

6.1. Miembros de instancia oculta

¿Qué sucede si tanto la superclase como la subclase definen una variable o método con el mismo nombre ? No se preocupe; todavía podemos acceder a ambos. Sin embargo, debemos dejar clara nuestra intención a Java, prefijando la variable o método con las palabras clave this o super .

La palabra clave this se refiere a la instancia en la que se usa. La súper palabra clave (como parece obvio) se refiere a la instancia de la clase principal:

public class ArmoredCar extends Car { private String model; public String getAValue() { return super.model; // returns value of model defined in base class Car // return this.model; // will return value of model defined in ArmoredCar // return model; // will return value of model defined in ArmoredCar } }

Muchos desarrolladores usan esta y súper palabras clave para indicar explícitamente a qué variable o método se refieren. Sin embargo, usarlos con todos los miembros puede hacer que nuestro código se vea desordenado.

6.2. Miembros estáticos ocultos

¿Qué sucede cuando nuestra clase base y subclases definen variables y métodos estáticos con el mismo nombre ? ¿Podemos acceder a un miembro estático de la clase base, en la clase derivada, como lo hacemos con las variables de instancia?

Averigüemos usando un ejemplo:

public class Car { public static String msg() { return "Car"; } }
public class ArmoredCar extends Car { public static String msg() { return super.msg(); // this won't compile. } }

No, no podemos. Los miembros estáticos pertenecen a una clase y no a instancias. Por lo tanto, no podemos usar la palabra clave super no estática en msg () .

Dado que los miembros estáticos pertenecen a una clase, podemos modificar la llamada anterior de la siguiente manera:

return Car.msg();

Considere el siguiente ejemplo, en el que tanto la clase base como la clase derivada definen un método estático msg () con la misma firma:

public class Car { public static String msg() { return "Car"; } }
public class ArmoredCar extends Car { public static String msg() { return "ArmoredCar"; } }

Así es como podemos llamarlos:

Car first = new ArmoredCar(); ArmoredCar second = new ArmoredCar();

Para el código anterior, first.msg () generará "Car " y second.msg () generará "ArmoredCar". El mensaje estático que se llama depende del tipo de variable utilizada para hacer referencia a la instancia de ArmoredCar .

7. Conclusión

En este artículo, cubrimos un aspecto central del lenguaje Java: la herencia.

Vimos cómo Java admite la herencia única con clases y la herencia múltiple con interfaces y discutimos las complejidades de cómo funciona el mecanismo en el lenguaje.

Como siempre, el código fuente completo de los ejemplos está disponible en GitHub.