Eredità contro composizione: come scegliere

Ereditarietà e composizione sono due tecniche di programmazione utilizzate dagli sviluppatori per stabilire relazioni tra classi e oggetti. Mentre l'ereditarietà deriva una classe da un'altra, la composizione definisce una classe come la somma delle sue parti.

Le classi e gli oggetti creati tramite l'ereditarietà sono strettamente collegati perché la modifica del genitore o della superclasse in una relazione di ereditarietà rischia di rompere il codice. Le classi e gli oggetti creati attraverso la composizione sono liberamente accoppiati , il che significa che puoi cambiare più facilmente le parti componenti senza rompere il codice.

Poiché il codice liberamente accoppiato offre maggiore flessibilità, molti sviluppatori hanno imparato che la composizione è una tecnica migliore dell'ereditarietà, ma la verità è più complessa. La scelta di uno strumento di programmazione è simile alla scelta dello strumento da cucina corretto: non useresti un coltello da burro per tagliare le verdure e allo stesso modo non dovresti scegliere la composizione per ogni scenario di programmazione. 

In questo Java Challenger imparerai la differenza tra ereditarietà e composizione e come decidere quale è corretto per il tuo programma. Successivamente, ti presenterò diversi aspetti importanti ma impegnativi dell'ereditarietà di Java: sostituzione del metodo, superparola chiave e casting del tipo. Infine, testerai ciò che hai imparato lavorando su un esempio di ereditarietà riga per riga per determinare quale dovrebbe essere l'output.

Quando utilizzare l'ereditarietà in Java

Nella programmazione orientata agli oggetti, possiamo usare l'ereditarietà quando sappiamo che esiste una relazione "è un" tra un figlio e la sua classe genitore. Alcuni esempi potrebbero essere:

  • Una persona è un essere umano.
  • Un gatto è un animale.
  • Una macchina è un   veicolo.

In ogni caso, il figlio o la sottoclasse è una versione specializzata del genitore o della superclasse. Ereditare dalla superclasse è un esempio di riutilizzo del codice. Per comprendere meglio questa relazione, prenditi un momento per studiare la Carclasse, che eredita da 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(); } } 

Quando stai pensando di usare l'ereditarietà, chiediti se la sottoclasse è davvero una versione più specializzata della superclasse. In questo caso, un'auto è un tipo di veicolo, quindi la relazione di eredità ha senso. 

Quando utilizzare la composizione in Java

Nella programmazione orientata agli oggetti, possiamo usare la composizione nei casi in cui un oggetto "ha" (o fa parte di) un altro oggetto. Alcuni esempi potrebbero essere:

  • Un'auto ha una batteria (una batteria fa parte di un'auto).
  • Una persona ha un cuore (un cuore fa parte di una persona).
  • Una casa ha un soggiorno (un soggiorno fa parte di una casa).

Per comprendere meglio questo tipo di relazione, considera la composizione di 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 { } } 

In questo caso sappiamo che una casa ha un soggiorno e una camera da letto, quindi possiamo utilizzare gli oggetti BedroomLivingRoomnella composizione di un file House

Ottieni il codice

Ottieni il codice sorgente per esempi in questo Java Challenger. Puoi eseguire i tuoi test mentre segui gli esempi.

Ereditarietà vs composizione: due esempi

Considera il codice seguente. È un buon esempio di eredità?

 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); } 

In questo caso la risposta è no. La classe figlia eredita molti metodi che non utilizzerà mai, risultando in un codice strettamente accoppiato che è sia confuso che difficile da mantenere. Se guardi da vicino, è anche chiaro che questo codice non supera il test "è un".

Ora proviamo lo stesso esempio usando la composizione:

 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'uso della composizione per questo scenario consente alla  CharacterCompositionExampleclasse di utilizzare solo due dei HashSetmetodi di, senza ereditarli tutti. Ciò si traduce in un codice più semplice e meno accoppiato che sarà più facile da capire e mantenere.

Esempi di ereditarietà nel JDK

Il Java Development Kit è pieno di buoni esempi di ereditarietà:

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

Si noti che in ciascuno di questi esempi, la classe figlia è una versione specializzata del suo genitore; ad esempio, IndexOutOfBoundsExceptionè un tipo di RuntimeException.

Metodo che ignora l'ereditarietà Java

L'ereditarietà ci consente di riutilizzare i metodi e altri attributi di una classe in una nuova classe, il che è molto conveniente. Ma affinché l'ereditarietà funzioni davvero, dobbiamo anche essere in grado di modificare alcuni dei comportamenti ereditati all'interno della nostra nuova sottoclasse. Ad esempio, potremmo voler specializzare il suono che Catfa:

 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(); } } 

Questo è un esempio di ereditarietà Java con sostituzione del metodo. Innanzitutto, estendiamo la Animalclasse per creare una nuova Catclasse. Successivamente, sovrascriviamo il metodo Animaldella classe emitSound()per ottenere il suono specifico che Catproduce. Anche se abbiamo dichiarato il tipo di classe come Animal, quando lo istanziamo, Catotterremo il miagolio del gatto. 

Il metodo prioritario è il polimorfismo

Potresti ricordare dal mio ultimo post che l'override del metodo è un esempio di polimorfismo o invocazione di metodi virtuali.

Java ha ereditarietà multipla?

A differenza di alcuni linguaggi, come C ++, Java non consente l'ereditarietà multipla con le classi. È tuttavia possibile utilizzare l'ereditarietà multipla con le interfacce. La differenza tra una classe e un'interfaccia, in questo caso, è che le interfacce non mantengono lo stato.

Se provi l'ereditarietà multipla come ho fatto di seguito, il codice non verrà compilato:

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

Una soluzione che utilizza le classi sarebbe quella di ereditare una per una:

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

Un'altra soluzione è sostituire le classi con interfacce:

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

Usare "super" per accedere ai metodi delle classi genitore

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

È possibile dichiarare a Dogcon il supertipo Animal, ma se vogliamo invocare un metodo specifico da Dog, avremo bisogno di eseguirne il cast. Ad esempio, e se volessimo richiamare il bark()metodo? Il Animalsupertipo non ha modo di sapere esattamente quale istanza animale stiamo invocando, quindi dobbiamo eseguire il cast Dogmanualmente prima di poter invocare il bark()metodo:

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

Puoi anche usare il casting senza assegnare l'oggetto a un tipo di classe. Questo approccio è utile quando non vuoi dichiarare un'altra variabile:

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

Accetta la sfida dell'ereditarietà di Java!

Hai imparato alcuni importanti concetti di eredità, quindi ora è il momento di provare una sfida di ereditarietà. Per iniziare, studia il seguente codice: