Ereditarietà in Java, parte 1: la parola chiave extends

Java supporta il riutilizzo delle classi attraverso l'ereditarietà e la composizione. Questo tutorial in due parti ti insegna come usare l'ereditarietà nei tuoi programmi Java. Nella parte 1 imparerai come usare la extendsparola chiave per derivare una classe figlia da una classe genitore, invocare costruttori e metodi della classe genitore e sovrascrivere metodi. Nella parte 2 visiterai java.lang.Object, che è la superclasse di Java da cui eredita ogni altra classe.

Per completare il tuo apprendimento sull'ereditarietà, assicurati di controllare il mio suggerimento su Java che spiega quando usare la composizione rispetto all'ereditarietà. Imparerai perché la composizione è un importante complemento all'ereditarietà e come usarla per proteggerti dai problemi di incapsulamento nei tuoi programmi Java.

download Ottieni il codice Scarica il codice sorgente per applicazioni di esempio in questo tutorial. Creato da Jeff Friesen per JavaWorld.

Ereditarietà Java: due esempi

L'ereditarietà è un costrutto di programmazione che gli sviluppatori di software utilizzano per stabilire relazioni is-a tra categorie. L'ereditarietà ci consente di derivare categorie più specifiche da categorie più generiche. La categoria più specifica è una sorta di categoria più generica. Ad esempio, un conto corrente è un tipo di conto in cui è possibile effettuare depositi e prelievi. Allo stesso modo, un camion è un tipo di veicolo utilizzato per il trasporto di oggetti di grandi dimensioni.

L'ereditarietà può scendere attraverso più livelli, portando a categorie sempre più specifiche. A titolo di esempio, la figura 1 mostra auto e camion che ereditano dal veicolo; station wagon ereditata dall'auto; e camion della spazzatura che eredita dal camion. Le frecce puntano da categorie "figlio" più specifiche (più in basso) a categorie "padre" meno specifiche (più in alto).

Jeff Friesen

Questo esempio illustra l' ereditarietà singola in cui una categoria figlio eredita stato e comportamenti da una categoria padre immediata. Al contrario, l'ereditarietà multipla consente a una categoria figlio di ereditare stato e comportamenti da due o più categorie padre immediate. La gerarchia nella Figura 2 illustra l'ereditarietà multipla.

Jeff Friesen

Le categorie sono descritte per classi. Java supporta l'ereditarietà singola tramite l' estensione di classe , in cui una classe eredita direttamente campi e metodi accessibili da un'altra classe estendendo quella classe. Tuttavia, Java non supporta l'ereditarietà multipla tramite l'estensione di classe.

Quando si visualizza una gerarchia di ereditarietà, è possibile rilevare facilmente l'ereditarietà multipla dalla presenza di un motivo a rombi. La Figura 2 mostra questo modello nel contesto di veicoli, veicoli terrestri, veicoli acquatici e hovercraft.

La parola chiave extends

Java supporta l'estensione della classe tramite la extendsparola chiave. Quando presente, extendsspecifica una relazione genitore-figlio tra due classi. Di seguito utilizzo extendsper stabilire una relazione tra le classi Vehiclee Car, quindi tra Accounte SavingsAccount:

Listato 1. La extendsparola chiave specifica una relazione genitore-figlio

class Vehicle { // member declarations } class Car extends Vehicle { // inherit accessible members from Vehicle // provide own member declarations } class Account { // member declarations } class SavingsAccount extends Account { // inherit accessible members from Account // provide own member declarations }

La extendsparola chiave viene specificata dopo il nome della classe e prima del nome di un altro classe. Il nome della classe prima extendsidentifica il bambino e il nome della classe dopo extendsidentifica il genitore. È impossibile specificare più nomi di classe dopo extendsperché Java non supporta l'ereditarietà multipla basata su classi.

Questi esempi codificano le relazioni is-a: Carè un specializzato Vehicleed SavingsAccountè uno specializzato Account. Vehiclee Accountsono note come classi base , classi genitore o superclassi . Care SavingsAccountsono note come classi derivate , classi figlie o sottoclassi .

Classi finali

Potreste dichiarare una classe che non dovrebbe essere estesa; ad esempio per motivi di sicurezza. In Java, utilizziamo la finalparola chiave per impedire che alcune classi vengano estese. Basta anteporre a un'intestazione di classe final, come in final class Password. Data questa dichiarazione, il compilatore segnalerà un errore se qualcuno tenta di estendere Password.

Le classi figlie ereditano campi e metodi accessibili dalle loro classi padre e altri antenati. Tuttavia, non ereditano mai i costruttori. Invece, le classi figlie dichiarano i propri costruttori. Inoltre, possono dichiarare i propri campi e metodi per differenziarli dai loro genitori. Considera il listato 2.

Listato 2. Una Accountclasse genitore

class Account { private String name; private long amount; Account(String name, long amount) { this.name = name; setAmount(amount); } void deposit(long amount) { this.amount += amount; } String getName() { return name; } long getAmount() { return amount; } void setAmount(long amount) { this.amount = amount; } }

Il Listato 2 descrive una classe di conto bancario generico che ha un nome e un importo iniziale, entrambi impostati nel costruttore. Inoltre, consente agli utenti di effettuare depositi. (Puoi effettuare prelievi depositando importi negativi di denaro, ma ignoreremo questa possibilità.) Nota che il nome dell'account deve essere impostato quando viene creato un account.

Rappresentare i valori di valuta

conteggio dei penny. Potresti preferire utilizzare a doubleo a floatper memorizzare valori monetari, ma ciò può portare a imprecisioni. Per una soluzione migliore, considera BigDecimal, che fa parte della libreria di classi standard di Java.

Il Listato 3 presenta una SavingsAccountclasse figlia che estende la sua Accountclasse genitore.

Listato 3. Una SavingsAccountclasse figlia estende la sua Accountclasse genitore

class SavingsAccount extends Account { SavingsAccount(long amount) { super("savings", amount); } }

La SavingsAccountclasse è banale perché non ha bisogno di dichiarare campi o metodi aggiuntivi. Tuttavia, dichiara un costruttore che inizializza i campi nella sua Accountsuperclasse. L'inizializzazione avviene quando Accountil costruttore di viene chiamato tramite la superparola chiave Java , seguita da un elenco di argomenti tra parentesi.

Quando e dove chiamare super ()

Just as this() must be the first element in a constructor that calls another constructor in the same class, super() must be the first element in a constructor that calls a constructor in its superclass. If you break this rule the compiler will report an error. The compiler will also report an error if it detects a super() call in a method; only ever call super() in a constructor.

Listing 4 further extends Account with a CheckingAccount class.

Listing 4. A CheckingAccount child class extends its Account parent class

class CheckingAccount extends Account { CheckingAccount(long amount) { super("checking", amount); } void withdraw(long amount) { setAmount(getAmount() - amount); } }

CheckingAccount is a little more substantial than SavingsAccount because it declares a withdraw() method. Notice this method's calls to setAmount() and getAmount(), which CheckingAccount inherits from Account. You cannot directly access the amount field in Account because this field is declared private (see Listing 2).

super() and the no-argument constructor

If super() is not specified in a subclass constructor, and if the superclass doesn't declare a no-argument constructor, then the compiler will report an error. This is because the subclass constructor must call a no-argument superclass constructor when super() isn't present.

Class hierarchy example

I've created an AccountDemo application class that lets you try out the Account class hierarchy. First take a look at AccountDemo's source code.

Listing 5. AccountDemo demonstrates the account class hierarchy

class AccountDemo { public static void main(String[] args) { SavingsAccount sa = new SavingsAccount(10000); System.out.println("account name: " + sa.getName()); System.out.println("initial amount: " + sa.getAmount()); sa.deposit(5000); System.out.println("new amount after deposit: " + sa.getAmount()); CheckingAccount ca = new CheckingAccount(20000); System.out.println("account name: " + ca.getName()); System.out.println("initial amount: " + ca.getAmount()); ca.deposit(6000); System.out.println("new amount after deposit: " + ca.getAmount()); ca.withdraw(3000); System.out.println("new amount after withdrawal: " + ca.getAmount()); } }

The main() method in Listing 5 first demonstrates SavingsAccount, then CheckingAccount. Assuming Account.java, SavingsAccount.java, CheckingAccount.java, and AccountDemo.java source files are in the same directory, execute either of the following commands to compile all of these source files:

javac AccountDemo.java javac *.java

Execute the following command to run the application:

java AccountDemo

You should observe the following output:

account name: savings initial amount: 10000 new amount after deposit: 15000 account name: checking initial amount: 20000 new amount after deposit: 26000 new amount after withdrawal: 23000

Method overriding (and method overloading)

A subclass can override (replace) an inherited method so that the subclass's version of the method is called instead. An overriding method must specify the same name, parameter list, and return type as the method being overridden. To demonstrate, I've declared a print() method in the Vehicle class below.

Listing 6. Declaring a print() method to be overridden

class Vehicle { private String make; private String model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } void print() { System.out.println("Make: " + make + ", Model: " + model + ", Year: " + year); } }

Next, I override print() in the Truck class.

Listing 7. Overriding print() in a Truck subclass

class Truck extends Vehicle { private double tonnage; Truck(String make, String model, int year, double tonnage) { super(make, model, year); this.tonnage = tonnage; } double getTonnage() { return tonnage; } void print() { super.print(); System.out.println("Tonnage: " + tonnage); } }

Truck's print() method has the same name, return type, and parameter list as Vehicle's print() method. Note, too, that Truck's print() method first calls Vehicle's print() method by prefixing super. to the method name. It's often a good idea to execute the superclass logic first and then execute the subclass logic.

Calling superclass methods from subclass methods

In order to call a superclass method from the overriding subclass method, prefix the method's name with the reserved word super and the member access operator. Otherwise you will end up recursively calling the subclass's overriding method. In some cases a subclass will mask non-private superclass fields by declaring same-named fields. You can use super and the member access operator to access the non-private superclass fields.

To complete this example, I've excerpted a VehicleDemo class's main() method:

Truck truck = new Truck("Ford", "F150", 2008, 0.5); System.out.println("Make = " + truck.getMake()); System.out.println("Model = " + truck.getModel()); System.out.println("Year = " + truck.getYear()); System.out.println("Tonnage = " + truck.getTonnage()); truck.print();

The final line, truck.print();, calls truck's print() method. This method first calls Vehicle's print() to output the truck's make, model, and year; then it outputs the truck's tonnage. This portion of the output is shown below:

Make: Ford, Model: F150, Year: 2008 Tonnage: 0.5

Use final to block method overriding

Occasionally you might need to declare a method that should not be overridden, for security or another reason. You can use the final keyword for this purpose. To prevent overriding, simply prefix a method header with final, as in final String getMake(). The compiler will then report an error if anyone attempts to override this method in a subclass.

Method overloading vs overriding

Suppose you replaced the print() method in Listing 7 with the one below:

void print(String owner) { System.out.print("Owner: " + owner); super.print(); }

The modified Truck class now has two print() methods: the preceding explicitly-declared method and the method inherited from Vehicle. The void print(String owner) method doesn't override Vehicle's print() method. Instead, it overloads it.

È possibile rilevare un tentativo di sovraccarico invece di sovrascrivere un metodo in fase di compilazione prefissando l'intestazione del metodo di una sottoclasse con l' @Overrideannotazione:

@Override void print(String owner) { System.out.print("Owner: " + owner); super.print(); }

La specifica @Overrideindica al compilatore che il metodo dato sovrascrive un altro metodo. Se invece qualcuno tentasse di sovraccaricare il metodo, il compilatore segnalava un errore. Senza questa annotazione, il compilatore non segnalerebbe un errore perché il sovraccarico del metodo è legale.

Quando usare @Override

Sviluppa l'abitudine di anteporre ai metodi di override @Override. Questa abitudine ti aiuterà a rilevare gli errori di sovraccarico molto prima.