JavaRush /Blog Java /Random-ES /Herencia múltiple en Java. Comparación de composición y h...
HonyaSaar
Nivel 26
Москва

Herencia múltiple en Java. Comparación de composición y herencia.

Publicado en el grupo Random-ES
Hace un tiempo escribí varios posts sobre herencia, interfaces y composición en Java. En este artículo, veremos la herencia múltiple y luego aprenderemos sobre los beneficios de la composición sobre la herencia.
Herencia múltiple en Java.  Comparación de composición y herencia - 1

Herencia múltiple en Java

La herencia múltiple es la capacidad de crear clases con varias clases principales. A diferencia de otros lenguajes populares orientados a objetos como C++, Java no admite la herencia de clases múltiples. No lo apoya debido a la probabilidad de encontrar el "problema del diamante" y en cambio prefiere proporcionar algún tipo de enfoque integral para resolverlo, utilizando las mejores opciones para que podamos lograr un resultado de herencia similar.

"El problema del diamante"

Para comprender el problema del diamante de manera más simple, supongamos que Java admite la herencia múltiple. En este caso, podemos obtener clases con la jerarquía que se muestra en la siguiente figura. jerarquía de clases de diamantesSupongamos que SuperClasses una clase abstracta que describe un determinado método, y las clases ClassAy ClassBson clases reales. SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
   	public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
    @Override
 public void doSomething(){
        System.out.println("Какая-то реализация класса A");
    }
  //собственный метод класса  ClassA
    public void methodA(){
    }
}
Ahora, supongamos que la clase ClassChereda de ClassAy ClassBal mismo tiempo, y al mismo tiempo tiene la siguiente implementación:
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
    public void test(){
        //вызов метода родительского класса
        doSomething();
    }
}
Tenga en cuenta que el método test()llama a un método doSomething()de la clase principal, lo que generará ambigüedad porque el compilador no sabe qué método de superclase debe llamarse. Debido a la forma del diagrama de herencia de clases en esta situación, que se asemeja al contorno de un diamante facetado, el problema se denomina "Problema del diamante". Ésta es la razón principal por la que Java no admite la herencia de clases múltiples. Tenga en cuenta que este problema con la herencia de clases múltiples también puede ocurrir con tres clases que tienen al menos un método común.

Herencia múltiple e interfaces.

Es posible que hayas notado que siempre digo "la herencia múltiple no se admite entre clases", pero sí se admite entre interfaces. A continuación se muestra un ejemplo sencillo: InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {

    public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;

public interface InterfaceB {

    public void doSomething();
}
Observe que ambas interfaces tienen un método con el mismo nombre. Ahora digamos que tenemos una interfaz que hereda de ambas interfaces. InterfaceC.java
package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

    //метод, с тем же названием описан в  InterfaceA и InterfaceB
    public void doSomething();
Aquí todo es ideal, ya que las interfaces son solo una reserva/descripción de un método, y la implementación del método en sí será en la clase concreta que implementa estas interfaces, por lo que no hay posibilidad de encontrar ambigüedad con la herencia múltiple de interfaces. Es por eso que las clases en Java pueden heredar de múltiples interfaces. Mostrémoslo con el siguiente ejemplo. InterfacesImpl.java
package com.journaldev.inheritance;

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

    @Override
    public void doSomething() {
        System.out.println("doSomething реализация реального класса ");
    }

    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();

        //все вызываемые ниже методы получат одинаковую реализацию конкретного класса

        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }
}
Quizás hayas notado que cada vez que anulo un método descrito en una superclase o en una interfaz, uso la anotación @Override. Esta es una de las tres anotaciones integradas de Java y siempre debe usarla al anular métodos.

La composición como salvación

Entonces, ¿qué pasa si queremos usar una methodA()clase ClassAy una función methodB()de clase ClassBen ClassС? Una solución a esto podría ser la composición: una versión reescrita ClassCque implementa ambos métodos de clase ClassAy ClassBtambién tiene una implementación doSomething()para 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 o herencia?

Es una buena práctica de programación Java aprovechar la composición sobre la herencia. Examinaremos algunos aspectos a favor de este enfoque.
  1. Supongamos que tenemos la siguiente combinación de clases de padre-heredero:

    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 ¿y si ClassCse implementara de manera diferente?

    package com.journaldev.inheritance;
    
    public class ClassC{
    
        public void methodC(){
        }
    
        public void test(){
        }
    }

    Tenga en cuenta que el método test()ya existe en la clase descendiente, pero devuelve un resultado de un tipo diferente. Ahora ClassD, en caso de que esté utilizando un IDE, no se compilará. Se le recomendará que cambie el tipo de devolución en la clase descendiente o superclase.

    Ahora imaginemos una situación en la que hay herencia de clases en varios niveles y la superclase no está disponible para nuestros cambios. Ahora, para deshacernos del error de compilación, no tenemos otra opción que cambiar la firma o el nombre del método de la subclase. También tendremos que realizar cambios en todos los lugares donde se llamó a este método. Por tanto, la herencia hace que nuestro código sea frágil.

    El problema descrito anteriormente nunca se produce en el caso de la composición y, por tanto, hace que esta última sea preferible a la herencia.

  2. El siguiente problema con la herencia es que exponemos todos los métodos del padre al cliente. Y si la superclase no está diseñada muy correctamente y contiene agujeros de seguridad. Entonces, aunque cuidemos por completo la seguridad en la implementación de nuestra subclase, seguiremos dependiendo de la implementación defectuosa de la clase principal.

    La composición nos ayuda a proporcionar acceso controlado a los métodos de una superclase, mientras que la herencia no mantiene ningún control sobre sus métodos. Ésta es también una de las principales ventajas de la composición sobre la herencia.

  3. Otro beneficio de la composición es que agrega flexibilidad al llamar a métodos. La implementación de la clase ClassCdescrita anteriormente no es óptima y utiliza enlace temprano al método llamado. Los cambios mínimos nos permitirán hacer que las llamadas a métodos sean flexibles y permitirán la vinculación tardía (vinculación en tiempo de ejecución).

    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();
        }
    }

    El programa anterior mostrará:

    doSomething implementation of A
    doSomething implementation of B

    Esta flexibilidad en la llamada a métodos no se ve en la herencia, lo que hace que la composición sea el mejor enfoque.

  4. Las pruebas unitarias son más fáciles en el caso de composición porque sabemos que para todos los métodos que se usan en la superclase podemos eliminar las pruebas, mientras que en herencia dependemos en gran medida de la superclase y no sabemos cómo funcionan los métodos de la clase principal. se utilizará. Entonces, debido a la herencia, tendremos que probar todos los métodos de la superclase, lo cual es un trabajo innecesario.

    Idealmente, la herencia solo debería usarse cuando la relación " es-a " es verdadera para las clases padre e hijo; de lo contrario, se debería preferir la composición.

Artículo original
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION