Hoje vamos analisar a Herança Múltipla em Java. Algum tempo atrás, escrevi alguns posts sobre herança, interface e composição em Java. Neste post, vamos explorar a herança múltipla em Java e depois comparar com a composição e herança.
Herança Múltipla em Java
A herança múltipla em Java é a capacidade de criar uma única classe com múltiplas superclasses. Ao contrário de algumas outras linguagens populares de programação orientada a objetos, como C++, o Java não oferece suporte para herança múltipla em classes. O Java não suporta heranças múltiplas em classes porque isso pode levar ao problema do diamante e, em vez de fornecer uma maneira complexa de resolvê-lo, existem maneiras melhores de alcançar o mesmo resultado da herança múltipla.
Problema do Diamante em Java
Para entender o problema do diamante facilmente, vamos supor que múltiplas heranças são suportadas em Java. Nesse caso, poderíamos ter uma hierarquia de classes como na imagem abaixo. Digamos que SuperClasse seja uma classe abstrata declarando alguns métodos e ClassA, ClassB sejam classes concretas.
SuperClasse.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("doSomething implementation of A");
}
//Método próprio da ClassA
public void methodA(){
}
}
ClassB.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 da ClassB
public void methodB(){
}
}
Agora, digamos que a implementação da ClassC seria algo como abaixo, estendendo tanto a ClassA quanto a ClassB. ClassC.java
package com.journaldev.inheritance;
//isso é apenas uma suposição para explicar o problema do diamante
//esse código não compilará
public class ClassC extends ClassA, ClassB{
public void test(){
//chamando o método da super classe
doSomething();
}
}
Observe que o método test()
está fazendo uma chamada ao método da superclasse doSomething()
. Isso leva à ambiguidade, pois o compilador não sabe qual método da superclasse executar. Devido ao diagrama de classe em forma de diamante, isso é referido como o Problema do Diamante em Java. O problema do diamante em Java é a principal razão pela qual o Java não suporta herança múltipla em classes. Observe que o problema mencionado acima com a herança múltipla de classes também pode ocorrer com apenas três classes, onde todas elas têm pelo menos um método comum.
Herança Múltipla em Interfaces Java
Você pode ter notado que estou sempre dizendo que a herança múltipla não é suportada em classes, mas é suportada em interfaces. Uma única interface pode estender várias interfaces, abaixo está um exemplo simples. 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 estão declarando o mesmo método, agora podemos ter uma interface estendendo ambas essas interfaces como abaixo. InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//mesmo método é declarado em InterfaceA e InterfaceB
public void doSomething();
}
Isso é perfeitamente aceitável porque as interfaces estão apenas declarando os métodos e a implementação real será feita por classes concretas que implementam as interfaces. Portanto, não há possibilidade de qualquer tipo de ambiguidade em heranças múltiplas em interfaces Java. É por isso que uma classe Java pode implementar múltiplas interfaces, algo como o exemplo abaixo. 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 as chamadas de método abaixo estão indo para a mesma implementação concreta
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
Você percebeu que toda vez que estou substituindo um método da superclasse ou implementando um método de interface, estou usando a anotação @Override. A anotação Override é uma das três anotações java embutidas e devemos sempre usar a anotação de substituição ao substituir qualquer método.
Composição para o resgate
E o que fazer se quisermos utilizar a função methodA()
da ClassA
e a função methodB()
da ClassB
na ClassC
. A solução está em usar composição. Aqui está uma versão refatorada da ClassC que está usando composição para utilizar os métodos de ambas as classes e também usando o método doSomething() de um dos 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();
}
}
Composição vs Herança
Uma das melhores práticas de programação em Java é “favorecer a composição em vez da herança”. Vamos analisar alguns dos aspectos que favorecem essa abordagem.
-
Suponha que tenhamos uma superclasse e uma subclasse como segue:
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 a implementação da ClasseC for alterada como abaixo:
ClassC.java
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 ClasseD não compilará e se você estiver usando qualquer IDE, ela sugerirá que você altere o tipo de retorno na superclasse ou na subclasse. Agora, imagine a situação em que temos vários níveis de herança de classe e a superclasse não é controlada por nós. Não teremos escolha a não ser alterar a assinatura do método da nossa subclasse ou seu nome para remover o erro de compilação. Além disso, teremos que fazer uma alteração em todos os lugares onde o método da nossa subclasse estava sendo invocado, então a herança torna nosso código frágil. O problema acima nunca ocorrerá com composição e isso a torna mais favorável à herança. -
Outro problema com a herança é que estamos expondo todos os métodos da superclasse ao cliente e se nossa superclasse não estiver devidamente projetada e houver falhas de segurança, mesmo que tenhamos cuidado completo na implementação de nossa classe, seremos afetados pela implementação inadequada da superclasse. A composição nos ajuda a fornecer acesso controlado aos métodos da superclasse, enquanto a herança não fornece nenhum controle sobre os métodos da superclasse, este também é uma das principais vantagens da composição sobre a herança.
-
Outra vantagem da composição é que ela fornece flexibilidade na invocação de métodos. Nossa implementação acima da
ClasseC
não é ótima e fornece ligação em tempo de compilação com o método que será invocado. Com uma alteração mínima, podemos tornar a invocação do método flexível e dinâmica.ClasseC.java
package com.journaldev.inheritance; public class ClasseC { SuperClasse obj = null; public ClasseC(SuperClasse o){ this.obj = o; } public void test(){ obj.fazerAlgo(); } public static void main(String args[]){ ClasseC obj1 = new ClasseC(new ClasseA()); ClasseC obj2 = new ClasseC(new ClasseB()); obj1.test(); obj2.test(); } }
A saída do programa acima é:
Implementação de fazerAlgo de A Implementação de fazerAlgo de B
Essa flexibilidade na invocação de métodos não está disponível na herança e reforça a prática recomendada de favorecer a composição sobre a herança.
-
Os testes unitários são fáceis na composição porque sabemos quais métodos estamos usando da superclasse e podemos simular isso para testes, enquanto na herança dependemos fortemente da superclasse e não sabemos quais métodos da superclasse serão usados, então precisamos testar todos os métodos da superclasse, o que é um trabalho extra e desnecessário devido à herança.
Isso resume as heranças múltiplas em Java e uma breve visão sobre a composição.
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java