Héritage versus composition: comment choisir

L'héritage et la composition sont deux techniques de programmation utilisées par les développeurs pour établir des relations entre les classes et les objets. Alors que l'héritage dérive une classe d'une autre, la composition définit une classe comme la somme de ses parties.

Les classes et les objets créés par héritage sont étroitement couplés car la modification du parent ou de la superclasse dans une relation d'héritage risque de casser votre code. Les classes et les objets créés par composition sont faiblement couplés , ce qui signifie que vous pouvez plus facilement modifier les composants sans casser votre code.

Comme le code faiblement couplé offre plus de flexibilité, de nombreux développeurs ont appris que la composition est une meilleure technique que l'héritage, mais la vérité est plus complexe. Le choix d'un outil de programmation est similaire au choix du bon outil de cuisine: vous n'utiliserez pas de couteau à beurre pour couper des légumes, et de la même manière, vous ne devriez pas choisir la composition pour chaque scénario de programmation. 

Dans ce Java Challenger, vous apprendrez la différence entre l'héritage et la composition et comment décider lequel est correct pour votre programme. Ensuite, je vais vous présenter plusieurs aspects importants mais difficiles de l'héritage Java: le remplacement de méthode, le supermot - clé et la conversion de type. Enfin, vous testerez ce que vous avez appris en utilisant un exemple d'héritage ligne par ligne pour déterminer ce que devrait être la sortie.

Quand utiliser l'héritage en Java

Dans la programmation orientée objet, nous pouvons utiliser l'héritage lorsque nous savons qu'il existe une relation «est une» entre un enfant et sa classe parente. Quelques exemples seraient:

  • Une personne est un humain.
  • Un chat est un animal.
  • Une voiture est un   véhicule.

Dans chaque cas, l'enfant ou la sous-classe est une version spécialisée du parent ou de la superclasse. L'héritage de la superclasse est un exemple de réutilisation de code. Pour mieux comprendre cette relation, prenez un moment pour étudier la Carclasse, qui hérite de Vehicle:

 class Vehicle { String brand; String color; double weight; double speed; void move() { System.out.println("The vehicle is moving"); } } public class Car extends Vehicle { String licensePlateNumber; String owner; String bodyStyle; public static void main(String... inheritanceExample) { System.out.println(new Vehicle().brand); System.out.println(new Car().brand); new Car().move(); } } 

Lorsque vous envisagez d'utiliser l'héritage, demandez-vous si la sous-classe est vraiment une version plus spécialisée de la superclasse. Dans ce cas, une voiture est un type de véhicule, donc la relation d'héritage a du sens. 

Quand utiliser la composition en Java

En programmation orientée objet, nous pouvons utiliser la composition dans les cas où un objet "a" (ou fait partie) d'un autre objet. Quelques exemples seraient:

  • Une voiture a une batterie (une batterie fait partie d' une voiture).
  • Une personne a un cœur (un cœur fait partie d' une personne).
  • Une maison dispose d'un salon (un salon fait partie d' une maison).

Pour mieux comprendre ce type de relation, considérez la composition d'un House:

 public class CompositionExample { public static void main(String... houseComposition) { new House(new Bedroom(), new LivingRoom()); // The house now is composed with a Bedroom and a LivingRoom } static class House { Bedroom bedroom; LivingRoom livingRoom; House(Bedroom bedroom, LivingRoom livingRoom) { this.bedroom = bedroom; this.livingRoom = livingRoom; } } static class Bedroom { } static class LivingRoom { } } 

Dans ce cas, on sait qu'une maison a un salon et une chambre, on peut donc utiliser les objets Bedroomet  LivingRoomdans la composition d'un House

Obtenez le code

Obtenez le code source pour des exemples dans ce Java Challenger. Vous pouvez exécuter vos propres tests en suivant les exemples.

Héritage vs composition: deux exemples

Considérez le code suivant. Est-ce un bon exemple d'héritage?

 import java.util.HashSet; public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); } 

Dans ce cas, la réponse est non. La classe enfant hérite de nombreuses méthodes qu'elle n'utilisera jamais, résultant en un code étroitement couplé qui est à la fois déroutant et difficile à maintenir. Si vous regardez de près, il est également clair que ce code ne passe pas le test «est un».

Essayons maintenant le même exemple en utilisant la composition:

 import java.util.HashSet; import java.util.Set; public class CharacterCompositionExample { static Set set = new HashSet(); public static void main(String... goodExampleOfComposition) { set.add("Homer"); set.forEach(System.out::println); } 

L'utilisation de la composition pour ce scénario permet à la  CharacterCompositionExampleclasse d'utiliser uniquement deux des HashSetméthodes de, sans les hériter toutes. Il en résulte un code plus simple, moins couplé qui sera plus facile à comprendre et à maintenir.

Exemples d'héritage dans le JDK

Le kit de développement Java regorge de bons exemples d'héritage:

 class IndexOutOfBoundsException extends RuntimeException {...} class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...} class FileWriter extends OutputStreamWriter {...} class OutputStreamWriter extends Writer {...} interface Stream extends BaseStream
    
      {...} 
    

Notez que dans chacun de ces exemples, la classe enfant est une version spécialisée de son parent; par exemple, IndexOutOfBoundsExceptionest un type de RuntimeException.

Remplacement de méthode avec l'héritage Java

L'héritage nous permet de réutiliser les méthodes et autres attributs d'une classe dans une nouvelle classe, ce qui est très pratique. Mais pour que l'héritage fonctionne vraiment, nous devons également être en mesure de modifier certains des comportements hérités dans notre nouvelle sous-classe. Par exemple, nous pourrions vouloir spécialiser le son Catproduit:

 class Animal { void emitSound() { System.out.println("The animal emitted a sound"); } } class Cat extends Animal { @Override void emitSound() { System.out.println("Meow"); } } class Dog extends Animal { } public class Main { public static void main(String... doYourBest) { Animal cat = new Cat(); // Meow Animal dog = new Dog(); // The animal emitted a sound Animal animal = new Animal(); // The animal emitted a sound cat.emitSound(); dog.emitSound(); animal.emitSound(); } } 

Voici un exemple d'héritage Java avec remplacement de méthode. Tout d'abord, nous étendons la Animalclasse pour créer une nouvelle Catclasse. Ensuite, nous remplaçons la méthode de la Animalclasse emitSound()pour obtenir le son spécifique Catproduit. Même si nous avons déclaré le type de classe comme Animal, lorsque nous l'instancions, Catnous obtiendrons le miaulement du chat. 

La substitution de méthode est un polymorphisme

Vous vous souvenez peut-être de mon dernier article que la substitution de méthode est un exemple de polymorphisme ou d'invocation de méthode virtuelle.

Java a-t-il un héritage multiple?

Contrairement à certains langages, tels que C ++, Java n'autorise pas l'héritage multiple avec des classes. Vous pouvez cependant utiliser l'héritage multiple avec les interfaces. La différence entre une classe et une interface, dans ce cas, est que les interfaces ne conservent pas l'état.

Si vous essayez d'héritage multiple comme je l'ai ci-dessous, le code ne se compilera pas:

 class Animal {} class Mammal {} class Dog extends Animal, Mammal {} 

Une solution utilisant des classes serait d'hériter une par une:

 class Animal {} class Mammal extends Animal {} class Dog extends Mammal {} 

Une autre solution consiste à remplacer les classes par des interfaces:

 interface Animal {} interface Mammal {} class Dog implements Animal, Mammal {} 

Utilisation de 'super' pour accéder aux méthodes des classes parentes

When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super to ensure the child class can still access its parent's overridden method:

 public class SuperWordExample { class Character { Character() { System.out.println("A Character has been created"); } void move() { System.out.println("Character walking..."); } } class Moe extends Character { Moe() { super(); } void giveBeer() { super.move(); System.out.println("Give beer"); } } } 

In this example, Character is the parent class for Moe.  Using super, we are able to access Character's  move() method in order to give Moe a beer.

Using constructors with inheritance

When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super will be added automatically to the constructor.  However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super constructor, as shown below:

 public class ConstructorSuper { class Character { Character() { System.out.println("The super constructor was invoked"); } } class Barney extends Character { // No need to declare the constructor or to invoke the super constructor // The JVM will to that } } 

If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super to explicitly invoke the parent constructor. The super reserved word won't be added automatically and the code won't compile without it.  For example:

 public class CustomizedConstructorSuper { class Character { Character(String name) { System.out.println(name + "was invoked"); } } class Barney extends Character { // We will have compilation error if we don't invoke the constructor explicitly // We need to add it Barney() { super("Barney Gumble"); } } } 

Type casting and the ClassCastException

Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type.  It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException.

In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.

Consider the following example:

 public class CastingExample { public static void main(String... castingExample) { Animal animal = new Animal(); Dog dogAnimal = (Dog) animal; // We will get ClassCastException Dog dog = new Dog(); Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); Animal anotherDog = dog; // It's fine here, no need for casting System.out.println(((Dog)anotherDog)); // This is another way to cast the object } } class Animal { } class Dog extends Animal { void bark() { System.out.println("Au au"); } } 

When we try to cast an Animal instance to a Dog we get an exception. This is because the Animal doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal. 

The problem in this case is that we've instantiated Animal like this:

 Animal animal = new Animal(); 

Then tried to cast it like this:

 Dog dogAnimal = (Dog) animal; 

Because we don't have a Dog instance, it's impossible to assign an Animal to the Dog.  If we try, we will get a ClassCastException

In order to avoid the exception, we should instantiate the Dog like this:

 Dog dog = new Dog(); 

then assign it to Animal:

 Animal anotherDog = dog; 

In this case, because  we've extended the Animal class, the Dog instance doesn't even need to be cast; the Animal parent class type simply accepts the assignment.

Casting with supertypes

Il est possible de déclarer a Dogavec le supertype Animal, mais si nous voulons invoquer une méthode spécifique à partir de Dog, nous devrons la convertir. A titre d'exemple, que faire si nous voulions invoquer la bark()méthode? Le Animalsupertype n'a aucun moyen de savoir exactement quelle instance d'animal nous invoquons, nous devons donc effectuer un cast Dogmanuellement avant de pouvoir invoquer la bark()méthode:

 Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); 

Vous pouvez également utiliser la conversion sans affecter l'objet à un type de classe. Cette approche est pratique lorsque vous ne souhaitez pas déclarer une autre variable:

 System.out.println(((Dog)anotherDog)); // This is another way to cast the object 

Relevez le défi de l'héritage Java!

Vous avez appris quelques concepts importants de l'héritage, alors il est maintenant temps d'essayer un défi d'héritage. Pour commencer, étudiez le code suivant: