Programování

Odhalte kouzlo podtypu polymorfismu

Slovo polymorfismus pochází z řečtiny pro „mnoho forem“. Většina vývojářů Java spojuje tento výraz se schopností objektu magicky provádět správné chování metody ve vhodných bodech programu. Tento pohled orientovaný na implementaci však vede spíše k představám o čarodějnictví než k pochopení základních pojmů.

Polymorfismus v Javě je vždy podtypem polymorfismu. Pečlivé prozkoumání mechanismů, které generují tuto rozmanitost polymorfního chování, vyžaduje, abychom odhodili naše obvyklé obavy z implementace a přemýšleli z hlediska typu. Tento článek zkoumá typově orientovanou perspektivu objektů a to, jak se tato perspektiva odděluje co chování, ze kterého může objekt vyjádřit jak objekt toto chování ve skutečnosti vyjadřuje. Uvolněním našeho konceptu polymorfismu z hierarchie implementace také zjistíme, jak rozhraní Java usnadňují polymorfní chování napříč skupinami objektů, které vůbec nesdílejí žádný implementační kód.

Quattro polymorphi

Polymorfismus je široký objektově orientovaný pojem. Ačkoli obvykle pojmeme obecný koncept s odrůdou podtypu, ve skutečnosti existují čtyři různé druhy polymorfismu. Před podrobným prozkoumáním podtypu polymorfismu uvádíme v následující části obecný přehled polymorfismu v objektově orientovaných jazycích.

Luca Cardelli a Peter Wegner, autoři publikace „O porozumění typům, abstrakci dat a polymorfismu“ (viz Zdroje pro odkaz na článek) dělí polymorfismus na dvě hlavní kategorie - ad hoc a univerzální - a čtyři varianty: nátlak, přetížení, parametrické a zahrnutí. Struktura klasifikace je:

 | - nátlak | - ad hoc - | | - přetížení polymorfismu - | | - parametrické | - univerzální - | | - zařazení 

V tomto obecném schématu představuje polymorfismus schopnost entity mít více forem. Univerzální polymorfismus odkazuje na uniformitu typové struktury, ve které polymorfismus působí na nekonečné množství typů, které mají společný rys. Méně strukturované ad hoc polymorfismus působí nad konečným počtem možná nesouvisejících typů. Tyto čtyři odrůdy lze popsat jako:

  • Donucování: jedna abstrakce slouží několika typům prostřednictvím implicitní konverze typů
  • Přetížení: jediný identifikátor označuje několik abstrakcí
  • Parametrické: abstrakce funguje jednotně napříč různými typy
  • Zařazení: abstrakce funguje prostřednictvím inkluzního vztahu

Krátce budu diskutovat o každé odrůdě, než se obrátím konkrétně k podtypu polymorfismu.

Donucování

Coercion představuje implicitní převod typu parametru na typ očekávaný metodou nebo operátorem, čímž se zabrání chybám typu. Pro následující výrazy musí kompilátor určit, zda je vhodný binární soubor + existuje operátor pro typy operandů:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

První výraz přidá dva dvojnásobek operandy; jazyk Java konkrétně definuje takového operátora.

Druhý výraz však přidává a dvojnásobek a int; Java nedefinuje operátora, který přijímá tyto typy operandů. Naštěstí kompilátor implicitně převede druhý operand na dvojnásobek a používá operátor definovaný pro dva dvojnásobek operandy. To je pro vývojáře mimořádně výhodné; bez implicitní převodu by došlo k chybě při kompilaci nebo by programátor musel explicitně odevzdat int na dvojnásobek.

Třetí výraz přidává a dvojnásobek a Tětiva. Jazyk Java takového operátora ještě jednou nedefinuje. Překladač tedy vynucuje dvojnásobek operand na a Tětivaa operátor plus provádí zřetězení řetězců.

K nátlaku dochází také při vyvolání metody. Předpokládejme třídu Odvozený rozšiřuje třídu Základnaa třída C má metodu s podpisem m (základna). Pro vyvolání metody v níže uvedeném kódu kompilátor implicitně převede odvozený referenční proměnná, která má typ Odvozený, do Základna typ předepsaný podpisem metody. Tato implicitní konverze umožňuje m (základna) implementační kód metody pro použití pouze typových operací definovaných Základna:

 Cc = nový C (); Odvozený odvozený = nový odvozený (); c.m (odvozeno); 

Implicitní nátlak během vyvolání metody opět vylučuje těžkopádné přetypování typu nebo zbytečnou chybu při kompilaci. Kompilátor samozřejmě stále ověřuje, že všechny převody typů odpovídají hierarchii definovaného typu.

Přetížení

Přetížení umožňuje použití stejného názvu operátoru nebo metody k označení více odlišných významů programu. The + operátor použitý v předchozí části vykazoval dvě formy: jednu pro přidání dvojnásobek operandy, jeden pro zřetězení Tětiva předměty. Existují i ​​jiné formy pro přidání dvou celých čísel, dvou délek atd. Voláme operátora přetížený a spoléhat se na to, že kompilátor vybere příslušnou funkčnost na základě kontextu programu. Jak již bylo uvedeno dříve, v případě potřeby kompilátor implicitně převede typy operandů tak, aby odpovídaly přesnému podpisu operátoru. Ačkoli Java specifikuje určité přetížené operátory, nepodporuje uživatelem definované přetížení operátorů.

Java umožňuje uživatelem definované přetížení názvů metod. Třída může mít více metod se stejným názvem za předpokladu, že jsou signatury metody odlišné. To znamená, že buď počet parametrů se musí lišit, nebo alespoň jedna pozice parametru musí mít jiný typ. Jedinečné podpisy umožňují kompilátoru rozlišovat mezi metodami, které mají stejný název. Kompilátor upravuje názvy metod pomocí jedinečných podpisů a efektivně tak vytváří jedinečné názvy. Ve světle toho se jakékoli zjevné polymorfní chování po bližším zkoumání odpaří.

Nátlak i přetížení jsou klasifikovány jako ad hoc, protože každý poskytuje polymorfní chování pouze v omezeném smyslu. Ačkoli spadají pod širokou definici polymorfismu, jsou tyto odrůdy primárně vymoženostmi pro vývojáře. Donucování vylučuje těžkopádné obsazení explicitního typu nebo zbytečné chyby typu kompilátoru. Přetížení na druhé straně poskytuje syntaktický cukr, což vývojáři umožňuje použít stejný název pro odlišné metody.

Parametrické

Parametrický polymorfismus umožňuje použití jedné abstrakce napříč mnoha typy. Například a Seznam abstrakce představující seznam homogenních objektů by mohla být poskytnuta jako obecný modul. Abstrakci byste znovu použili zadáním typů objektů obsažených v seznamu. Protože parametrizovaným typem může být libovolný uživatelsky definovaný datový typ, existuje pro generickou abstrakci potenciálně nekonečné množství použití, což z něj činí pravděpodobně nejvýkonnější typ polymorfismu.

Na první pohled výše Seznam abstrakce se může jevit jako užitečnost třídy java.util.List. Java však nepodporuje skutečný parametrický polymorfismus způsobem bezpečným pro typ, a proto java.util.List a java.utilOstatní třídy kolekce jsou psány z hlediska prvotní třídy Java, java.lang.Object. (Další informace najdete v mém článku „Prvotní rozhraní?“) Dědičnost implementace Java s jedním kořenem nabízí částečné řešení, ale ne skutečnou moc parametrického polymorfismu. Vynikající článek Erica Allena „Behold the Power of Parametric Polymorphism“ popisuje potřebu generických typů v Javě a návrhy řešení Sun's Java Specification Request # 000014, „Add Generic Types to the Java Programming Language“. (Odkaz najdete v Zdrojích.)

Zařazení

Inklusivní polymorfismus dosahuje polymorfního chování prostřednictvím inkluzního vztahu mezi typy nebo sadami hodnot. Pro mnoho objektově orientovaných jazyků, včetně Javy, je relace inkluze relací podtypu. V Javě je tedy polymorfismus začlenění podtypem polymorfismu.

Jak již bylo zmíněno dříve, když vývojáři Java obecně odkazují na polymorfismus, znamenají vždy polymorfismus podtypu. Získání solidního zhodnocení síly podtypu polymorfismu vyžaduje prohlížení mechanismů poskytujících polymorfní chování z pohledu typu. Zbytek tohoto článku tuto perspektivu podrobně zkoumá. Pro stručnost a jasnost používám výraz polymorfismus ve smyslu podtypu polymorfismu.

Typově orientovaný pohled

Diagram tříd UML na obrázku 1 ukazuje jednoduchou hierarchii typů a tříd používanou k ilustraci mechaniky polymorfismu. Model zobrazuje pět typů, čtyři třídy a jedno rozhraní. Ačkoli se model nazývá třídní diagram, považuji to za typový diagram. Jak je podrobně popsáno v části „Díky typu a jemné třídě“, každá třída a rozhraní Java deklaruje uživatelem definovaný datový typ. Takže z pohledu nezávislého na implementaci (tj. Typově orientovaného pohledu) každý z pěti obdélníků na obrázku představuje typ. Z hlediska implementace jsou čtyři z těchto typů definovány pomocí konstrukcí třídy a jeden je definován pomocí rozhraní.

Následující kód definuje a implementuje každý uživatelem definovaný datový typ. Záměrně udržuji implementaci co nejjednodušší:

/ * 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); Řetězec 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 ()"; }} 

Pomocí těchto deklarací typu a definic tříd Obrázek 2 zobrazuje koncepční pohled na příkaz Java:

Odvozeno2 odvozeno2 = nové Odvozeno2 (); 

Výše uvedené prohlášení deklaruje explicitně zadanou referenční proměnnou, odvozený2a připojí tento odkaz k nově vytvořenému Odvozeno2 objekt třídy. Horní panel na obrázku 2 zobrazuje Odvozeno2 odkaz jako soubor světlíků, skrz které je podklad Odvozeno2 objekt lze zobrazit. Pro každého je jedna díra Odvozeno2 operace typu. Aktuální Odvozeno2 každý objekt mapuje Odvozeno2 operace do příslušného implementačního kódu, jak je předepsáno implementační hierarchií definovanou ve výše uvedeném kódu. Například Odvozeno2 mapy objektů m1 () k implementačnímu kódu definovanému ve třídě Odvozený. Kromě toho tento implementační kód přepíše m1 () metoda ve třídě Základna. A Odvozeno2 referenční proměnná nemá přístup k přepsané m1 () implementace ve třídě Základna. To neznamená, že skutečný implementační kód ve třídě Odvozený nelze použít Základna implementace třídy prostřednictvím super.m1 (). Ale pokud jde o referenční proměnnou odvozený2 je znepokojen tím, že tento kód je nepřístupný. Mapování druhého Odvozeno2 operace podobně ukazují implementační kód provedený pro každou operaci typu.

Teď, když máte Odvozeno2 objekt, můžete na něj odkazovat s libovolnou proměnnou, která odpovídá typu Odvozeno2. To ukazuje hierarchie typů v diagramu UML obrázku 1 Odvozený, Základna, a Píši jsou všechny super typy Odvozeno2. Takže například a Základna k objektu lze připojit odkaz. Obrázek 3 zobrazuje koncepční pohled na následující příkaz Java:

Base base = derived2; 

Absolutně nedochází k žádné změně podkladu Odvozeno2 objekt nebo kterékoli z mapování operací, ačkoli metody m3 () a m4 () již nejsou přístupné přes internet Základna odkaz. Povolání m1 () nebo m2 (řetězec) pomocí kterékoli proměnné odvozený2 nebo základna má za následek provedení stejného implementačního kódu:

Řetězec tmp; // Odvozená reference2 (obrázek 2) tmp = derived2.m1 (); // tmp je „Derived.m1 ()“ tmp = derived2.m2 („Hello“); // tmp je „Derived2.m2 (Hello)“ // Základní reference (obrázek 3) tmp = base.m1 (); // tmp je „Derived.m1 ()“ tmp = base.m2 („Hello“); // tmp je „Derived2.m2 (Hello)“ 

Realizace identického chování prostřednictvím obou odkazů má smysl, protože Odvozeno2 objekt neví, co volá každou metodu. Objekt ví jen to, že když je vyvolán, následuje pochodové příkazy definované hierarchií implementace. Tyto objednávky to stanoví pro metodu m1 ()Odvozeno2 objekt provede kód ve třídě Odvozenýa pro metodu m2 (řetězec), provede kód ve třídě Odvozeno2. Akce prováděná podkladovým objektem nezávisí na typu referenční proměnné.

Všechno však není stejné, když použijete referenční proměnné odvozený2 a základna. Jak je znázorněno na obrázku 3, a Základna odkaz na typ může vidět pouze Základna operace typu podkladového objektu. Takže ačkoli Odvozeno2 má mapování pro metody m3 () a m4 (), variabilní základna k těmto metodám nemám přístup:

Řetězec tmp; // Odvozená reference2 (obrázek 2) tmp = derived2.m3 (); // tmp je „Odvozeno.m3 ()“ tmp = odvozeno2.m4 (); // tmp je „Derived2.m4 ()“ // Základní reference (obrázek 3) tmp = base.m3 (); // Chyba při kompilaci tmp = base.m4 (); // Chyba při kompilaci 

Runtime

Odvozeno2

objekt zůstává plně schopný přijmout buď

m3 ()

nebo

m4 ()

volání metod. Omezení typu, která zakazují tyto pokusy o volání prostřednictvím

Základna