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. Vamos supor que a classeSuperClass
seja abstrata e algum método seja declarado nela. Ambas as classes concretas ClassA
e 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 ClassC
e herdá-lo de ClassA
e 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 ClassA
e uma função methodB()
de uma classe ClassB
em uma classe ClassC
? A solução está no uso da composição. Abaixo está uma versão da classe ClassC
que 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.-
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
ClassC
conforme 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 classeClassD
nã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.
-
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.
-
Outra vantagem da composição é que ela permite flexibilidade na chamada de métodos. Nossa implementação da classe
ClassC
apresentada 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.
-
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.
GO TO FULL VERSION