Polymorfismus označuje schopnost některých entit vyskytovat se v různých formách. To je populárně reprezentováno motýlem, který se mění z larvy na kuklu na imago. Polymorfismus existuje také v programovacích jazycích jako metoda modelování, která umožňuje vytvořit jedno rozhraní pro různé operandy, argumenty a objekty. Výsledkem polymorfismu Java je kód, který je stručnější a snadněji se udržuje.
I když se tento kurz zaměřuje na podtyp polymorfismu, existuje několik dalších typů, o kterých byste měli vědět. Začneme s přehledem všech čtyř typů polymorfismu.
stáhnout Získat kód Stáhněte si zdrojový kód například pro aplikace v tomto výukovém programu. Vytvořil Jeff Friesen pro JavaWorld.Typy polymorfismu v Javě
V Javě existují čtyři typy polymorfismu:
- Donucování je operace, která slouží více typů prostřednictvím převodu implicitního typu. Například vydělíte celé číslo jiným celým číslem nebo hodnotu s plovoucí desetinnou čárkou jinou hodnotou s plovoucí desetinnou čárkou. Pokud je jeden operand celé číslo a druhý operand je hodnota s plovoucí desetinnou čárkou, kompilátor nátlaky (implicitně převede) celé číslo na hodnotu s plovoucí desetinnou čárkou, aby se zabránilo chybě typu. (Neexistuje žádná operace dělení, která podporuje celočíselný operand a operand s plovoucí desetinnou čárkou.) Dalším příkladem je předání odkazu na objekt podtřídy na parametr nadtřídy metody. Kompilátor vynucuje typ podtřídy na typ nadtřídy, aby omezil operace na nadtřídu.
- Přetížení odkazuje na použití stejného symbolu operátora nebo názvu metody v různých kontextech. Můžete například použít
+
provádět sčítání celých čísel, sčítání s plovoucí desetinnou čárkou nebo zřetězení řetězců v závislosti na typech jeho operandů. Ve třídě se také může objevit více metod se stejným názvem (prostřednictvím deklarace a / nebo dědičnosti). - Parametrické polymorfismus stanoví, že v rámci deklarace třídy se název pole může přidružit k různým typům a název metody se může přidružit k různým typům parametrů a návratů. Pole a metoda pak mohou nabývat různých typů v každé instanci třídy (objektu). Například pole může být typu
Dvojnásobek
(člen standardní knihovny tříd Java, která zabalí advojnásobek
hodnota) a metoda může vrátit aDvojnásobek
v jednom objektu a stejné pole může být typuTětiva
a stejná metoda může vrátit aTětiva
v jiném objektu. Java podporuje parametrický polymorfismus prostřednictvím generik, o kterých pojednám v budoucím článku. - Podtyp znamená, že typ může sloužit jako podtyp jiného typu. Když se instance podtypu objeví v kontextu nadtypu, provedení operace supertypu v instanci podtypu způsobí provedení této operace podtypu. Zvažte například fragment kódu, který kreslí libovolné tvary. Tento výkresový kód můžete vyjádřit výstižněji zavedením a
Tvar
třída s akreslit()
metoda; zavedenímKruh
,Obdélník
a další podtřídy, které mají přednostkreslit()
; zavedením pole typuTvar
jejichž prvky ukládají odkazy naTvar
instance podtřídy; a volánímTvar
jekreslit()
metoda pro každou instanci. Když zavoláškreslit()
, to jeKruh
je,Obdélník
nebo jinéTvar
instancekreslit()
metoda, která se volá. Říkáme, že existuje mnoho foremTvar
jekreslit()
metoda.
Tento kurz představuje podtyp polymorfismu. Dozvíte se o upcastingu a pozdní vazbě, abstraktních třídách (které nelze vytvořit instanci) a abstraktních metodách (které nelze vyvolat). Dozvíte se také o downcastingu a identifikaci typu runtime a získáte první pohled na typy kovariantních návratů. Uložím parametrický polymorfismus pro budoucí výukový program.
Ad-hoc vs univerzální polymorfismus
Stejně jako mnoho vývojářů klasifikuji nátlak a přetížení jako ad hoc polymorfismus a parametrický a podtyp jako univerzální polymorfismus. I když hodnotné techniky nevěřím, že nátlak a přetížení jsou opravdovým polymorfismem; spíš jako konverze typu a syntaktický cukr.
Polymorfismus podtypu: Upcasting a pozdní vazba
Polymorfismus podtypu závisí na upcastingu a pozdní vazbě. Upcasting je forma castingu, při které sesíláte hierarchii dědičnosti z podtypu na supertyp. Není zahrnut žádný operátor přetypování, protože podtyp je specializací supertypu. Například, Tvar s = nový kruh ();
upcasts z Kruh
na Tvar
. To dává smysl, protože kruh má určitý tvar.
Po upcastingu Kruh
na Tvar
, nemůžete volat Kruh
- specifické metody, jako je a getRadius ()
metoda, která vrací poloměr kruhu, protože Kruh
- specifické metody nejsou součástí Tvar
rozhraní. Ztráta přístupu k funkcím podtypů po zúžení podtřídy na její nadtřídu se zdá zbytečná, ale je nezbytná pro dosažení polymorfismu podtypu.
Předpokládejme to Tvar
prohlašuje a kreslit()
metoda, její Kruh
podtřída přepíše tuto metodu, Tvar s = nový kruh ();
právě provedeno a určuje další řádek s.draw ();
. Který kreslit()
metoda se nazývá: Tvar
je kreslit()
metoda nebo Kruh
je kreslit()
metoda? Kompilátor neví který kreslit()
způsob volání. Jediné, co může udělat, je ověřit, zda metoda existuje v nadtřídě, a ověřit, že seznam argumentů volání metody a návratový typ odpovídají deklaraci metody nadtřídy. Kompilátor však také vloží instrukci do kompilovaného kódu, který za běhu načte a použije jakýkoli odkaz v s
volat správné kreslit()
metoda. Tento úkol je známý jako pozdní vazba.
Pozdní vazba vs časná vazba
Pozdní vazba se používá pro volání nafinále
instance metody. Pro všechna ostatní volání metod kompilátor ví, kterou metodu volat. Vloží do kompilovaného kódu instrukci, která volá metodu přidruženou k typu proměnné a ne její hodnotu. Tato technika je známá jako časná vazba.
Vytvořil jsem aplikaci, která demonstruje podtypový polymorfismus, pokud jde o upcasting a pozdní vazbu. Tato aplikace se skládá z Tvar
, Kruh
, Obdélník
, a Tvary
třídy, kde je každá třída uložena ve vlastním zdrojovém souboru. Výpis 1 představuje první tři třídy.
Výpis 1. Deklarace hierarchie tvarů
třída Tvar {void draw () {}} třída Kruh rozšiřuje Tvar {soukromý int x, y, r; Kruh (int x, int y, int r) {this.x = x; this.y = y; this.r = r; } // Pro stručnost jsem vynechal metody getX (), getY () a getRadius (). @Override void draw () {System.out.println ("Kreslení kruhu (" + x + "," + y + "," + r + ")"); }} class Rectangle extends Shape {private int x, y, w, h; Obdélník (int x, int y, int w, int h) {this.x = x; this.y = y; this.w = w; this.h = h; } // Pro stručnost jsem vynechal metody getX (), getY (), getWidth () a getHeight () //. @Override void draw () {System.out.println ("Kreslení obdélníku (" + x + "," + y + "," + w + "," + h + ")"); }}
Výpis 2 představuje Tvary
třída aplikace, jejíž hlavní()
metoda řídí aplikaci.
Výpis 2. Upcasting a pozdní vazba v podtypu polymorfismu
class Shapes {public static void main (String [] args) {Shape [] tvary = {nový kruh (10, 20, 30), nový obdélník (20, 30, 40, 50)}; pro (int i = 0; i <tvary.délka; i ++) tvary [i] .draw (); }}
Prohlášení tvary
pole ukazuje upcasting. The Kruh
a Obdélník
odkazy jsou uloženy v tvary [0]
a tvary [1]
a jsou upcast na psaní Tvar
. Každý z tvary [0]
a tvary [1]
je považován za Tvar
instance: tvary [0]
není považován za Kruh
; tvary [1]
není považován za Obdélník
.
Pozdní vazba je prokázána tvary [i] .draw ();
výraz. Když i
se rovná 0
, způsobí instrukce generovaná překladačem Kruh
je kreslit()
metoda, která se má volat. Když i
se rovná 1
však tato instrukce způsobí Obdélník
je kreslit()
metoda, která se má volat. To je podstata podtypu polymorfismu.
Za předpokladu, že všechny čtyři zdrojové soubory (Shapes.java
, Shape.java
, Obdélník.java
, a Circle.java
) jsou umístěny v aktuálním adresáři, zkompilujte je pomocí některého z následujících příkazových řádků:
javac * .java javac Shapes.java
Spusťte výslednou aplikaci:
java tvary
Měli byste dodržovat následující výstup:
Kreslící kruh (10, 20, 30) Kreslící obdélník (20, 30, 40, 50)
Abstraktní třídy a metody
Při navrhování hierarchií tříd zjistíte, že třídy blíže k horní části těchto hierarchií jsou obecnější než třídy, které jsou níže. Například a Vozidlo
superclass je obecnější než a Kamion
podtřída. Podobně, a Tvar
superclass je obecnější než a Kruh
nebo a Obdélník
podtřída.
Nemá smysl vytvořit instanci generické třídy. Koneckonců, co by a Vozidlo
popis objektu? Podobně jaký tvar představuje a Tvar
objekt? Spíše než kódovat prázdný kreslit()
metoda v Tvar
, můžeme zabránit volání této metody a vytvoření instance této třídy deklarováním obou entit jako abstraktních.
Java poskytuje abstraktní
vyhrazené slovo pro deklaraci třídy, kterou nelze vytvořit instancí. Kompilátor nahlásí chybu, když se pokusíte vytvořit instanci této třídy. abstraktní
se také používá k deklaraci metody bez těla. The kreslit()
metoda nepotřebuje tělo, protože nedokáže nakreslit abstraktní tvar. Výpis 3 ukazuje.
Výpis 3. Abstrahování třídy Shape a její metody draw ()
abstraktní třída Tvar {abstract void draw (); // je povinný středník}
Abstraktní upozornění
Při pokusu o deklaraci třídy hlásí kompilátor chybu abstraktní
a finále
. Například si stěžovatel stěžuje abstraktní závěrečná třída Tvar
protože abstraktní třídu nelze vytvořit instanci a konečnou třídu nelze rozšířit. Kompilátor také hlásí chybu, když deklarujete metodu abstraktní
ale nedeklarujte jeho třídu abstraktní
. Odstraňování abstraktní
z Tvar
záhlaví třídy v seznamu 3 by například vedlo k chybě. To by byla chyba, protože neabstrahující (konkrétní) třídu nelze vytvořit, pokud obsahuje abstraktní metodu. Nakonec, když rozšíříte abstraktní třídu, rozšiřující třída musí přepsat všechny abstraktní metody, jinak musí být samotná rozšiřující třída deklarována jako abstraktní; jinak kompilátor nahlásí chybu.
Abstraktní třída může deklarovat pole, konstruktory a jiné než abstraktní metody kromě nebo místo abstraktních metod. Například abstrakt Vozidlo
třída může deklarovat pole popisující její značku, model a rok. Může také deklarovat konstruktor, který inicializuje tato pole a konkrétní metody, aby vrátil jejich hodnoty. Podívejte se na výpis 4.
Výpis 4. Abstrahování vozidla
abstraktní třída Vehicle {private String make, model; soukromý int rok; Vehicle (String make, String model, int year) {this.make = make; this.model = model; this.year = rok; } String getMake () {return make; } String getModel () {návratový model; } int getYear () {rok návratu; } abstract void move (); }
To si všimnete Vozidlo
deklaruje abstrakt hýbat se()
metoda k popisu pohybu vozidla. Například auto se valí po silnici, loď pluje po vodě a letadlo letí vzduchem. Vozidlo
Podtřídy by byly přepsány hýbat se()
a poskytnout odpovídající popis. Také by zdědili metody a jejich konstruktéři by volali Vozidlo
konstruktor.
Downcasting a RTTI
Přesunutí hierarchie tříd pomocí upcastingu znamená ztrátu přístupu k funkcím podtypu. Například přiřazení a Kruh
vznést námitku Tvar
proměnná s
znamená, že nemůžete použít s
zavolat Kruh
je getRadius ()
metoda. Je však možné znovu získat přístup Kruh
je getRadius ()
metoda provedením operace explicitního obsazení jako tento: Kruh c = (Kruh) s;
.
Tento úkol je znám jako vylidňování protože sesouváte hierarchii dědičnosti ze supertypu na podtyp (z Tvar
nadtřída do Kruh
podtřída). Ačkoli upcast je vždy bezpečný (rozhraní nadtřídy je podmnožinou rozhraní podtřídy), downcast není vždy bezpečný. Výpis 5 ukazuje, jaké potíže by mohly nastat, pokud použijete nesprávné seskupování.
Výpis 5. Problém s downcastingem
class Superclass {} class Subclass extends Superclass {void method () {}} public class BadDowncast {public static void main (String [] args) {Superclass superclass = new Superclass (); Podtřída podtřída = (Podtřída) nadtřída; subclass.method (); }}
Výpis 5 představuje hierarchii tříd skládající se z Nadtřída
a Podtřída
, který se rozšiřuje Nadtřída
. Dále Podtřída
prohlašuje metoda()
. Třetí třída s názvem BadDowncast
poskytuje a hlavní()
metoda, která vytváří instanci Nadtřída
. BadDowncast
pak se pokusí tento objekt seskočit na Podtřída
a přiřadit výsledek proměnné podtřída
.
V tomto případě si kompilátor nebude stěžovat, protože downcasting z nadtřídy do podtřídy ve stejné hierarchii typů je legální. To znamená, že pokud bylo přiřazení povoleno, aplikace by se při pokusu o spuštění zhroutila subclass.method ();
. V tomto případě by se JVM pokoušelo zavolat neexistující metodu, protože Nadtřída
nedeklaruje metoda()
. Naštěstí JVM před provedením operace obsazení ověří, zda je obsazení legální. Zjistit to Nadtřída
nedeklaruje metoda()
, to by hodilo ClassCastException
objekt. (O výjimkách pojednám v budoucím článku.)
Sestavte výpis 5 následujícím způsobem:
javac BadDowncast.java
Spusťte výslednou aplikaci:
java BadDowncast