Programiranje

Razkrijte čarobnost polimorfizma podtipa

Beseda polimorfizem prihaja iz grščine za "številne oblike". Večina razvijalcev Java izraz povezuje s sposobnostjo predmeta, da čarobno izvaja pravilno vedenje metode na ustreznih točkah programa. Vendar ta pogled, usmerjen k izvedbi, prej privede do podob čarovništva in ne do razumevanja temeljnih konceptov.

Polimorfizem v Javi je vedno podtip polimorfizma. Če natančno preučimo mehanizme, ki ustvarjajo to raznolikost polimorfnega vedenja, moramo zavreči običajne pomisleke glede izvedbe in razmišljati v smislu vrste. Ta članek raziskuje tipsko usmerjeno perspektivo predmetov in kako se ta perspektiva ločuje kaj vedenje, iz katerega lahko objekt izraža kako predmet dejansko izraža to vedenje. Z osvoboditvijo našega koncepta polimorfizma iz hierarhije izvajanja odkrivamo tudi, kako vmesniki Java olajšajo polimorfno vedenje v skupinah predmetov, ki nimajo skupne implementacijske kode.

Quattro polimorfi

Polimorfizem je širok objektno usmerjen izraz. Čeprav običajno enačimo splošni koncept s sorto podtipa, v resnici obstajajo štiri različne vrste polimorfizma. Preden podrobno preučimo podtip polimorfizma, je v nadaljevanju predstavljen splošen pregled polimorfizma v objektno usmerjenih jezikih.

Luca Cardelli in Peter Wegner, avtorja knjige "O razumevanju tipov, abstrakcije podatkov in polimorfizma" (glejte Vire za povezavo do članka) polimorfizem delita v dve glavni kategoriji - ad hoc in univerzalno - in štiri sorte: prisila, preobremenitev, parametrično in vključitev. Klasifikacijska struktura je:

 | - prisila | - ad hoc - | | - preobremenjujoči polimorfizem - | | - parametrični | - univerzalni - | | - vključitev 

V tej splošni shemi polimorfizem predstavlja sposobnost entitete, da ima več oblik. Univerzalni polimorfizem se nanaša na enotnost strukture tipov, pri kateri polimorfizem deluje nad neskončnim številom tipov, ki imajo skupno značilnost. Manj strukturirani ad hoc polimorfizem deluje nad končnim številom morda nepovezanih vrst. Štiri sorte lahko opišemo kot:

  • Prisila: ena abstrakcija služi več vrst z implicitno pretvorbo tipa
  • Preobremenitev: en identifikator označuje več abstrakcij
  • Parametrično: abstrakcija deluje enakomerno pri različnih vrstah
  • Vključitev: abstrakcija deluje prek razmerja vključenosti

Na kratko bom razpravljal o vsaki sorti, preden se bom posebej posvetil polimorfizmu podtipa.

Prisila

Prisila predstavlja implicitno pretvorbo tipa parametra v tip, ki ga pričakuje metoda ali operater, s čimer se izognemo napakam tipa. Za naslednje izraze mora prevajalnik določiti, ali je primeren binarni + operator obstaja za vrste operandov:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Prvi izraz doda dva dvojno operandi; jezik Java posebej določa takega operaterja.

Vendar drugi izraz doda a dvojno in an int; Java ne opredeli operaterja, ki bi sprejel te vrste operandov. Na srečo prevajalnik implicitno pretvori drugi operand v dvojno in uporablja operator, definiran za dva dvojno operandi. To je izjemno priročno za razvijalca; brez implicitne pretvorbe bi prišlo do napake v času prevajanja ali pa bi moral programer eksplicitno oddati datoteko int do dvojno.

Tretji izraz doda a dvojno in a Vrvica. Še enkrat, jezik Java ne določa takega operaterja. Torej prevajalnik prisili dvojno operand v a Vrvica, in operator plus izvede združevanje nizov.

Prisila se pojavi tudi pri priklicu metode. Recimo razred Izpeljano podaljša razred Osnova, in razred C ima metodo s podpisom m (podnožje). Za klic metode v spodnji kodi prevajalnik implicitno pretvori izpeljana referenčna spremenljivka, ki ima tip Izpeljano, do Osnova vrsta, predpisana s podpisom metode. Ta implicitna pretvorba omogoča m (podnožje) koda izvedbene metode za uporabo samo operacij tipa, ki jih določa Osnova:

 C c = novo C (); Izpeljano izpeljano = novo Izvedeno (); c.m (pridobljeno); 

Spet implicitna prisila med priklicem metode odpravi okoren tip ali nepotrebno napako pri prevajanju. Seveda prevajalnik še vedno preveri, ali so vse pretvorbe tipov skladne z definirano hierarhijo tipov.

Preobremenitev

Preobremenitev dovoljuje uporabo istega imena operaterja ali metode za označevanje več različnih ločenih pomenov programa. The + operator, uporabljen v prejšnjem oddelku, je razstavil dve obliki: eno za dodajanje dvojno operandi, enega za združevanje Vrvica predmetov. Obstajajo tudi drugi obrazci za dodajanje dveh celih števil, dveh dolžin itd. Pokličemo operaterja preobremenjeni in se zanašajte na prevajalnik, da izbere ustrezno funkcionalnost glede na programski kontekst. Kot smo že omenili, prevajalnik po potrebi implicitno pretvori vrste operanda, da se ujemajo z natančnim podpisom operaterja. Čeprav Java določa nekatere preobremenjene operaterje, ne podpira uporabniško definiranega preobremenitve operaterjev.

Java dovoljuje uporabniško določeno preobremenitev imen metod. Razred ima lahko več metod z istim imenom, če so podpisi metode različni. To pomeni, da se mora število parametrov razlikovati ali pa mora imeti vsaj en položaj parametra drugačen tip. Edinstveni podpisi omogočajo prevajalniku, da razlikuje med metodami z istim imenom. Prevajalnik imena metod pokvari z uporabo edinstvenih podpisov in tako ustvari edinstvena imena. Glede na to vsako navidezno polimorfno vedenje ob natančnejšem pregledu izhlapi.

Prisila in preobremenitev sta razvrščena kot priložnostna, ker vsaka zagotavlja polimorfno vedenje le v omejenem smislu. Čeprav spadajo pod široko definicijo polimorfizma, so te sorte predvsem ugodnosti za razvijalce. Prisila odpravi okorne eksplicitne odlitke vrst ali nepotrebne napake tipa prevajalnika. Preobremenitev pa zagotavlja skladenjski sladkor, ki razvijalcu omogoča, da isto ime uporablja za različne metode.

Parametrično

Parametrični polimorfizem omogoča uporabo ene ali več abstrakcij. Na primer, a Seznam abstrakcijo, ki predstavlja seznam homogenih predmetov, bi lahko zagotovili kot generični modul. Abstrakcijo bi ponovno uporabili tako, da bi določili vrste predmetov na seznamu. Ker je lahko parametrizirani tip kateri koli uporabniško določen podatkovni tip, je generične abstrakcije potencialno neskončno veliko, kar je verjetno najmočnejša vrsta polimorfizma.

Na prvi pogled zgoraj Seznam zdi se, da je abstrakcija koristnost razreda java.util.List. Vendar Java ne podpira resničnega parametričnega polimorfizma na varen način, zato java.util.List in java.utildrugi razredi zbirke so napisani v smislu prvotnega razreda Java, java.lang.Object. (Za več podrobnosti glejte moj članek »Primordialni vmesnik?«.) Enokoreninsko dedovanje implementacije Java ponuja delno rešitev, ne pa tudi resnično moč parametričnega polimorfizma. Odličen članek Erica Allena "Glej moč parametričnega polimorfizma" opisuje potrebo po generičnih tipih v Javi in ​​predloge za obravnavo zahteve za specifikacijo Java's Java št. 000014, "Dodajanje generičnih tipov v programski jezik Java." (Za povezavo glejte Vire.)

Vključenost

Inkluzijski polimorfizem doseže polimorfno vedenje z vključevalno zvezo med vrstami ali sklopi vrednosti. Za številne objektno usmerjene jezike, vključno z Javo, je vključitvena relacija podtip relacije. V Javi je torej vključujoči polimorfizem podtip polimorfizma.

Kot smo že omenili, ko se razvijalci Java splošno nanašajo na polimorfizem, vedno pomenijo polimorfizem podtipa. Če želimo trdno razumeti moč podtipa polimorfizma, je treba mehanizme, ki ustvarjajo polimorfno vedenje, gledati s tipsko usmerjenega vidika. Preostanek tega članka natančno preučuje to perspektivo. Za kratkost in jasnost uporabljam izraz polimorfizem v pomenu podtipa polimorfizem.

Tip usmerjen pogled

Diagram razredov UML na sliki 1 prikazuje preprosto hierarhijo vrst in razredov, ki se uporablja za ponazoritev mehanike polimorfizma. Model prikazuje pet vrst, štiri razrede in en vmesnik. Čeprav se model imenuje diagram razredov, ga smatram kot tipski diagram. Kot je podrobno opisano v poglavju "Hvala in nežen razred", vsak razred in vmesnik Java razglasi uporabniško določen podatkovni tip. Torej iz pogleda, neodvisnega od izvedbe (tj. Tipsko usmerjenega pogleda), vsak od petih pravokotnikov na sliki predstavlja vrsto. Z vidika izvedbe so štirje tipi definirani s pomočjo konstrukcij razredov, eden pa z vmesnikom.

Naslednja koda definira in izvaja vsak uporabniško določen podatkovni tip. Namero poenostavljam čim bolj preprosto:

/ * Base.java * / javni razred Base {javni niz m1 () {return "Base.m1 ()"; } public String m2 (String s) {return "Base.m2 (" + s + ")"; }} / * IType.java * / vmesnik IType {String m2 (String s); Niz m3 (); } / * Izvedeno.java * / javni razred Izvedeno razširja Base izvaja IType {javni niz m1 () {return "Izvedeno.m1 ()"; } javni niz m3 () {return "Izvedeno.m3 ()"; }} / * Izvedeno2.java * / javni razred Izvedeno2 razširja Izvedeno {javni String m2 (String s) {return "Izvedeno2.m2 (" + s + ")"; } javni niz m4 () {return "Izvedeno2.m4 ()"; }} / * Separate.java * / javni razred Ločeno implementira IType {javni niz m1 () {return "Separate.m1 ()"; } public String m2 (String s) {return "Ločeno.m2 (" + s + ")"; } public String m3 () {return "Ločeno.m3 ()"; }} 

Z uporabo teh deklaracij tipov in definicij razredov slika 2 prikazuje konceptualni pogled stavka Java:

Izvedeno2 izpeljano2 = novo Izvedeno2 (); 

Zgornja izjava izjavlja izrecno tipkano referenčno spremenljivko, izpeljana2, in sklic na novo ustvarjeno Izpeljano2 predmet razreda. Zgornja plošča na sliki 2 prikazuje Izpeljano2 sklic kot nabor lukenj, skozi katere je spodaj Izpeljano2 predmet si lahko ogledate. Za vsakega je ena luknja Izpeljano2 tip operacije. Dejansko Izpeljano2 objekt preslika vsak Izpeljano2 operacija do ustrezne izvedbene kode, kot to predpisuje hierarhija izvajanja, opredeljena v zgornji kodi. Na primer Izpeljano2 zemljevidi objektov m1 () k izvedbeni kodi, definirani v razredu Izpeljano. Poleg tega ta izvedbena koda preglasi m1 () metoda pri pouku Osnova. A Izpeljano2 referenčna spremenljivka ne more dostopati do razveljavljene m1 () izvajanje pri pouku Osnova. To ne pomeni, da je dejanska izvedbena koda v razredu Izpeljano ne morem uporabiti Osnova izvajanje razreda prek super.m1 (). Kar pa zadeva referenčno spremenljivko izpeljana2 je zaskrbljen, da je ta koda nedostopna. Preslikave drugega Izpeljano2 operacije podobno prikazujejo izvedbeno kodo, izvedeno za vsako vrsto operacije.

Zdaj, ko imate Izpeljano2 predmeta, se lahko sklicujete na katero koli spremenljivko, ki ustreza tipu Izpeljano2. To razkriva hierarhija tipov v diagramu UML na sliki 1 Izpeljano, Osnova, in IType so vse super vrste Izpeljano2. Tako je na primer a Osnova sklic je lahko pritrjen na predmet. Slika 3 prikazuje konceptualni pogled naslednje izjave Java:

Osnovna osnova = izpeljana2; 

V osnovi ni nobene spremembe Izpeljano2 objekt ali katero koli preslikavo operacije, čeprav metode m3 () in m4 () niso več dostopni prek Osnova sklic. Klicanje m1 () ali m2 (niz) z uporabo katere koli spremenljivke izpeljana2 ali osnova povzroči izvedbo iste izvedbene kode:

Niz tmp; // Izpeljana referenca2 (slika 2) tmp = izvedena2.m1 (); // tmp je "Izpeljano.m1 ()" tmp = izpeljano2.m2 ("Pozdravljeni"); // tmp je "Izpeljano2.m2 (Pozdravljeni)" // Osnovna referenca (slika 3) tmp = base.m1 (); // tmp je "Izpeljano.m1 ()" tmp = base.m2 ("Pozdravljeni"); // tmp je "Izpeljano2.m2 (Pozdravljeni)" 

Uresničevanje enakega vedenja prek obeh referenc je smiselno, ker Izpeljano2 objekt ne ve, kaj kliče posamezna metoda. Predmet ve samo, da ob klicu sledi zaporedjem pohodov, ki jih definira hierarhija izvajanja. Ti ukazi to določajo za metodo m1 (), Izpeljano2 objekt izvede kodo v razredu Izpeljano, in za metodo m2 (niz), izvrši kodo v razredu Izpeljano2. Dejanje osnovnega predmeta ni odvisno od vrste referenčne spremenljivke.

Vendar pri uporabi referenčnih spremenljivk ni vse enako izpeljana2 in osnova. Kot je prikazano na sliki 3, a Osnova sklic na tip lahko vidi samo Osnova tipske operacije osnovnega predmeta. Torej čeprav Izpeljano2 ima preslikave za metode m3 () in m4 (), spremenljivka osnova ne morem dostopati do teh metod:

Niz tmp; // Izpeljana referenca2 (slika 2) tmp = izvedena2.m3 (); // tmp je "Izpeljano.m3 ()" tmp = izpeljano2.m4 (); // tmp je "Izpeljano2.m4 ()" // Osnovna referenca (slika 3) tmp = base.m3 (); // Napaka časa prevajanja tmp = base.m4 (); // Napaka v času prevajanja 

Izvajanje

Izpeljano2

objekt ostane popolnoma sposoben sprejeti bodisi

m3 ()

ali

m4 ()

klici metode. Omejitve vrste, ki onemogočajo poskuse klicev prek

Osnova

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