Programovanie

Odhaľte kúzlo podtypu polymorfizmu

Slovo polymorfizmus pochádza z gréčtiny pre „mnoho podôb“. Väčšina vývojárov v Java spája tento výraz so schopnosťou objektu magicky vykonávať správne chovanie metódy vo vhodných bodoch programu. Tento pohľad orientovaný na implementáciu však vedie skôr k obrazom čarodejníctva, než k pochopeniu základných pojmov.

Polymorfizmus v Jave je vždy podtypom polymorfizmu. Dôkladné preskúmanie mechanizmov, ktoré generujú túto rozmanitosť polymorfného správania, si vyžaduje, aby sme odhodili svoje obvyklé implementačné obavy a uvažovali z hľadiska typu. Tento článok skúma typovo orientovanú perspektívu objektov a to, ako sa táto perspektíva oddeľuje čo správanie, z ktorého môže objekt vyjadrovať ako objekt toto správanie skutočne vyjadruje. Uvoľnením nášho konceptu polymorfizmu z hierarchie implementácie tiež zistíme, ako rozhrania Java uľahčujú polymorfné správanie naprieč skupinami objektov, ktoré vôbec nezdieľajú žiadny implementačný kód.

Quattro polymorfy

Polymorfizmus je široký objektovo orientovaný pojem. Aj keď obvykle zrovnávame všeobecný koncept s odrodou podtypu, v skutočnosti existujú štyri rôzne druhy polymorfizmu. Predtým, ako podrobne preskúmame polymorfizmus podtypu, v nasledujúcej časti nájdete všeobecný prehľad polymorfizmu v objektovo orientovaných jazykoch.

Luca Cardelli a Peter Wegner, autori publikácie „O porozumení typov, abstrakcie údajov a polymorfizmu“ (pozri Zdroje pre odkaz na článok) rozdeľujú polymorfizmus do dvoch hlavných kategórií - ad hoc a univerzálna - a na štyri varianty: nátlak, preťaženie, parametrické a začlenenie. Štruktúra klasifikácie je:

 | - nátlak | - ad hoc - | | - preťažujúci polymorfizmus - | | - parametrické | - univerzálne - | | - začlenenie 

V tejto všeobecnej schéme predstavuje polymorfizmus schopnosť entity mať viac foriem. Univerzálny polymorfizmus označuje uniformitu typovej štruktúry, v ktorej polymorfizmus účinkuje na nekonečné množstvo typov, ktoré majú spoločnú vlastnosť. Tým menej štruktúrovaným ad hoc polymorfizmus koná nad konečným počtom pravdepodobne nesúvisiacich typov. Tieto štyri odrody možno označiť ako:

  • Nátlak: jedna abstrakcia slúži niekoľkým typom prostredníctvom implicitnej konverzie typov
  • Preťaženie: jediný identifikátor označuje niekoľko abstrakcií
  • Parametrické: abstrakcia funguje jednotne medzi rôznymi typmi
  • Zahrnutie: abstrakcia funguje prostredníctvom inklúzneho vzťahu

Stručne prediskutujem každú odrodu, skôr ako prejdem konkrétne k podtypu polymorfizmu.

Nátlak

Nátlak predstavuje implicitnú konverziu typu parametra na typ očakávaný metódou alebo operátorom, čím sa zabráni chybám typu. Pre nasledujúce výrazy musí kompilátor určiť, či je vhodný binárny súbor + existuje operátor pre typy operandov:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Prvý výraz pridáva dva dvojitý operandy; jazyk Java osobitne definuje takého operátora.

Druhý výraz však pridáva a dvojitý a an int; Java nedefinuje operátora, ktorý prijíma tieto typy operandov. Našťastie kompilátor implicitne prevádza druhý operand na dvojitý a používa operátor definovaný pre dvoch dvojitý operandy. To je pre vývojára mimoriadne výhodné; bez implicitnej konverzie by vznikla chyba v čase kompilácie alebo by programátor musel explicitne obsadiť int do dvojitý.

Tretí výraz pridáva a dvojitý a a String. Jazyk Java takého operátora opäť nedefinuje. Takže kompilátor vynúti dvojitý operand do a String, a operátor plus vykoná zreťazenie reťazca.

K nátlaku dochádza aj pri vyvolaní metódy. Predpokladajme, že trieda Odvodené rozširuje triedu Základňaa trieda C. má metódu s podpisom m (základňa). Pre vyvolanie metódy v nižšie uvedenom kóde kompilátor implicitne prevádza odvodené referenčná premenná, ktorá má typ Odvodené, do Základňa typ predpísaný podpisom metódy. Táto implicitná konverzia umožňuje m (základňa) implementačný kód metódy na použitie iba typových operácií definovaných v Základňa:

 CC = nové C (); Odvodené odvodené = nové Odvodené (); c.m (odvodené); 

Implicitné nátlak počas vyvolania metódy opäť zabraňuje ťažkopádnemu obsadeniu typu alebo zbytočnej chybe pri kompilácii. Kompilátor samozrejme stále overuje, či sú všetky prevody typov v súlade s definovanou hierarchiou typov.

Preťaženie

Preťaženie umožňuje použitie rovnakého názvu operátora alebo metódy na označenie viacerých odlišných významov programu. The + operátor použitý v predchádzajúcej časti vystavil dve formy: jednu na pridanie dvojitý operandy, jeden na zreťazenie String predmety. Existujú aj iné formy na pridanie dvoch celých čísel, dvoch dĺžok atď. Voláme operátorovi preťažený a spoľahnúť sa na to, že kompilátor vyberie príslušnú funkčnosť na základe kontextu programu. Ako už bolo uvedené, v prípade potreby kompilátor implicitne prevádza typy operandov tak, aby zodpovedali presnému podpisu operátora. Aj keď Java špecifikuje určité preťažené operátory, nepodporuje používateľom definované preťaženie operátorov.

Java umožňuje používateľom definované preťaženie názvov metód. Trieda môže mať viac metód s rovnakým názvom za predpokladu, že sú podpisy metód odlišné. To znamená, že buď sa počet parametrov musí líšiť, alebo aspoň jedna pozícia parametra musí mať iný typ. Jedinečné podpisy umožňujú kompilátoru rozlišovať medzi metódami, ktoré majú rovnaký názov. Kompilátor upravuje názvy metód pomocou jedinečných podpisov a efektívne tak vytvára jedinečné názvy. Vo svetle toho sa akékoľvek zjavné polymorfné správanie pri bližšom skúmaní odparí.

Nátlak aj preťaženie sú klasifikované ako ad hoc, pretože každá poskytuje polymorfné správanie iba v obmedzenom zmysle. Aj keď spadajú pod širokú definíciu polymorfizmu, sú tieto odrody predovšetkým vymoženosťami vývojárov. Donucovanie odstraňuje ťažkopádne explicitné obsadenie alebo zbytočné chyby typu kompilátora. Preťaženie na druhej strane poskytuje syntaktický cukor, čo umožňuje vývojárovi používať rovnaký názov pre odlišné metódy.

Parametrické

Parametrický polymorfizmus umožňuje použitie jednej abstrakcie naprieč mnohými typmi. Napríklad a Zoznam abstrakciu, predstavujúcu zoznam homogénnych objektov, je možné poskytnúť ako všeobecný modul. Abstrakciu by ste opakovane použili zadaním typov objektov obsiahnutých v zozname. Pretože parametrizovaným typom môže byť akýkoľvek používateľom definovaný dátový typ, existuje všeobecná abstrakcia potenciálne nekonečného počtu použití, čo z neho robí pravdepodobne najvýkonnejší typ polymorfizmu.

Na prvý pohľad vyššie uvedené Zoznam abstrakcia sa môže javiť ako užitočnosť triedy java.util.List. Java však nepodporuje skutočný parametrický polymorfizmus spôsobom bezpečným pre typ, a preto java.util.List a java.utilOstatné zbierkové triedy sú napísané v zmysle prvotnej triedy Java, java.lang.Objekt. (Viac podrobností nájdete v mojom článku „Prvotné rozhranie?“.) Jedno-zakorenené dedičstvo implementácie Java ponúka čiastočné riešenie, ale nie skutočnú moc parametrického polymorfizmu. Vynikajúci článok Erica Allena s názvom „Hľa, sila parametrického polymorfizmu“ popisuje potrebu generických typov v prostredí Java a návrhy týkajúce sa žiadosti spoločnosti Sun o špecifikáciu Java č. 000014, „Add Generic Types to the Java Programming Language“. (Odkaz nájdete v zdrojoch.)

Zahrnutie

Inklúzny polymorfizmus dosahuje polymorfné správanie prostredníctvom inklúzneho vzťahu medzi typmi alebo množinami hodnôt. Pre mnoho objektovo orientovaných jazykov vrátane Java je inklúzny vzťah vzťahom podtypu. Takže v Jave je inklúzny polymorfizmus podtypom polymorfizmu.

Ako už bolo uvedené skôr, keď vývojári Java všeobecne odkazujú na polymorfizmus, znamenajú vždy podtypový polymorfizmus. Získanie solídneho ocenenia sily podtypu polymorfizmu si vyžaduje pohľad na mechanizmy poskytujúce polymorfné správanie z pohľadu typu. Zvyšok tohto článku podrobne skúma túto perspektívu. Pre stručnosť a prehľadnosť používam výraz polymorfizmus vo význame polymorfizmu podtypu.

Pohľad orientovaný na typ

Diagram tried UML na obrázku 1 zobrazuje jednoduchú hierarchiu typov a tried používanú na ilustráciu mechaniky polymorfizmu. Model zobrazuje päť typov, štyri triedy a jedno rozhranie. Aj keď sa model nazýva triedny diagram, považujem ho za typový diagram. Ako je podrobne uvedené v časti „Vďaka typu a jemnej triede“, každá trieda a rozhranie Java deklaruje používateľom definovaný dátový typ. Takže z pohľadu nezávislého na implementácii (t.j. typovo orientovaného pohľadu) predstavuje každý z piatich obdĺžnikov na obrázku typ. Z hľadiska implementácie sú štyri z týchto typov definované pomocou konštrukcií tried a jeden je definovaný pomocou rozhrania.

Nasledujúci kód definuje a implementuje každý používateľom definovaný dátový typ. Úmyselne udržujem implementáciu čo najjednoduchšiu:

/ * Base.java * / public class Base {public String m1 () {return "Base.m1 ()"; } public String m2 (String s) {return "Base.m2 (" + s + ")"; }} / * IType.java * / interface IType {String m2 (String s); Reťazec m3 (); } / * Derived.java * / public class Derived extends Base implements IType {public String m1 () {return "Derived.m1 ()"; } public String m3 () {return "Derived.m3 ()"; }} / * Derived2.java * / public class Derived2 extends Derived {public String m2 (String s) {return "Derived2.m2 (" + s + ")"; } public String m4 () {return "Derived2.m4 ()"; }} / * Separate.java * / public class Separate implements IType {public String m1 () {return "Separate.m1 ()"; } public String m2 (String s) {return "Separate.m2 (" + s + ")"; } public String m3 () {return "Separate.m3 ()"; }} 

Použitím týchto deklarácií typov a definícií tried, Obrázok 2 zobrazuje koncepčný pohľad na príkaz Java:

Odvodené2 odvodené2 = nové Odvodené2 (); 

Vyššie uvedené vyhlásenie deklaruje explicitne napísanú referenčnú premennú, odvodené2, a pripojí tento odkaz k novovytvorenému Odvodené2 objekt triedy. Horný panel na obrázku 2 zobrazuje Odvodené2 odkaz ako súbor svetlíkov, ktorými prechádzajú podkladové Odvodené2 objekt je možné prezerať. Pre každú je určená jedna diera Odvodené2 operácia typu. Aktuálny Odvodené2 každý objekt mapuje Odvodené2 operácie na vhodný implementačný kód, ako to predpisuje implementačná hierarchia definovaná vo vyššie uvedenom kóde. Napríklad Odvodené2 objektové mapy m1 () na implementačný kód definovaný v triede Odvodené. Uvedený implementačný kód ďalej prednosť pred m1 () metóda v triede Základňa. A Odvodené2 referenčná premenná nemá prístup k prepísanej m1 () implementácia v triede Základňa. To neznamená, že skutočný implementačný kód v triede Odvodené nemôže použiť Základňa implementácia triedy cez super.m1 (). Ale pokiaľ ide o referenčnú premennú odvodené2 je znepokojený tým, že tento kód je neprístupný. Mapovania druhého Odvodené2 operácie podobne ukazujú implementačný kód vykonaný pre každú operáciu typu.

Teraz, keď máte Odvodené2 objekt, môžete ho odkázať na akúkoľvek premennú, ktorá zodpovedá typu Odvodené2. Odhaľuje to hierarchia typov na UML diagrame na obrázku 1 Odvodené, Základňaa IType sú všetky super typy Odvodené2. Takže napríklad a Základňa k objektu je možné pripojiť odkaz. Obrázok 3 zobrazuje koncepčné zobrazenie nasledujúceho príkazu Java:

Základňa základňa = odvodené2; 

Na podklade nie je absolútne žiadna zmena Odvodené2 objekt alebo ktorékoľvek z mapovaní operácií, hoci sú to metódy m3 () a m4 () už nie sú prístupné cez internet Základňa odkaz. Telefonovanie m1 () alebo m2 (reťazec) pomocou ktorejkoľvek premennej odvodené2 alebo základňa má za následok vykonanie rovnakého implementačného kódu:

Reťazec tmp; // Odkaz na odvodené2 (obrázok 2) tmp = odvodené2.m1 (); // tmp je „Derived.m1 ()“ tmp = derived2.m2 („Hello“); // tmp je „Derived2.m2 (Hello)“ // Základná referencia (obrázok 3) tmp = base.m1 (); // tmp je „Derived.m1 ()“ tmp = base.m2 („Hello“); // tmp je „Derived2.m2 (Hello)“ 

Realizácia rovnakého správania prostredníctvom oboch referencií má zmysel, pretože Odvodené2 objekt nevie, čo volá každú metódu. Objekt iba vie, že keď je vyzvaný, riadi sa pochodovými príkazmi definovanými hierarchiou implementácie. Tieto príkazy stanovujú, že pre metódu m1 (), Odvodené2 objekt vykoná kód v triede Odvodenéa pre metódu m2 (reťazec), vykoná kód v triede Odvodené2. Akcia vykonaná podkladovým objektom nezávisí od typu referenčnej premennej.

Všetko však nie je rovnaké, ak použijete referenčné premenné odvodené2 a základňa. Ako je znázornené na obrázku 3, a Základňa odkaz na typ môže vidieť iba Základňa operácie typu podkladového objektu. Takže hoci Odvodené2 má mapovania pre metódy m3 () a m4 (), premenná základňa nemá prístup k týmto metódam:

Reťazec tmp; // Odkaz na odvodené2 (obrázok 2) tmp = odvodené2.m3 (); // tmp je „Odvodené.m3 ()“ tmp = odvodené2.m4 (); // tmp je „Derived2.m4 ()“ // Základná referencia (obrázok 3) tmp = base.m3 (); // Chyba v čase kompilácie tmp = base.m4 (); // Chyba v čase kompilácie 

Runtime

Odvodené2

objekt zostáva plne schopný prijať buď

m3 ()

alebo

m4 ()

volania metód. Obmedzenia typu, ktoré zakazujú pokusy o volanie cez server

Základňa

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