JavaRush /Blogue Java /Random-PT /Herança múltipla em Java. Comparação de composição e hera...
HonyaSaar
Nível 26
Москва

Herança múltipla em Java. Comparação de composição e herança

Publicado no grupo Random-PT
Há algum tempo escrevi vários posts sobre herança, interfaces e composição em Java. Neste artigo, veremos a herança múltipla e depois aprenderemos sobre os benefícios da composição em relação à herança.
Herança múltipla em Java.  Comparação de composição e herança - 1

Herança múltipla em Java

Herança múltipla é a capacidade de criar classes com múltiplas classes pai. Ao contrário de outras linguagens orientadas a objetos populares, como C++, Java não oferece suporte a herança de múltiplas classes. Ele não o apóia devido à probabilidade de encontrarmos o “problema do diamante” e, em vez disso, prefere fornecer algum tipo de abordagem abrangente para resolvê-lo, usando as melhores opções para que possamos alcançar um resultado de herança semelhante.

"O problema do diamante"

Para entender o problema do diamante de forma mais simples, vamos supor que herança múltipla seja suportada em Java. Neste caso, podemos obter classes com a hierarquia mostrada na figura abaixo. hierarquia de classes de diamanteVamos supor que SuperClassseja uma classe abstrata que descreve um determinado método, e as classes ClassAe ClassBsejam classes reais. 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(){
    }
}
Agora, vamos supor que a classe ClassCherde de ClassAe ClassBao mesmo tempo, e ao mesmo tempo tenha a seguinte implementação:
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
    public void test(){
        //вызов метода родительского класса
        doSomething();
    }
}
Observe que o método test()chama um método doSomething()da classe pai, o que levará à ambiguidade porque o compilador não sabe qual método da superclasse deve ser chamado. Devido ao formato do diagrama de herança de classes nesta situação, que lembra o contorno de um diamante facetado, o problema é chamado de “Problema do Diamante”. Esta é a principal razão pela qual Java não suporta herança de múltiplas classes. Observe que esse problema com herança de múltiplas classes também pode ocorrer com três classes que possuem pelo menos um método comum.

Herança múltipla e interfaces

Você deve ter notado que eu sempre digo "herança múltipla não é suportada entre classes", mas é suportada entre interfaces. Um exemplo simples é mostrado abaixo: 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 as interfaces possuem um método com o mesmo nome. Agora digamos que temos uma interface que herda de ambas as interfaces. InterfaceC.java
package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

    //метод, с тем же названием описан в  InterfaceA и InterfaceB
    public void doSomething();
Aqui tudo é ideal, pois as interfaces são apenas uma reserva/descrição de um método, e a implementação do método em si será na classe concreta que implementa essas interfaces, portanto não há possibilidade de encontrar ambiguidade com herança múltipla de interfaces. É por isso que as classes em Java podem herdar de múltiplas interfaces. Vamos mostrar isso com o exemplo abaixo. 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();
    }
}
Você deve ter notado que toda vez que sobrescrevo um método descrito em uma superclasse ou interface, utilizo a anotação @Override. Esta é uma das três anotações Java integradas e você deve sempre usá-la ao substituir métodos.

Composição como salvação

E daí se quisermos usar uma methodA()classe ClassAe uma função de methodB()classe ? Uma solução para isso poderia ser a composição - uma versão reescrita que implementa ambos os métodos de classe e também possui uma implementação para um dos objetos. ClassB ClassС ClassC ClassA ClassB doSomething() 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();
    }
}

Composição ou herança?

É uma boa prática de programação Java aproveitar a composição em vez da herança. Veremos alguns aspectos a favor desta abordagem.
  1. Suponha que temos a seguinte combinação de classes pai-herdeiro:

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

    O código acima compila e funciona bem, mas e se ClassCfosse implementado de forma diferente:

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

    Observe que o método test()já existe na classe descendente, mas retorna um resultado de tipo diferente. Agora ClassD, caso você esteja usando um IDE, ele não irá compilar. Você será aconselhado a alterar o tipo de retorno no descendente ou na superclasse.

    Agora vamos imaginar uma situação em que haja herança de classes multinível e a superclasse não esteja disponível para nossas alterações. Agora, para nos livrarmos do erro de compilação, não temos outra opção a não ser alterar a assinatura ou o nome do método da subclasse. Teremos também que fazer alterações em todos os locais onde este método foi chamado. Assim, a herança torna nosso código frágil.

    O problema descrito acima nunca ocorre no caso de composição e, portanto, torna esta última preferível à herança.

  2. O próximo problema com herança é que expomos todos os métodos do pai ao cliente. E se a superclasse não for projetada corretamente e contiver falhas de segurança. Então, mesmo que tomemos todo cuidado com a segurança na implementação de nossa subclasse, ainda dependeremos da implementação falha da classe pai.

    A composição nos ajuda a fornecer acesso controlado aos métodos de uma superclasse, enquanto a herança não mantém nenhum controle sobre seus métodos. Esta é também uma das principais vantagens da composição sobre a herança.

  3. Outro benefício da composição é que ela adiciona flexibilidade ao chamar métodos. A implementação da classe ClassCdescrita acima não é ideal e utiliza ligação antecipada ao método chamado. Mudanças mínimas nos permitirão tornar a chamada de método flexível e permitir vinculação tardia (vinculação em tempo de execução).

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

    O programa acima exibirá:

    doSomething implementation of A
    doSomething implementation of B

    Essa flexibilidade na chamada de métodos não é vista na herança, o que torna a composição a melhor abordagem.

  4. O teste unitário é mais fácil no caso de composição porque sabemos que para todos os métodos que são usados ​​na superclasse podemos fazer stub dos testes, enquanto na herança dependemos muito da superclasse e não sabemos como funcionam os métodos da classe pai. será usado. Então, devido à herança, teremos que testar todos os métodos da superclasse, o que é um trabalho desnecessário.

    Idealmente, a herança só deve ser usada quando o relacionamento “ is-a ” for verdadeiro para as classes pai e filho, caso contrário, a composição deve ser preferida.

Artigo original
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION