Metodeoverbelastning og tilsidesættelse i Java

1. Oversigt

Metodeoverbelastning og tilsidesættelse er nøglebegreber i Java-programmeringssproget, og som sådan fortjener de et dybtgående udseende.

I denne artikel lærer vi det grundlæggende i disse begreber og ser i hvilke situationer de kan være nyttige.

2. Metodeoverbelastning

Metodeoverbelastning er en stærk mekanisme, der giver os mulighed for at definere sammenhængende klasse API'er. For bedre at forstå, hvorfor metodeoverbelastning er en så værdifuld funktion, lad os se et simpelt eksempel.

Antag, at vi har skrevet en naiv nytteklasse, der implementerer forskellige metoder til at multiplicere to tal, tre tal osv.

Hvis vi har givet metoderne vildledende eller tvetydige navne, såsom multiplicere2 (), gang 3 (), multiplicere4 (), så ville det være en dårligt designet klasse API. Her er hvor metode overbelastning kommer i spil.

Kort sagt kan vi implementere metodeoverbelastning på to forskellige måder:

  • implementering af to eller flere metoder, der har samme navn, men som tager forskellige antal argumenter
  • implementering af to eller flere metoder, der har samme navn, men som tager argumenter af forskellige typer

2.1. Forskellige antal argumenter

Det Multiplikator klasse viser i en nøddeskal, hvordan man overbelaster formere sig() metode ved blot at definere to implementeringer, der tager forskellige antal argumenter:

offentlig klasse Multiplikator {offentlig int multiplicere (int a, int b) {return a * b; } offentlig int multiplicere (int a, int b, int c) {return a * b * c; }}

2.2. Argumenter af forskellige typer

På samme måde kan vi overbelaste formere sig() metode ved at lade den acceptere argumenter af forskellige typer:

offentlig klasse Multiplikator {offentlig int multiplicere (int a, int b) {return a * b; } offentlig dobbelt gang (dobbelt a, dobbelt b) {returner * b; }} 

Desuden er det legitimt at definere Multiplikator klasse med begge typer overbelastning af metoder:

offentlig klasse Multiplikator {offentlig int multiplicere (int a, int b) {return a * b; } public int multiplicere (int a, int b, int c) {return a * b * c; } offentlig dobbelt gang (dobbelt a, dobbelt b) {returner * b; }} 

Det er dog værd at bemærke det det er ikke muligt at have to metodeimplementeringer, der kun adskiller sig i deres returtyper.

For at forstå hvorfor - lad os overveje følgende eksempel:

offentlig int multiplicere (int a, int b) {return a * b; } offentlig dobbelt gang (int a, int b) {return a * b; }

I dette tilfælde, koden ville simpelthen ikke kompilere på grund af metoden kalder tvetydighed - kompilatoren ville ikke vide, hvilken implementering af formere sig() at ringe.

2.3. Type forfremmelse

En pæn funktion leveret af metodeoverbelastning er den såkaldte type forfremmelse, altså udvidelse af primitiv konvertering .

Enkelt set promoveres en given type implicit til en anden, når der ikke er nogen matchning mellem de typer af argumenter, der sendes til den overbelastede metode, og en specifik metodeimplementering.

For at forstå mere klart, hvordan typefremme fungerer, skal du overveje følgende implementeringer af formere sig() metode:

offentlig dobbelt gang (int a, lang b) {return a * b; } public int multiplicere (int a, int b, int c) {return a * b * c; } 

Nu kalder man metoden med to int argumenter vil resultere i, at det andet argument fremmes til lang, som i dette tilfælde er der ikke en matchende implementering af metoden med to int argumenter.

Lad os se en hurtig enhedstest for at demonstrere typefremmelse:

@Test offentlig ugyldig nårCalledMultiplyAndNoMatching_thenTypePromotion () {assertThat (multiplier.multiply (10, 10)). IsEqualTo (100.0); }

Omvendt, hvis vi kalder metoden med en matchende implementering, finder kampagnen ikke sted:

@Test offentlig ugyldig nårCalledMultiplyAndMatching_thenNoTypePromotion () {assertThat (multiplier.multiply (10, 10, 10)). IsEqualTo (1000); }

Her er et resumé af de typer salgsfremmende regler, der gælder for metodeoverbelastning:

  • byte kan forfremmes til kort, int, langt, flyde, eller dobbelt
  • kort kan forfremmes til int, lang, flyde, eller dobbelt
  • char kan forfremmes til int, lang, flyde, eller dobbelt
  • int kan forfremmes til lang, flyde, eller dobbelt
  • lang kan forfremmes til flyde eller dobbelt
  • flyde kan forfremmes til dobbelt

2.4. Statisk binding

Evnen til at knytte et specifikt metodekald til metodens krop er kendt som bindende.

I tilfælde af metodeoverbelastning udføres bindingen statisk på kompileringstidspunktet, hvorfor det kaldes statisk binding.

Compileren kan effektivt indstille bindingen på kompileringstidspunktet ved blot at kontrollere metodernes underskrifter.

3. Overstyring af metode

Metodeoverstyring giver os mulighed for at levere finkornede implementeringer i underklasser til metoder defineret i en basisklasse.

Mens metodeoverstyring er en stærk funktion - i betragtning af at det er en logisk konsekvens af at bruge arv, en af ​​de største søjler i OOP - hvornår og hvor det skal bruges, skal det analyseres omhyggeligt pr. anvendelse.

Lad os nu se, hvordan man bruger metodeoverstyring ved at oprette et simpelt, arvebaseret ("is-a") forhold.

Her er basisklassen:

public class Vehicle {public String accelerate (long mph) {return "Køretøjet accelererer ved:" + mph + "MPH."; } public String stop () {return "Køretøjet er stoppet."; } public String run () {return "Køretøjet kører."; }}

Og her er en konstrueret underklasse:

offentlig klasse Bil udvider køretøj {@Override public String accelerate (long mph) {return "Bilen accelererer ved:" + mph + "MPH."; }}

I hierarkiet ovenfor har vi simpelthen tilsidesat fremskynde() metode for at give en mere raffineret implementering af undertypen Bil.

Her er det klart at se det hvis et program bruger forekomster af Køretøj klasse, så kan det arbejde med forekomster af Bil såvel, da begge implementeringer af fremskynde() metode har den samme signatur og den samme returtype.

Lad os skrive et par enhedstests for at kontrollere Køretøj og Bil klasser:

@Test offentligt ugyldigt nårCalledAccelerate_thenOneAssertion () {assertThat (vehicle.accelerate (100)) .isEqualTo ("Køretøjet accelererer ved: 100 MPH."); } @Test offentlig ugyldig nårCalledRun_thenOneAssertion () {assertThat (vehicle.run ()) .isEqualTo ("Vehicle running."); } @Test offentligt ugyldigt nårCalledStop_thenOneAssertion () {assertThat (vehicle.stop ()) .isEqualTo ("Vehicle has stop."); } @Test offentlig ugyldig nårCalledAccelerate_thenOneAssertion () {assertThat (car.accelerate (80)) .isEqualTo ("Bilen accelererer ved: 80 MPH."); } @Test offentlig ugyldig nårCalledRun_thenOneAssertion () {assertThat (car.run ()) .isEqualTo ("Vehicle running."); } @Test offentlig ugyldig nårCalledStop_thenOneAssertion () {assertThat (car.stop ()) .isEqualTo ("Køretøjet er stoppet."); } 

Lad os nu se nogle enhedstest, der viser, hvordan løb() og hold op() metoder, som ikke tilsidesættes, returnerer lige store værdier for begge Bil og Køretøj:

@Test offentlig ugyldighed givenVehicleCarInstances_whenCalledRun_thenEqual () {assertThat (vehicle.run ()). IsEqualTo (car.run ()); } @Test offentligt ugyldigt givenVehicleCarInstances_whenCalledStop_thenEqual () {assertThat (vehicle.stop ()). IsEqualTo (car.stop ()); }

I vores tilfælde har vi adgang til kildekoden for begge klasser, så vi kan tydeligt se, at det ringer til fremskynde() metode på en base Køretøj instans og opkald fremskynde() på en Bil instans returnerer forskellige værdier for det samme argument.

Derfor demonstrerer følgende test, at den tilsidesatte metode påberåbes til en forekomst af Bil:

@Test offentlig ugyldig nårCalledAccelerateWithSameArgument_thenNotEqual () {assertThat (vehicle.accelerate (100)) .isNotEqualTo (car.accelerate (100)); }

3.1. Type substituerbarhed

Et grundlæggende princip i OOP er typen af ​​substituerbarhed, som er tæt forbundet med Liskov-substitutionsprincippet (LSP).

Enkelt sagt siger LSP det hvis en applikation fungerer med en given basetype, skal den også fungere med en hvilken som helst af dens undertyper. På den måde bevares type substituerbarhed korrekt.

Det største problem med metodeoverstyring er, at nogle specifikke metodeimplementeringer i de afledte klasser måske ikke fuldt ud overholder LSP og derfor ikke bevarer typesubstituerbarhed.

Det er selvfølgelig gyldigt at lave en tilsidesat metode til at acceptere argumenter af forskellige typer og også returnere en anden type, men med fuld overholdelse af disse regler:

  • Hvis en metode i baseklassen tager argument (er) af en given type, skal den tilsidesatte metode tage den samme type eller en supertype (aka modstridende metode argumenter)
  • Hvis en metode i basisklassen vender tilbage ugyldig, den tilsidesatte metode skal vende tilbage ugyldig
  • Hvis en metode i baseklassen returnerer en primitiv, skal den tilsidesatte metode returnere den samme primitive
  • Hvis en metode i basisklassen returnerer en bestemt type, skal den tilsidesatte metode returnere den samme type eller en undertype (aka kovariant returtype)
  • Hvis en metode i basisklassen kaster en undtagelse, skal den tilsidesatte metode kaste den samme undtagelse eller en undertype til baseklasseundtagelsen

3.2. Dynamisk binding

I betragtning af at metodeoverstyring kun kan implementeres med arv, hvor der er et hierarki af en basistype og undertype, kan compileren ikke bestemme på kompileringstidspunktet, hvilken metode der skal kaldes, da både baseklassen og underklasserne definerer samme metoder.

Som en konsekvens skal compileren kontrollere typen af ​​objekt for at vide, hvilken metode der skal påberåbes.

Da denne kontrol sker ved kørsel, er metodeoverstyring et typisk eksempel på dynamisk binding.

4. Konklusion

I denne vejledning lærte vi, hvordan man implementerer metodeoverbelastning og metodeoverstyring, og vi udforskede nogle typiske situationer, hvor de er nyttige.

Som sædvanligt er alle kodeeksempler vist i denne artikel tilgængelige på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found