Overload e override del metodo in Java

1. Panoramica

Il sovraccarico e l'override dei metodi sono concetti chiave del linguaggio di programmazione Java e, come tali, meritano uno sguardo approfondito.

In questo articolo impareremo le basi di questi concetti e vedremo in quali situazioni possono essere utili.

2. Metodo di sovraccarico

Il sovraccarico dei metodi è un meccanismo potente che ci consente di definire API di classi coesive. Per capire meglio perché il sovraccarico del metodo è una funzionalità così preziosa, vediamo un semplice esempio.

Supponiamo di aver scritto una classe di utilità ingenua che implementa metodi diversi per moltiplicare due numeri, tre numeri e così via.

Se abbiamo dato ai metodi nomi fuorvianti o ambigui, come multiply2 () , multiply3 () , multiply4 (), allora sarebbe un'API di classe mal progettata. È qui che entra in gioco il sovraccarico del metodo.

In poche parole, possiamo implementare il sovraccarico del metodo in due modi diversi:

  • implementando due o più metodi che hanno lo stesso nome ma accettano un numero diverso di argomenti
  • implementando due o più metodi che hanno lo stesso nome ma accettano argomenti di tipo diverso

2.1. Diversi numeri di argomenti

La classe Multiplier mostra, in poche parole, come sovraccaricare il metodo multiply () definendo semplicemente due implementazioni che richiedono un numero diverso di argomenti:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } }

2.2. Argomenti di diverso tipo

Allo stesso modo, possiamo sovraccaricare il metodo multiply () facendolo accettare argomenti di diversi tipi:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public double multiply(double a, double b) { return a * b; } } 

Inoltre, è legittimo definire la classe Multiplier con entrambi i tipi di sovraccarico del metodo:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } public double multiply(double a, double b) { return a * b; } } 

Vale la pena notare, tuttavia, che non è possibile avere due implementazioni di metodo che differiscono solo per i tipi restituiti .

Per capire perché, consideriamo il seguente esempio:

public int multiply(int a, int b) { return a * b; } public double multiply(int a, int b) { return a * b; }

In questo caso, il codice semplicemente non si compilerebbe a causa dell'ambiguità della chiamata al metodo : il compilatore non saprebbe quale implementazione di multiply () chiamare.

2.3. Tipo Promozione

Una caratteristica interessante fornita dal sovraccarico del metodo è la cosiddetta promozione del tipo, ovvero la conversione primitiva allargata .

In termini semplici, un dato tipo viene promosso implicitamente a un altro quando non c'è corrispondenza tra i tipi degli argomenti passati al metodo sovraccarico e una specifica implementazione del metodo.

Per capire più chiaramente come funziona la promozione del tipo, considera le seguenti implementazioni del metodo multiply () :

public double multiply(int a, long b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } 

Ora, la chiamata del metodo con due argomenti int comporterà la promozione del secondo argomento a long , poiché in questo caso non esiste un'implementazione corrispondente del metodo con due argomenti int .

Vediamo un rapido unit test per dimostrare la promozione del tipo:

@Test public void whenCalledMultiplyAndNoMatching_thenTypePromotion() { assertThat(multiplier.multiply(10, 10)).isEqualTo(100.0); }

Al contrario, se chiamiamo il metodo con un'implementazione corrispondente, la promozione del tipo semplicemente non ha luogo:

@Test public void whenCalledMultiplyAndMatching_thenNoTypePromotion() { assertThat(multiplier.multiply(10, 10, 10)).isEqualTo(1000); }

Ecco un riepilogo delle regole di promozione del tipo che si applicano per il sovraccarico del metodo:

  • byte può essere promosso a short, int, long, float o double
  • short può essere promosso a int, long, float o double
  • char può essere promosso a int, long, float o double
  • int può essere promosso a long, float o double
  • long può essere promosso a float o double
  • float può essere promosso a double

2.4. Legame statico

La capacità di associare una specifica chiamata al metodo al corpo del metodo è nota come associazione.

Nel caso del sovraccarico del metodo, l'associazione viene eseguita staticamente in fase di compilazione, quindi viene chiamata associazione statica.

Il compilatore può impostare efficacemente l'associazione in fase di compilazione semplicemente controllando le firme dei metodi.

3. Sostituzione del metodo

L'override del metodo ci consente di fornire implementazioni a grana fine in sottoclassi per metodi definiti in una classe base.

Sebbene l'override del metodo sia una caratteristica potente, considerando che è una conseguenza logica dell'uso dell'ereditarietà, uno dei più grandi pilastri dell'OOP, quando e dove utilizzarlo dovrebbe essere analizzato attentamente, in base al caso d'uso .

Vediamo ora come utilizzare l'override del metodo creando una semplice relazione basata sull'ereditarietà ("è-a").

Ecco la classe base:

public class Vehicle { public String accelerate(long mph) { return "The vehicle accelerates at : " + mph + " MPH."; } public String stop() { return "The vehicle has stopped."; } public String run() { return "The vehicle is running."; } }

Ed ecco una sottoclasse inventata:

public class Car extends Vehicle { @Override public String accelerate(long mph) { return "The car accelerates at : " + mph + " MPH."; } }

Nella gerarchia sopra, abbiamo semplicemente sovrascritto il metodo accelerate () per fornire un'implementazione più raffinata per il sottotipo Car.

Qui, è chiaro a vedere che se un utilizza l'applicazione istanze della Vehicle di classe, quindi si può lavorare con le istanze di auto e , in quanto entrambe le implementazioni del accelerare () metodo hanno la stessa firma e lo stesso tipo di ritorno.

Scriviamo alcuni unit test per controllare le classi Veicolo e Auto :

@Test public void whenCalledAccelerate_thenOneAssertion() { assertThat(vehicle.accelerate(100)) .isEqualTo("The vehicle accelerates at : 100 MPH."); } @Test public void whenCalledRun_thenOneAssertion() { assertThat(vehicle.run()) .isEqualTo("The vehicle is running."); } @Test public void whenCalledStop_thenOneAssertion() { assertThat(vehicle.stop()) .isEqualTo("The vehicle has stopped."); } @Test public void whenCalledAccelerate_thenOneAssertion() { assertThat(car.accelerate(80)) .isEqualTo("The car accelerates at : 80 MPH."); } @Test public void whenCalledRun_thenOneAssertion() { assertThat(car.run()) .isEqualTo("The vehicle is running."); } @Test public void whenCalledStop_thenOneAssertion() { assertThat(car.stop()) .isEqualTo("The vehicle has stopped."); } 

Vediamo ora alcuni unit test che mostrano come i metodi run () e stop () , che non sono sovrascritti, restituiscono valori uguali sia per Auto che per Veicolo :

@Test public void givenVehicleCarInstances_whenCalledRun_thenEqual() { assertThat(vehicle.run()).isEqualTo(car.run()); } @Test public void givenVehicleCarInstances_whenCalledStop_thenEqual() { assertThat(vehicle.stop()).isEqualTo(car.stop()); }

Nel nostro caso, abbiamo accesso al codice sorgente per entrambe le classi, quindi possiamo vedere chiaramente che la chiamata del metodo accelerate () su un'istanza di Vehicle base e la chiamata di accelerate () su un'istanza di Car restituiranno valori diversi per lo stesso argomento.

Pertanto, il seguente test dimostra che il metodo sottoposto a override viene richiamato per un'istanza di Car :

@Test public void whenCalledAccelerateWithSameArgument_thenNotEqual() { assertThat(vehicle.accelerate(100)) .isNotEqualTo(car.accelerate(100)); }

3.1. Tipo Sostituibilità

Un principio fondamentale in OOP è quello della sostituibilità del tipo, che è strettamente associato al principio di sostituzione di Liskov (LSP).

In poche parole, l'LSP afferma che se un'applicazione funziona con un determinato tipo di base, dovrebbe funzionare anche con uno qualsiasi dei suoi sottotipi . In questo modo, la sostituibilità del tipo viene adeguatamente preservata.

Il problema più grande con l'override del metodo è che alcune implementazioni di metodi specifici nelle classi derivate potrebbero non aderire completamente all'LSP e quindi non riuscire a preservare la sostituibilità del tipo.

Ovviamente, è valido creare un metodo sovrascritto per accettare argomenti di tipi diversi e restituire anche un tipo diverso, ma con piena aderenza a queste regole:

  • Se un metodo nella classe base accetta uno o più argomenti di un dato tipo, il metodo sovrascritto dovrebbe prendere lo stesso tipo o un supertipo (ovvero argomenti del metodo controvariante )
  • Se un metodo nella classe base restituisce void , il metodo sottoposto a override dovrebbe restituire void
  • Se un metodo nella classe base restituisce una primitiva, il metodo sottoposto a override dovrebbe restituire la stessa primitiva
  • Se un metodo nella classe base restituisce un certo tipo, il metodo sostituito dovrebbe restituire lo stesso tipo o un sottotipo (noto anche come tipo restituito covariante )
  • Se un metodo nella classe base genera un'eccezione, il metodo sottoposto a override deve generare la stessa eccezione o un sottotipo dell'eccezione della classe base

3.2. Associazione dinamica

Considerando che l'override del metodo può essere implementato solo con l'ereditarietà, dove esiste una gerarchia di un tipo di base e sottotipi, il compilatore non può determinare in fase di compilazione quale metodo chiamare, poiché sia ​​la classe base che le sottoclassi definiscono il stessi metodi.

Di conseguenza, il compilatore deve controllare il tipo di oggetto per sapere quale metodo deve essere invocato.

Poiché questo controllo avviene in fase di esecuzione, l'override del metodo è un tipico esempio di associazione dinamica.

4. Conclusione

In questo tutorial, abbiamo imparato come implementare il sovraccarico del metodo e l'override del metodo e abbiamo esplorato alcune situazioni tipiche in cui sono utili.

Come al solito, tutti gli esempi di codice mostrati in questo articolo sono disponibili su GitHub.