Methode overbelasting en overschrijven in Java

1. Overzicht

Overbelasting en overschrijven van methoden zijn sleutelbegrippen van de programmeertaal Java, en als zodanig verdienen ze een diepgaande blik.

In dit artikel leren we de basisprincipes van deze concepten en zien we in welke situaties ze nuttig kunnen zijn.

2. Methode overbelasting

Overbelasting van methoden is een krachtig mechanisme waarmee we samenhangende klasse-API's kunnen definiëren. Laten we een eenvoudig voorbeeld bekijken om beter te begrijpen waarom overbelasting van methoden zo'n waardevolle functie is.

Stel dat we een naïeve utility-klasse hebben geschreven die verschillende methoden implementeert voor het vermenigvuldigen van twee getallen, drie getallen, enzovoort.

Als we de methoden misleidende of dubbelzinnige namen hebben gegeven, zoals vermenigvuldigen2 (), vermenigvuldigen3 (), multiply4 (), dan zou dat een slecht ontworpen klasse-API zijn. Hier komt overbelasting van de methode om de hoek kijken.

Simpel gezegd, we kunnen methode-overbelasting op twee verschillende manieren implementeren:

  • het implementeren van twee of meer methoden die dezelfde naam hebben maar een ander aantal argumenten hebben
  • implementatie van twee of meer methoden die dezelfde naam hebben maar argumenten van verschillende typen aannemen

2.1. Verschillende aantallen argumenten

De Vermenigvuldiger class laat in een notendop zien hoe de vermenigvuldigen() methode door simpelweg twee implementaties te definiëren die verschillende aantallen argumenten hebben:

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

2.2. Argumenten van verschillende typen

Evenzo kunnen we de vermenigvuldigen() methode door het argumenten van verschillende typen te laten accepteren:

public class Multiplier {public int vermenigvuldigen (int a, int b) {return a * b; } publiek dubbel vermenigvuldigen (dubbel a, dubbel b) {retourneer a * b; }} 

Bovendien is het legitiem om het Vermenigvuldiger klasse met beide soorten methodeoverbelasting:

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

Het is echter vermeldenswaard dat het is niet mogelijk om twee methode-implementaties te hebben die alleen verschillen in hun retourtypen.

Laten we, om te begrijpen waarom, het volgende voorbeeld bekijken:

public int multiply (int a, int b) {return a * b; } publieke dubbele vermenigvuldiging (int a, int b) {return a * b; }

In dit geval, de code zou simpelweg niet compileren vanwege de ambiguïteit van de methodeaanroep - de compiler zou niet weten welke implementatie van vermenigvuldigen() bellen.

2.3. Typ Promotie

Een leuke functie die wordt geboden door overbelasting van de methode is de zogenaamde type promotie, ook bekend als het verbreden van primitieve conversie .

In eenvoudige bewoordingen wordt een bepaald type impliciet gepromoveerd naar een ander wanneer er geen overeenkomst is tussen de typen argumenten die zijn doorgegeven aan de overbelaste methode en een specifieke methode-implementatie.

Overweeg de volgende implementaties van het vermenigvuldigen() methode:

publieke dubbele vermenigvuldiging (int a, long b) {return a * b; } public int multiply (int a, int b, int c) {return a * b * c; } 

Nu, de methode met twee aanroepen int argumenten zullen ertoe leiden dat het tweede argument wordt gepromoveerd tot lang, omdat er in dit geval geen overeenkomende implementatie van de methode met twee is int argumenten.

Laten we eens kijken naar een snelle eenheidstest om typepromotie te demonstreren:

@Test openbare leegte whenCalledMultiplyAndNoMatching_thenTypePromotion () {assertThat (multiplier.multiply (10, 10)). IsEqualTo (100.0); }

Omgekeerd, als we de methode aanroepen met een overeenkomende implementatie, type promotie vindt gewoon niet plaats:

@Test openbare leegte whenCalledMultiplyAndMatching_thenNoTypePromotion () {assertThat (multiplier.multiply (10, 10, 10)). IsEqualTo (1000); }

Hier is een samenvatting van de promotieregels van het type die van toepassing zijn op overbelasting van methoden:

  • byte kan worden gepromoveerd tot kort, int, lang, zweven, of dubbele
  • kort kan worden gepromoveerd tot int, lang, zweven, of dubbele
  • char kan worden gepromoveerd tot int, lang, zweven, of dubbele
  • int kan worden gepromoveerd tot lang, zweven, of dubbele
  • lang kan worden gepromoveerd tot vlotter of dubbele
  • vlotter kan worden gepromoveerd tot dubbele

2.4. Statische binding

De mogelijkheid om een ​​specifieke methodeaanroep aan de body van de methode te koppelen, staat bekend als binding.

In het geval van overbelasting van de methode, wordt de binding statisch uitgevoerd tijdens het compileren, vandaar de naam statische binding.

De compiler kan de binding tijdens het compileren effectief instellen door simpelweg de handtekeningen van de methoden te controleren.

3. Methode overschrijven

Methode overschrijven stelt ons in staat om fijnmazige implementaties in subklassen te bieden voor methoden die zijn gedefinieerd in een basisklasse.

Hoewel het overschrijven van methoden een krachtige functie is - aangezien dit een logisch gevolg is van het gebruik van overerving, een van de grootste pijlers van OOP - wanneer en waar het moet worden gebruikt, moet zorgvuldig worden geanalyseerd, per gebruik.

Laten we nu eens kijken hoe we methode-overschrijving kunnen gebruiken door een eenvoudige, op overerving gebaseerde ("is-a") relatie te creëren.

Hier is de basisklasse:

openbare klasse Voertuig {openbaar String versnellen (lange mph) {terug "Het voertuig accelereert met:" + mph + "MPH."; } public String stop () {return "Het voertuig is gestopt."; } public String run () {return "Het voertuig rijdt."; }}

En hier is een gekunstelde subklasse:

openbare klasse Auto verlengt Voertuig {@Override openbaar Snaar versnellen (lange mph) {return "De auto accelereert bij:" + mph + "MPH."; }}

In de bovenstaande hiërarchie hebben we simpelweg de versnellen() methode om een ​​meer verfijnde implementatie voor het subtype te bieden Auto.

Hier is het duidelijk om dat te zien als een applicatie exemplaren van de Voertuig class, dan kan het werken met instanties van Auto ook, aangezien beide implementaties van de versnellen() methode hebben dezelfde handtekening en hetzelfde retourtype.

Laten we een paar eenheidstests schrijven om het Voertuig en Auto klassen:

@Test public void whenCalledAccelerate_thenOneAssertion () {assertThat (vehicle.accelerate (100)) .isEqualTo ("Het voertuig accelereert met: 100 MPH."); } @Test public void whenCalledRun_thenOneAssertion () {assertThat (vehicle.run ()) .isEqualTo ("Het voertuig rijdt."); } @Test public void whenCalledStop_thenOneAssertion () {assertThat (vehicle.stop ()) .isEqualTo ("Het voertuig is gestopt."); } @Test public void whenCalledAccelerate_thenOneAssertion () {assertThat (car.accelerate (80)) .isEqualTo ("De auto accelereert met: 80 MPH."); } @Test public void whenCalledRun_thenOneAssertion () {assertThat (car.run ()) .isEqualTo ("Het voertuig rijdt."); } @Test public void whenCalledStop_thenOneAssertion () {assertThat (car.stop ()) .isEqualTo ("Het voertuig is gestopt."); } 

Laten we nu eens kijken naar enkele eenheidstests die laten zien hoe de rennen() en hou op() methoden, die niet worden overschreven, retourneren gelijke waarden voor beide Auto en Voertuig:

@Test openbare leegte gegevenVehicleCarInstances_whenCalledRun_thenEqual () {assertThat (vehicle.run ()). IsEqualTo (car.run ()); } @Test openbare leegte gegevenVehicleCarInstances_whenCalledStop_thenEqual () {assertThat (vehicle.stop ()). IsEqualTo (car.stop ()); }

In ons geval hebben we toegang tot de broncode voor beide klassen, dus we kunnen duidelijk zien dat het versnellen() methode op een basis Voertuig instantie en bellen versnellen() op een Auto instantie retourneert verschillende waarden voor hetzelfde argument.

Daarom laat de volgende test zien dat de overschreven methode wordt aangeroepen voor een exemplaar van Auto:

@Test openbare leegte whenCalledAccelerateWithSameArgument_thenNotEqual () {assertThat (vehicle.accelerate (100)) .isNotEqualTo (car.accelerate (100)); }

3.1. Typ Vervangbaarheid

Een kernprincipe in OOP is dat van type substitueerbaarheid, dat nauw verbonden is met het Liskov Substitution Principle (LSP).

Simpel gezegd stelt het LSP dat als een applicatie werkt met een bepaald basistype, dan zou het ook moeten werken met elk van zijn subtypen. Op die manier blijft de substitueerbaarheid van het type goed behouden.

Het grootste probleem met het overschrijven van methoden is dat sommige specifieke methode-implementaties in de afgeleide klassen mogelijk niet volledig voldoen aan het LSP en daarom de substitueerbaarheid van het type niet behouden.

Het is natuurlijk geldig om een ​​overschreven methode te maken om argumenten van verschillende typen te accepteren en ook een ander type te retourneren, maar met volledige naleving van deze regels:

  • Als een methode in de basisklasse argument (en) van een bepaald type accepteert, moet de overschreven methode hetzelfde type of een supertype (ook bekend als een supertype) aannemen. contravariant methode argumenten)
  • Als een methode in de basisklasse terugkeert leegte, zou de overschreven methode moeten terugkeren leegte
  • Als een methode in de basisklasse een primitief retourneert, moet de overschreven methode dezelfde primitief retourneren
  • Als een methode in de basisklasse een bepaald type retourneert, moet de overschreven methode hetzelfde type of een subtype (a.k.a. covariant retourtype)
  • Als een methode in de basisklasse een uitzondering genereert, moet de overschreven methode dezelfde uitzondering of een subtype van de basisklasse-uitzondering genereren

3.2. Dynamische binding

Aangezien het overschrijven van methoden alleen kan worden geïmplementeerd met overerving, waar er een hiërarchie is van een basistype en subtype (n), kan de compiler tijdens het compileren niet bepalen welke methode moet worden aangeroepen, omdat zowel de basisklasse als de subklassen de dezelfde methoden.

Als gevolg hiervan moet de compiler het type object controleren om te weten welke methode moet worden aangeroepen.

Aangezien deze controle tijdens runtime plaatsvindt, is het overschrijven van methoden een typisch voorbeeld van dynamische binding.

4. Conclusie

In deze zelfstudie hebben we geleerd hoe we methode-overbelasting en methode-overschrijving kunnen implementeren, en we hebben enkele typische situaties onderzocht waarin ze nuttig zijn.

Zoals gewoonlijk zijn alle codevoorbeelden die in dit artikel worden getoond, beschikbaar op GitHub.