JavaRush /Blogue Java /Random-PT /Herança múltipla em Java. Composição vs. Herança
DSergey_Kh
Nível 12

Herança múltipla em Java. Composição vs. Herança

Publicado no grupo Random-PT

Herança múltipla em Java

A herança múltipla permite criar uma classe que herda de múltiplas superclasses. Ao contrário de algumas outras linguagens de programação orientadas a objetos populares, como C++, Java não permite herança múltipla de classes. Java não suporta herança de múltiplas classes porque pode levar ao problema do diamante. E em vez de procurar maneiras de resolver esse problema, existem opções melhores de como podemos alcançar o mesmo resultado, como herança múltipla.

Problema de diamante

Para entender o problema do diamante mais facilmente, vamos supor que herança múltipla seja suportada em Java. Neste caso, poderíamos ter uma hierarquia de classes conforme mostrado na imagem abaixo. Herança múltipla em Java.  Composição vs. Herança - 1Vamos supor que a classe SuperClassseja abstrata e algum método seja declarado nela. Ambas as classes concretas ClassAe ClassB.
package com.journaldev.inheritance;
public abstract class SuperClass {
	public abstract void doSomething();
}
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of A");
	}
	//ClassA own method
	public void methodA(){
	}
}
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of B");
	}
	//ClassB specific method
	public void methodB(){
	}
}
Agora suponha que queremos implementá-lo ClassCe herdá-lo de ClassAe ClassB.
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
	public void test(){
		//calling super class method
		doSomething();
	}
}
Observe que o método test()chama o método da superclasse doSomething(). Isso leva à ambigüidade porque o compilador não sabe qual método da superclasse executar. Este é um diagrama de classes em forma de diamante chamado problema de diamante. Esta é a principal razão pela qual Java não suporta herança múltipla. Observe que o problema acima com herança de múltiplas classes só pode acontecer com três classes que possuem pelo menos um método comum.

Herança de múltiplas interfaces

Em Java, a herança múltipla não é suportada em classes, mas é suportada em interfaces. E uma interface pode estender muitas outras interfaces. Abaixo está um exemplo simples.
package com.journaldev.inheritance;
public interface InterfaceA {
	public void doSomething();
}
package com.journaldev.inheritance;
public interface InterfaceB {
	public void doSomething();
}
Observe que ambas as interfaces declaram o mesmo método. Agora podemos criar uma interface que estenda ambas as interfaces, conforme mostrado no exemplo abaixo.
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
	//same method is declared in InterfaceA and InterfaceB both
	public void doSomething();
}
Isso funciona muito bem porque as interfaces apenas declaram métodos e a implementação será feita nas classes que herdam a interface. Portanto, não há como obter ambiguidade na herança de múltiplas interfaces.
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();

		//all the method calls below are going to same concrete implementation
		objA.doSomething();
		objB.doSomething();
		objC.doSomething();
	}
}
Observe que sempre que você substituir qualquer método de superclasse ou implementar um método de interface, use a anotação @Override. E se quisermos usar uma função methodA()de uma classe ClassAe uma função methodB()de uma classe ClassBem uma classe ClassC? A solução está no uso da composição. Abaixo está uma versão da classe ClassCque utiliza composição para definir os métodos das classes e o método doSomething()de um dos objetos.
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 vs. Herança

Uma das melhores práticas de programação Java é “aprovar a composição antes da herança”. Exploraremos alguns dos aspectos que favorecem esta abordagem.
  1. Digamos que temos uma superclasse e uma classe que a estende:

    package com.journaldev.inheritance;
    public class ClassC{
    	public void methodC(){
    	}
    }
    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 mudarmos a implementação da classe ClassCconforme mostrado abaixo:

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

    Observe que o método test()já existe na subclasse, mas o tipo de retorno é diferente. Agora a classe ClassDnão será compilada e se você usar qualquer IDE, ele solicitará que você altere o tipo de retorno na superclasse ou subclasse.

    Agora imagine uma situação em que temos uma hierarquia de herança de classes multinível e não temos acesso à superclasse. Não teremos escolha a não ser alterar a assinatura do método da subclasse ou seu nome para remover o erro de compilação. Teremos também que alterar o método da subclasse em todos os locais onde ele é chamado. Assim, a herança torna nosso código frágil.

    O problema acima nunca acontecerá com a composição e isso a torna mais atrativa para herança.

  2. Outro problema com herança é que expomos todos os métodos da superclasse ao cliente e se nossa superclasse não for projetada corretamente e houver falhas de segurança, mesmo que implementemos a melhor implementação de nossa classe, somos afetados pela má implementação da superclasse. A composição nos ajuda a fornecer acesso controlado aos métodos da superclasse, enquanto a herança não fornece controle sobre os métodos da superclasse. Este também é um dos principais benefícios da composição por herança.

  3. Outra vantagem da composição é que ela permite flexibilidade na chamada de métodos. Nossa implementação da classe ClassCapresentada acima não é ideal e garante que o tempo de compilação esteja vinculado ao método que será chamado. Com alterações mínimas podemos tornar a chamada do método flexível e dinâmica.

    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 resultado do programa apresentado acima:

    doSomething implementation of A
    doSomething implementation of B

    Esta flexibilidade na chamada de métodos não está disponível com herança, o que acrescenta outro benefício à escolha da composição.

  4. O teste unitário é mais fácil de fazer com composição porque sabemos que estamos usando todos os métodos da superclasse e podemos copiá-los para o teste. Já na herança dependemos mais da superclasse e não conhecemos todos os métodos da superclasse que serão utilizados. Então temos que testar todos os métodos da superclasse, o que é um trabalho extra devido à herança.

    Idealmente, só deveríamos usar herança quando o relacionamento de subclasse para superclasse for definido como “é”. Em todos os outros casos, recomenda-se o uso da composição.

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