Hoy vamos a analizar la Herencia Múltiple en Java. Hace algún tiempo escribí algunas publicaciones sobre herencia, interfaz y composición en Java. En esta publicación, examinaremos la herencia múltiple en Java y luego compararemos la composición y la herencia.
Herencia Múltiple en Java
La herencia múltiple en Java es la capacidad de crear una sola clase con múltiples superclases. A diferencia de algunos otros lenguajes de programación orientados a objetos populares como C++, Java no proporciona soporte para la herencia múltiple en clases. Java no admite la herencia múltiple en clases porque puede conducir al problema del diamante y en lugar de proporcionar una forma compleja de resolverlo, existen mejores formas a través de las cuales podemos lograr el mismo resultado que con las herencias múltiples.
Problema del Diamante en Java
Para entender fácilmente el problema del diamante, supongamos que Java admitiera herencia múltiple. En ese caso, podríamos tener una jerarquía de clases como se muestra en la siguiente imagen. Digamos que SuperClase es una clase abstracta que declara algunos métodos, y ClassA y ClassB son clases concretas.
SuperClase.java
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
ClaseA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
// Método propio de ClaseA
public void methodA(){
}
}
ClaseB.java
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
// Método específico de ClaseB
public void methodB(){
}
}
Ahora supongamos que la implementación de ClaseC sería algo como lo siguiente y que está extendiendo tanto a ClaseA como a ClaseB. ClaseC.java
package com.journaldev.inheritance;
// Esto es solo una suposición para explicar el problema del diamante
// Este código no se compilará
public class ClassC extends ClassA, ClassB{
public void test(){
// Llamada al método de la superclase
doSomething();
}
}
Observa que el método test()
realiza una llamada al método doSomething()
de la superclase. Esto lleva a la ambigüedad, ya que el compilador no sabe qué método de la superclase ejecutar. Debido al diagrama de clases en forma de diamante, se le conoce como el Problema del Diamante en Java. El problema del diamante en Java es la razón principal por la que Java no admite herencia múltiple en clases. Ten en cuenta que el problema mencionado con la herencia múltiple de clases también puede ocurrir con solo tres clases, donde todas tienen al menos un método común.
Herencia Múltiple en Interfaces de Java
Puede que hayas notado que siempre digo que la herencia múltiple no está admitida en clases, pero sí está admitida en interfaces. Una única interfaz puede extender varias interfaces, aquí tienes un ejemplo simple. InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
Observa que ambas interfaces declaran el mismo método. Ahora podemos tener una interfaz que extienda ambas interfaces de la siguiente manera. InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//el mismo método está declarado en InterfaceA e InterfaceB
public void doSomething();
}
Esto está perfectamente bien porque las interfaces solo declaran los métodos y la implementación real será realizada por clases concretas que implementan las interfaces. Por lo tanto, no hay posibilidad de ningún tipo de ambigüedad en herencias múltiples en las interfaces de Java. Es por eso que una clase java puede implementar múltiples interfaces, algo así como en el ejemplo siguiente. InterfacesImpl.java
package com.journaldev.inheritance;
public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {
@Override
public void doSomething() {
System.out.println("doSomething implementation of concrete class");
}
public static void main(String[] args) {
InterfaceA objA = new InterfacesImpl();
InterfaceB objB = new InterfacesImpl();
InterfaceC objC = new InterfacesImpl();
// todas las llamadas de método a continuación van a la misma implementación concreta
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
¿Notaste que cada vez que anulo un método de la superclase o implemento un método de la interfaz, estoy usando la anotación @Override? La anotación Override es una de las tres anotaciones integradas en Java y siempre debemos usar la anotación override al anular cualquier método.
Composición para el rescate
Entonces, ¿qué hacer si queremos utilizar la función methodA()
de ClassA
y la función methodB()
de ClassB
en ClassC
? La solución radica en usar composición. Aquí hay una versión refactorizada de ClassC que utiliza composición para aprovechar los métodos de ambas clases y también utiliza el método doSomething() de uno de los objetos. ClassC.java
package com.journaldev.inheritance;
public class ClassC{
ClassA objA = new ClassA();
ClassB objB = new ClassB();
public void test(){
objA.doSomething();
}
public void methodA(){
objA.methodA();
}
public void methodB(){
objB.methodB();
}
}
Composición vs Herencia
Una de las mejores prácticas de programación en Java es “favorecer la composición sobre la herencia”. Analizaremos algunos de los aspectos que favorecen este enfoque.
-
Supongamos que tenemos una superclase y una subclase de la siguiente manera:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } }
ClassD.java
package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
El código anterior se compila y funciona bien, pero ¿qué pasa si la implementación de ClassC cambia como se muestra a continuación:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
Observa que el método
test()
ya existe en la subclase, pero el tipo de retorno es diferente. Ahora, ClassD no se compilará y si estás utilizando algún IDE, te sugerirá cambiar el tipo de retorno en la superclase o en la subclase. Imagina la situación en la que tenemos múltiples niveles de herencia de clases y la superclase no está controlada por nosotros. No tendremos más opción que cambiar la firma o el nombre del método de nuestra subclase para eliminar el error de compilación. Además, tendremos que realizar cambios en todos los lugares donde se invocaba el método de nuestra subclase, lo que hace que la herencia vuelva nuestro código frágil. Este problema nunca ocurrirá con la composición, lo que la hace más favorable que la herencia. -
Otro problema con la herencia es que estamos exponiendo todos los métodos de la superclase al cliente y si nuestra superclase no está diseñada correctamente y hay agujeros de seguridad, entonces, aunque nos aseguremos de implementar completamente nuestra clase, nos veremos afectados por la mala implementación de la superclase. La composición nos ayuda a proporcionar un acceso controlado a los métodos de la superclase, mientras que la herencia no proporciona ningún control sobre los métodos de la superclase, esta también es una de las principales ventajas de la composición sobre la herencia.
-
Otro beneficio con la composición es que proporciona flexibilidad en la invocación de métodos. Nuestra implementación anterior de
ClassC
no es óptima y proporciona enlace en tiempo de compilación con el método que se invocará; con un cambio mínimo, podemos hacer que la invocación del método sea flexible y dinámica.ClassC.java
package com.journaldev.inheritance; public class ClassC{ SuperClass obj = null; public ClassC(SuperClass o){ this.obj = o; } public void test(){ obj.doSomething(); } public static void main(String args[]){ ClassC obj1 = new ClassC(new ClassA()); ClassC obj2 = new ClassC(new ClassB()); obj1.test(); obj2.test(); } }
La salida del programa anterior es:
Implementación de doSomething de A Implementación de doSomething de B
Esta flexibilidad en la invocación de métodos no está disponible en la herencia y refuerza la mejor práctica de favorecer la composición sobre la herencia.
-
Las pruebas unitarias son fáciles en la composición porque sabemos qué métodos estamos utilizando de la superclase y podemos simularlo para las pruebas. En cambio, en la herencia dependemos en gran medida de la superclase y no sabemos qué métodos de la superclase se utilizarán, por lo que debemos probar todos los métodos de la superclase, lo cual es un trabajo adicional e innecesario debido a la herencia.
Eso es todo para las herencias múltiples en Java y un vistazo breve a la composición.
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java