Héritage Multiple en Java

Aujourd’hui, nous allons examiner la notion d’héritage multiple en Java. Il y a quelque temps, j’ai écrit quelques articles sur l’héritage, l’interface et la composition en Java. Dans ce post, nous allons nous pencher sur l’héritage multiple en Java, puis comparer la composition et l’héritage.

Héritage multiple en Java

L’héritage multiple en Java permet de créer une seule classe avec plusieurs superclasses. Contrairement à d’autres langages de programmation orientés objet populaires tels que C++, Java ne prend pas en charge l’héritage multiple dans les classes. Java n’autorise pas l’héritage multiple dans les classes car cela peut entraîner un problème en losange, et plutôt que de fournir une solution complexe, il existe de meilleures façons d’obtenir le même résultat que l’héritage multiple.

Problème de diamant en Java

Pour comprendre facilement le problème de diamant, supposons que l’héritage multiple était supporté en java. Dans ce cas, nous pourrions avoir une hiérarchie de classes comme l’image ci-dessous. Disons que SuperClass est une classe abstraite déclarant une certaine méthode et ClassA, ClassB sont des classes concrètes. 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("doSomething implementation of A");
	}
	
	//Méthode propre à 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éthode spécifique à ClassB
	public void methodB(){
		
	}
}

Maintenant disons que l’implémentation de ClassC serait quelque chose comme ci-dessous et elle étend à la fois ClassA et ClassB. ClassC.java

package com.journaldev.inheritance;

// ceci est juste une supposition pour expliquer le problème de diamant
//ce code ne compilera pas
public class ClassC extends ClassA, ClassB{

	public void test(){
		//appelant la méthode de la super classe
		doSomething();
	}

}

Remarquez que la méthode test() effectue un appel à la méthode doSomething() de la superclasse. Cela entraîne une ambiguïté car le compilateur ne sait pas quelle méthode de superclasse exécuter. En raison du diagramme de classe en forme de diamant, cela est appelé le problème du diamant en Java. Le problème du diamant en Java est la principale raison pour laquelle Java ne prend pas en charge les héritages multiples dans les classes. Remarquez que le problème ci-dessus avec l’héritage de classes multiples peut également survenir avec seulement trois classes où chacune a au moins une méthode commune.

Héritage Multiple dans les Interfaces Java

Vous avez peut-être remarqué que je dis toujours que les héritages multiples ne sont pas pris en charge dans les classes, mais ils le sont dans les interfaces. Une seule interface peut étendre plusieurs interfaces, voici un exemple simple. InterfaceA.java

package com.journaldev.inheritance;

public interface InterfaceA {

	public void doSomething();
}

InterfaceB.java

package com.journaldev.inheritance;

public interface InterfaceB {

	public void doSomething();
}

Remarquez que les deux interfaces déclarent la même méthode, maintenant nous pouvons avoir une interface étendant ces deux interfaces comme ci-dessous. InterfaceC.java

package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

	//même méthode est déclarée dans InterfaceA et InterfaceB
	public void doSomething();
	
}

C’est parfaitement acceptable car les interfaces ne font que déclarer les méthodes et la mise en œuvre réelle sera réalisée par des classes concrètes implémentant les interfaces. Il n’y a donc aucune possibilité d’ambiguïté dans les héritages multiples dans les interfaces Java. C’est pourquoi une classe Java peut implémenter plusieurs interfaces, quelque chose comme l’exemple ci-dessous. 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();
		
		// tous les appels de méthode ci-dessous vont vers la même implémentation concrète
		objA.doSomething();
		objB.doSomething();
		objC.doSomething();
	}

}

Avez-vous remarqué que chaque fois que j’annule la méthode d’une superclasse ou que j’implémente une méthode d’interface, j’utilise l’annotation @Override? L’annotation Override est l’une des trois annotations Java intégrées et nous devrions toujours utiliser l’annotation override lors de l’annulation d’une méthode.

Composition à la rescousse

Alors que faire si nous voulons utiliser la fonction methodA() de ClassA et la fonction methodB() de ClassB dans ClassC? La solution réside dans l’utilisation de la composition. Voici une version refactorisée de ClassC qui utilise la composition pour utiliser les méthodes des deux classes et qui utilise également la méthode doSomething() de l’un des objets. 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();
	}
}

Composition vs Inheritance

Une des meilleures pratiques de programmation Java est de « favoriser la composition plutôt que l’héritage ». Nous examinerons certains des aspects favorisant cette approche.

  1. Supposons que nous ayons une superclasse et une sous-classe comme suit : 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;
    	}
    }
    

    Le code ci-dessus se compile et fonctionne bien, mais que se passe-t-il si l’implémentation de ClassC est modifiée comme suit : ClassC.java

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

    Remarquez que la méthode test() existe déjà dans la sous-classe mais que le type de retour est différent. Maintenant, ClassD ne se compilera pas et si vous utilisez un IDE, il vous suggérera de changer le type de retour soit dans la superclasse, soit dans la sous-classe. Imaginez maintenant la situation où nous avons plusieurs niveaux d’héritage de classes et que la superclasse n’est pas contrôlée par nous. Nous n’aurons d’autre choix que de modifier la signature de la méthode de notre sous-classe ou son nom pour supprimer l’erreur de compilation. De plus, nous devrons apporter une modification à tous les endroits où notre méthode de sous-classe était invoquée, ce qui rend notre code fragile. Le problème ci-dessus ne se produira jamais avec la composition, ce qui la rend plus favorable que l’héritage.

  2. Un autre problème avec l’héritage est que nous exposons toutes les méthodes de la superclasse au client. Si notre superclasse n’est pas correctement conçue et présente des failles de sécurité, alors même si nous prenons un soin complet dans la mise en œuvre de notre classe, nous sommes affectés par la mauvaise implémentation de la superclasse. La composition nous aide à fournir un accès contrôlé aux méthodes de la superclasse, tandis que l’héritage ne donne aucun contrôle sur les méthodes de la superclasse. C’est également l’un des avantages majeurs de la composition par rapport à l’héritage.

  3. Un autre avantage de la composition est qu’elle offre de la flexibilité dans l’invocation des méthodes. Notre implémentation ci-dessus de ClassC n’est pas optimale et fournit une liaison au moment de la compilation avec la méthode qui sera invoquée, avec un changement minimal, nous pouvons rendre l’invocation de la méthode flexible et la rendre dynamique. 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();
    	}
    }
    

    Le résultat du programme ci-dessus est:

    Implémentation de doSomething de A
    Implémentation de doSomething de B
    

    Cette flexibilité dans l’invocation des méthodes n’est pas disponible dans l’héritage et favorise la composition plutôt que l’héritage comme meilleure pratique.

  4. Les tests unitaires sont faciles en composition car nous savons quelles méthodes nous utilisons de la superclasse et nous pouvons les simuler pour les tests, tandis qu’en héritage, nous dépendons fortement de la superclasse et ne savons pas quelles méthodes de la superclasse seront utilisées. Nous devons donc tester toutes les méthodes de la superclasse, ce qui est un travail supplémentaire et que nous devons faire inutilement en raison de l’héritage.

C’est tout pour les héritages multiples en Java et un bref aperçu de la composition.

Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java