Metóda preťaženia a prepísania v Jave

1. Prehľad

Preťaženie a prepísanie metódy sú kľúčové koncepty programovacieho jazyka Java, a preto si zaslúžia podrobný pohľad.

V tomto článku sa naučíme základy týchto pojmov a uvidíme, v akých situáciách môžu byť užitočné.

2. Preťaženie metódy

Preťaženie metód je mocný mechanizmus, ktorý nám umožňuje definovať API súdržnej triedy. Aby sme lepšie pochopili, prečo je preťaženie metód takou cennou vlastnosťou, pozrime sa na jednoduchý príklad.

Predpokladajme, že sme napísali naivnú triedu obslužných prostriedkov, ktorá implementuje rôzne metódy na vynásobenie dvoch čísel, troch čísel atď.

Keby sme dali metódy zavádzajúce alebo nejednoznačné názvy, ako napr multiply2 (), znásobiť3 (), vynásobiť4 (), potom by to bolo zle navrhnuté triedne API. Tu prichádza do úvahy preťaženie metód.

Zjednodušene môžeme preťaženie metód implementovať dvoma rôznymi spôsobmi:

  • implementácia dvoch alebo viacerých metódy, ktoré majú rovnaký názov, ale používajú rôzny počet argumentov
  • implementácia dvoch alebo viacerých metódy, ktoré majú rovnaký názov, ale prijímajú argumenty rôznych typov

2.1. Rôzny počet argumentov

The Násobiteľ trieda v skratke ukazuje, ako preťažiť znásobiť () jednoduchou definíciou dvoch implementácií, ktoré berú rôzny počet argumentov:

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. Argumenty rôznych typov

Podobne môžeme preťažiť znásobiť () metóda tým, že umožňuje akceptovať argumenty rôznych typov:

public class Multiplier {public int multiply (int a, int b) {return a * b; } verejné zdvojnásobenie (dvojité a, dvojité b) {návrat a * b; }} 

Ďalej je legitímne definovať Násobiteľ trieda s oboma typmi preťaženia metódy:

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; } verejné zdvojnásobenie (dvojité a, dvojité b) {návrat a * b; }} 

Stojí však za zmienku, že nie je možné mať dve implementácie metód, ktoré sa líšia iba návratovými typmi.

Aby sme pochopili prečo - zvážme nasledujúci príklad:

public int multiply (int a, int b) {return a * b; } verejné zdvojnásobenie (int a, int b) {return a * b; }

V tomto prípade, kód by sa jednoducho nezostavil kvôli nejednoznačnosti volania metódy - kompilátor by nevedel, ktorá implementácia znásobiť () zavolať.

2.3. Propagácia typu

Jednou z úhľadných funkcií poskytovaných preťažovaním metód je tzv propagácia typu, a.k.a. rozširujúca sa primitívna konverzia .

Zjednodušene povedané, jeden daný typ je implicitne povýšený na iný, ak neexistuje zhoda medzi typmi argumentov odovzdaných preťaženej metóde a implementácii konkrétnej metódy.

Ak chcete jasnejšie pochopiť, ako funguje propagácia typu, zvážte nasledujúce implementácie znásobiť () metóda:

verejné zdvojnásobenie (int a, dlhé b) {return a * b; } public int multiply (int a, int b, int c) {return a * b * c; } 

Teraz, volanie metódy dvoma int bude mať za následok povýšenie druhého argumentu na dlho, pretože v tomto prípade neexistuje zodpovedajúca implementácia metódy s dvoma int argumenty.

Pozrime sa na rýchly test jednotky na demonštráciu propagácie typu:

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

Naopak, ak zavoláme metódu s implementáciou zhody, propagácia typu sa neuskutoční:

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

Tu je súhrn pravidiel propagácie typu, ktoré platia pre preťaženie metód:

  • bajt možno povýšiť na krátky, int, dlhý, plavák, alebo dvojitý
  • krátky možno povýšiť na int, long, float, alebo dvojitý
  • char možno povýšiť na int, long, float, alebo dvojitý
  • int možno povýšiť na dlhý, plavák, alebo dvojitý
  • dlho možno povýšiť na plavák alebo dvojitý
  • plavák možno povýšiť na dvojitý

2.4. Statické viazanie

Schopnosť priradiť konkrétne volanie metódy k telu metódy je známa ako väzba.

V prípade preťaženia metódy sa väzba vykonáva staticky v čase kompilácie, preto sa nazýva statická väzba.

Kompilátor môže efektívne nastaviť väzbu v čase kompilácie jednoduchou kontrolou podpisov metód.

3. Prepísanie metódy

Prepísanie metódy nám umožňuje poskytnúť jemnozrnné implementácie v podtriedach pre metódy definované v základnej triede.

Zatiaľ čo prepísanie metódy je mocná vlastnosť - vzhľadom na to, že je to logický dôsledok používania dedičnosti, jedného z najväčších pilierov OOP - kedy a kde sa má použiť, by sa malo starostlivo analyzovať na základe jednotlivých prípadov.

Pozrime sa teraz, ako použiť prepísanie metódy vytvorením jednoduchého vzťahu založeného na dedičstve („je-a“).

Tu je základná trieda:

public class Vehicle {public String accelerate (long mph) {return "Vozidlo akceleruje pri:" + mph + "MPH."; } public String stop () {návrat "Vozidlo sa zastavilo."; } public String run () {return "Vozidlo je v prevádzke."; }}

A tu je vykonštruovaná podtrieda:

public class Car extends Vehicle {@Override public String accelerate (long mph) {return "Auto zrýchľuje o:" + mph + "MPH."; }}

V hierarchii vyššie sme jednoducho prepísali urýchliť() metóda s cieľom poskytnúť rafinovanejšiu implementáciu pre podtyp Auto.

Tu je to jasne vidieť ak aplikácia používa inštancie Vozidlo triedy, potom môže pracovať s inštanciami Auto tiež, pretože obe implementácie urýchliť() metóda majú rovnaký podpis a rovnaký návratový typ.

Napíšeme niekoľko jednotkových testov na kontrolu Vozidlo a Auto triedy:

@Test public void whenCalledAccelerate_thenOneAssertion () {assertThat (vehicle.accelerate (100)) .isEqualTo ("Vozidlo akceleruje pri: 100 MPH."); } @Test public void whenCalledRun_thenOneAssertion () {assertThat (vehicle.run ()) .isEqualTo ("Vozidlo je v prevádzke."); } @Test public void whenCalledStop_thenOneAssertion () {assertThat (vehicle.stop ()) .isEqualTo ("Vozidlo sa zastavilo."); } @Test public void whenCalledAccelerate_thenOneAssertion () {assertThat (car.accelerate (80)) .isEqualTo ("Auto zrýchľuje pri: 80 MPH."); } @Test public void whenCalledRun_thenOneAssertion () {assertThat (car.run ()) .isEqualTo ("Vozidlo je v prevádzke."); } @Test public void whenCalledStop_thenOneAssertion () {assertThat (car.stop ()) .isEqualTo ("Vozidlo sa zastavilo."); } 

Teraz sa pozrime na niekoľko testov jednotiek, ktoré ukazujú, ako run () a stop () metódy, ktoré nie sú prepísané, vracajú rovnaké hodnoty pre obidve Auto a Vozidlo:

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

V našom prípade máme prístup k zdrojovému kódu pre obe triedy, takže môžeme jasne vidieť, že volanie kódu urýchliť() metóda na základe Vozidlo inštancia a volanie urýchliť() na a Auto inštancia vráti rôzne hodnoty pre ten istý argument.

Nasledujúci test preto demonštruje, že prepísaná metóda je vyvolaná pre inštanciu Auto:

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

3.1. Typ Zameniteľnosť

Základným princípom v OOP je princíp zastupiteľnosti typu, ktorý je úzko spojený s princípom substitúcie Liskov (LSP).

Jednoducho povedané, LSP to tvrdí ak aplikácia pracuje s daným základným typom, potom by mala pracovať aj s ktorýmkoľvek z jej podtypov. Takto je nahraditeľnosť typu správne zachovaná.

Najväčší problém s prepísaním metódy je, že niektoré implementácie špecifických metód v odvodených triedach nemusia úplne dodržiavať LSP, a preto nezachovajú nahraditeľnosť typu.

Je samozrejme platné urobiť prepísanú metódu na prijímanie argumentov rôznych typov a vrátenie iného typu, ale s úplným dodržiavaním týchto pravidiel:

  • Ak metóda v základnej triede prijíma argumenty daného typu, prepísaná metóda by mala mať rovnaký typ alebo supertyp (a.k.a. protikladný argumenty metódy)
  • Ak sa vráti metóda v základnej triede neplatný, prepísaná metóda by sa mala vrátiť neplatný
  • Ak metóda v základnej triede vráti primitív, prepísaná metóda by mala vrátiť to isté primitívne
  • Ak metóda v základnej triede vráti určitý typ, prepísaná metóda by mala vrátiť rovnaký typ alebo podtyp (a.k.a. kovarianty návratový typ)
  • Ak metóda v základnej triede vyvolá výnimku, prepísaná metóda musí vyvolať rovnakú výnimku alebo podtyp výnimky základnej triedy

3.2. Dynamická väzba

Vzhľadom na to, že prepísanie metódy je možné implementovať iba pomocou dedičnosti, kde existuje hierarchia základného typu a podtypov, kompilátor nemôže v čase kompilácie určiť, ktorá metóda sa má volať, pretože základná trieda aj podtriedy definujú rovnaké metódy.

V dôsledku toho musí kompilátor skontrolovať typ objektu, aby vedel, ktorá metóda by sa mala vyvolať.

Pretože táto kontrola prebieha za behu, prepísanie metódy je typickým príkladom dynamickej väzby.

4. Záver

V tomto tutoriáli sme sa naučili, ako implementovať preťaženie metód a prepisovanie metód, a preskúmali sme niektoré typické situácie, kde sú užitočné.

Ako obvykle sú všetky ukážky kódu zobrazené v tomto článku k dispozícii na GitHub.