Guida all'ereditarietà in Java

1. Panoramica

Uno dei principi fondamentali della programmazione orientata agli oggetti, l' ereditarietà, ci consente di riutilizzare il codice esistente o estendere un tipo esistente.

In poche parole, in Java, una classe può ereditare un'altra classe e più interfacce, mentre un'interfaccia può ereditare altre interfacce.

In questo articolo, inizieremo con la necessità dell'ereditarietà, passando a come funziona l'ereditarietà con classi e interfacce.

Quindi, vedremo come i nomi di variabili / metodi ei modificatori di accesso influenzano i membri ereditati.

E alla fine vedremo cosa significa ereditare un tipo.

2. Il bisogno di eredità

Immagina, come produttore di automobili, di offrire più modelli di auto ai tuoi clienti. Anche se diversi modelli di auto potrebbero offrire caratteristiche diverse come un tetto apribile o finestrini antiproiettile, includerebbero tutti componenti e caratteristiche comuni, come motore e ruote.

Ha senso creare un design di base ed estenderlo per creare le loro versioni specializzate, piuttosto che progettare ogni modello di auto separatamente, da zero.

In modo simile, con l'ereditarietà, possiamo creare una classe con caratteristiche e comportamenti di base e creare le sue versioni specializzate, creando classi, che ereditano questa classe di base. Allo stesso modo, le interfacce possono estendere le interfacce esistenti.

Noteremo l'uso di più termini per fare riferimento a un tipo ereditato da un altro tipo, in particolare:

  • un tipo base è anche chiamato super o tipo genitore
  • un tipo derivato è indicato come un tipo esteso, sub o figlio

3. Eredità di classe

3.1. Estensione di una classe

Una classe può ereditare un'altra classe e definire membri aggiuntivi.

Iniziamo definendo un'auto di classe base :

public class Car { int wheels; String model; void start() { // Check essential parts } }

La classe ArmoredCar può ereditare i membri della classe Car utilizzando la parola chiave extends nella sua dichiarazione :

public class ArmoredCar extends Car { int bulletProofWindows; void remoteStartCar() { // this vehicle can be started by using a remote control } }

Possiamo ora dire che la classe ArmoredCar è una sottoclasse di Car, e quest'ultima è una superclasse di ArmoredCar.

Le classi in Java supportano l'ereditarietà singola ; la classe ArmoredCar non può estendere più classi.

Inoltre, si noti che in assenza di una parola chiave extends , una classe eredita implicitamente la classe java.lang.Object .

Una classe di sottoclasse eredita i membri protetti e pubblici non statici dalla classe della superclasse. Inoltre, i membri con accesso predefinito e al pacchetto vengono ereditati se le due classi si trovano nello stesso pacchetto.

D'altra parte, i membri privati e statici di una classe non vengono ereditati.

3.2. Accesso ai membri principali da una classe figlio

Per accedere a proprietà o metodi ereditati, possiamo semplicemente usarli direttamente:

public class ArmoredCar extends Car { public String registerModel() { return model; } }

Nota che non abbiamo bisogno di un riferimento alla superclasse per accedere ai suoi membri.

4. Ereditarietà dell'interfaccia

4.1. Implementazione di più interfacce

Sebbene le classi possano ereditare solo una classe, possono implementare più interfacce.

Immagina che l' ArmoredCar che abbiamo definito nella sezione precedente sia necessaria per una super spia. Quindi la società di produzione di automobili ha pensato di aggiungere funzionalità di volo e galleggiamento:

public interface Floatable { void floatOnWater(); }
public interface Flyable { void fly(); }
public class ArmoredCar extends Car implements Floatable, Flyable{ public void floatOnWater() { System.out.println("I can float!"); } public void fly() { System.out.println("I can fly!"); } }

Nell'esempio sopra, abbiamo notato l'uso della parola chiave implements per ereditare da un'interfaccia.

4.2. Problemi con ereditarietà multipla

Java consente l'ereditarietà multipla utilizzando le interfacce.

Fino a Java 7, questo non era un problema. Le interfacce potevano definire solo metodi astratti , cioè metodi senza alcuna implementazione. Quindi, se una classe implementava più interfacce con la stessa firma del metodo, non era un problema. La classe di implementazione alla fine aveva un solo metodo da implementare.

Vediamo come è cambiata questa semplice equazione con l'introduzione di metodi predefiniti nelle interfacce, con Java 8.

A partire da Java 8, le interfacce possono scegliere di definire implementazioni predefinite per i suoi metodi (un'interfaccia può ancora definire metodi astratti ). Ciò significa che se una classe implementa più interfacce, che definiscono metodi con la stessa firma, la classe figlia erediterà implementazioni separate. Sembra complesso e non è consentito.

Java non consente l'ereditarietà di più implementazioni degli stessi metodi, definiti in interfacce separate.

Ecco un esempio:

public interface Floatable { default void repair() { System.out.println("Repairing Floatable object"); } }
public interface Flyable { default void repair() { System.out.println("Repairing Flyable object"); } }
public class ArmoredCar extends Car implements Floatable, Flyable { // this won't compile }

Se vogliamo implementare entrambe le interfacce, dovremo sovrascrivere il metodo repair () .

Se le interfacce negli esempi precedenti definiscono variabili con lo stesso nome, ad esempio durata , non possiamo accedervi senza far precedere il nome della variabile dal nome dell'interfaccia:

public interface Floatable { int duration = 10; }
public interface Flyable { int duration = 20; }
public class ArmoredCar extends Car implements Floatable, Flyable { public void aMethod() { System.out.println(duration); // won't compile System.out.println(Floatable.duration); // outputs 10 System.out.println(Flyable.duration); // outputs 20 } }

4.3. Interfaces Extending Other Interfaces

An interface can extend multiple interfaces. Here's an example:

public interface Floatable { void floatOnWater(); }
interface interface Flyable { void fly(); }
public interface SpaceTraveller extends Floatable, Flyable { void remoteControl(); }

An interface inherits other interfaces by using the keyword extends. Classes use the keyword implements to inherit an interface.

5. Inheriting Type

When a class inherits another class or interfaces, apart from inheriting their members, it also inherits their type. This also applies to an interface that inherits other interfaces.

This is a very powerful concept, which allows developers to program to an interface (base class or interface), rather than programming to their implementations.

For example, imagine a condition, where an organization maintains a list of the cars owned by its employees. Of course, all employees might own different car models. So how can we refer to different car instances? Here's the solution:

public class Employee { private String name; private Car car; // standard constructor }

Because all derived classes of Car inherit the type Car, the derived class instances can be referred by using a variable of class Car:

Employee e1 = new Employee("Shreya", new ArmoredCar()); Employee e2 = new Employee("Paul", new SpaceCar()); Employee e3 = new Employee("Pavni", new BMW());

6. Hidden Class Members

6.1. Hidden Instance Members

What happens if both the superclass and subclass define a variable or method with the same name? Don't worry; we can still access both of them. However, we must make our intent clear to Java, by prefixing the variable or method with the keywords this or super.

The this keyword refers to the instance in which it's used. The super keyword (as it seems obvious) refers to the parent class instance:

public class ArmoredCar extends Car { private String model; public String getAValue() { return super.model; // returns value of model defined in base class Car // return this.model; // will return value of model defined in ArmoredCar // return model; // will return value of model defined in ArmoredCar } }

A lot of developers use this and super keywords to explicitly state which variable or method they're referring to. However, using them with all members can make our code look cluttered.

6.2. Hidden Static Members

What happens when our base class and subclasses define static variables and methods with the same name? Can we access a static member from the base class, in the derived class, the way we do for the instance variables?

Let's find out using an example:

public class Car { public static String msg() { return "Car"; } }
public class ArmoredCar extends Car { public static String msg() { return super.msg(); // this won't compile. } }

No, we can't. The static members belong to a class and not to instances. So we can't use the non-static super keyword in msg().

Since static members belong to a class, we can modify the preceding call as follows:

return Car.msg();

Consider the following example, in which both the base class and derived class define a static method msg() with the same signature:

public class Car { public static String msg() { return "Car"; } }
public class ArmoredCar extends Car { public static String msg() { return "ArmoredCar"; } }

Here's how we can call them:

Car first = new ArmoredCar(); ArmoredCar second = new ArmoredCar();

For the preceding code, first.msg() will output “Car and second.msg() will output “ArmoredCar”. The static message that is called depends on the type of the variable used to refer to ArmoredCar instance.

7. Conclusion

In questo articolo, abbiamo trattato un aspetto fondamentale del linguaggio Java: l'ereditarietà.

Abbiamo visto come Java supporta l'ereditarietà singola con classi e l'ereditarietà multipla con le interfacce e abbiamo discusso le complessità di come funziona il meccanismo nel linguaggio.

Come sempre, il codice sorgente completo per gli esempi è disponibile su GitHub.