Ohjelmointi

Paljasta alatyypin polymorfismin taika

Sana polymorfismi tulee kreikan kielestä "monissa muodoissa". Suurin osa Java-kehittäjistä yhdistää termin kohteen kykyyn suorittaa maagisesti oikea menetelmäkäyttäytyminen ohjelman sopivissa kohdissa. Tuo toteutuslähtöinen näkemys johtaa kuitenkin mielikuvituksen kuviin pikemminkin kuin peruskäsitteiden ymmärtämiseen.

Java-polymorfismi on poikkeuksetta alatyyppinen polymorfismi. Polymorfisen käyttäytymisen monimuotoisuuden synnyttävien mekanismien läheinen tutkiminen edellyttää, että hylkäämme tavanomaiset toteutusongelmamme ja ajattelemme tyypin suhteen. Tässä artikkelissa tutkitaan objektien tyyppisuuntautunutta perspektiiviä ja sitä, miten perspektiivi erottuu mitä käyttäytyminen, josta esine voi ilmaista Miten esine itse asiassa ilmaisee kyseisen käyttäytymisen. Vapauttamalla käsitteemme polymorfismista toteutushierarkiasta löydämme myös, kuinka Java-rajapinnat helpottavat polymorfista käyttäytymistä objektiryhmissä, joilla ei ole lainkaan toteutuskoodia.

Quattro polymorphi

Polymorfismi on laaja olio-termi. Vaikka yleensä verrataan yleinen käsite alatyyppilajikkeeseen, on tosiasiassa neljä erilaista polymorfismia. Ennen kuin tutkimme alatyypin polymorfismia yksityiskohtaisesti, seuraavassa osassa esitetään yleiskatsaus polymorfismista olio-orientoiduilla kielillä.

Luca Cardelli ja Peter Wegner, kirjoittajat "On Understanding Type, Data Abstraction, and Polymorphism" (katso lähteet artikkeliin), jakavat polymorfismin kahteen pääryhmään - ad hoc ja universal - ja neljään lajikkeeseen: pakko, ylikuormitus, parametri ja inkluusio. Luokittelurakenne on:

 | - pakko | - ad hoc - | | - ylikuormittava polymorfismi - | | - parametrinen | - universaali - | | - sisällyttäminen 

Tässä yleisessä kaaviossa polymorfismi edustaa yksikön kykyä olla useita muotoja. Universaali polymorfismi viittaa tyyppirakenteen yhtenäisyyteen, jossa polymorfismi vaikuttaa loputtomasti tyyppeihin, joilla on yhteinen piirre. Vähemmän jäsennelty ad hoc -polymorfismi toimii rajallisessa määrin mahdollisesti etuyhteydettömiä tyyppejä. Neljä lajiketta voidaan kuvata seuraavasti:

  • Pakko: yksi abstraktio palvelee useita tyyppejä implisiittisen tyyppimuunnoksen avulla
  • Ylikuormitus: yksi tunniste tarkoittaa useita abstrakteja
  • Parametri: abstraktio toimii tasaisesti eri tyyppien välillä
  • Sisältö: abstraktio toimii inkluusiosuhteen kautta

Keskustelen lyhyesti jokaisesta lajikkeesta ennen kuin palaan nimenomaan alatyypin polymorfismiin.

Pakko

Pakko tarkoittaa implisiittistä parametrityypin muuntamista menetelmän tai operaattorin odottamaan tyyppiin välttäen siten tyypin virheitä. Seuraavien lausekkeiden osalta kääntäjän on määritettävä, onko asianmukainen binaarinen + operaattori on olemassa operandityypeille:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Ensimmäinen lauseke lisää kaksi kaksinkertainen operandit; Java-kieli määrittelee nimenomaisesti tällaisen operaattorin.

Toinen lauseke lisää kuitenkin a kaksinkertainen ja int; Java ei määritä operaattoria, joka hyväksyy nämä operandityypit. Onneksi kääntäjä muuntaa implisiittisesti toisen operandin kaksinkertainen ja käyttää kahdelle määritettyä operaattoria kaksinkertainen operandit. Se on erittäin kätevää kehittäjälle; ilman implisiittistä muunnosta seurauksena olisi käännösaika-virhe tai ohjelmoijan olisi nimenomaisesti heitettävä se int että kaksinkertainen.

Kolmas lauseke lisää a kaksinkertainen ja a Merkkijono. Jälleen kerran Java-kieli ei määritä tällaista operaattoria. Joten kääntäjä pakottaa kaksinkertainen operandi a Merkkijonoja plus-operaattori suorittaa merkkijonon ketjutuksen.

Pakkoa esiintyy myös menetelmän kutsumisessa. Oletetaan, että luokka Johdettu pidentää luokkaa Pohjaja luokka C on menetelmä allekirjoituksella m (pohja). Kääntäjä muuntaa implisiittisesti alla olevan koodin menetelmäkutsu johdettu viitemuuttuja, jolla on tyyppi Johdettu, Pohja menetelmän allekirjoituksella määrätty tyyppi. Tämä implisiittinen muunnos sallii m (pohja) Methodin toteutuskoodi käyttää vain määrittelemiä tyyppitoimintoja Pohja:

 C c = uusi C (); Johdettu johdettu = uusi Johdettu (); c.m (johdettu); 

Jälleen implisiittinen pakottaminen menetelmän kutsun aikana eliminoi hankalan tyyppikokoelman tai tarpeettoman kääntöaikavirheen. Tietenkin kääntäjä tarkistaa edelleen, että kaikki tyyppimuunnokset ovat määritetyn tyyppihierarkian mukaisia.

Ylikuormitus

Ylikuormitus sallii saman operaattorin tai menetelmän nimen käytön merkitsemään useita, erillisiä ohjelman merkityksiä. + edellisessä osassa käytetyllä operaattorilla oli kaksi muotoa: yksi lisättäväksi kaksinkertainen operandit, yksi ketjutettavaksi Merkkijono esineitä. Muita muotoja on olemassa kahden kokonaisluvun, kahden pitkän ja niin edelleen lisäämiseen. Soitamme operaattorille ylikuormitettu ja luottaa kääntäjään valitsemaan sopivat toiminnot ohjelman kontekstin perusteella. Kuten aiemmin todettiin, kääntäjä muuntaa implisiittisesti operandityypit vastaamaan operaattorin tarkkaa allekirjoitusta. Vaikka Java määrittelee tietyt ylikuormitetut operaattorit, se ei tue käyttäjän määrittelemää operaattoreiden ylikuormitusta.

Java sallii menetelmien nimien käyttäjän määrittelemän ylikuormituksen. Luokalla voi olla useita menetelmiä samalla nimellä, edellyttäen että menetelmän allekirjoitukset ovat erilliset. Tämä tarkoittaa, että joko parametrien lukumäärän on oltava erilainen tai ainakin yhdellä parametrin sijainnilla on oltava erilainen tyyppi. Yksilöllisten allekirjoitusten avulla kääntäjä voi erottaa menetelmät, joilla on sama nimi. Kääntäjä sekoittaa menetelmän nimet yksilöllisten allekirjoitusten avulla ja luo tehokkaasti yksilöllisiä nimiä. Tämän valossa mikä tahansa näennäinen polymorfinen käyttäytyminen haihtuu tarkemman tarkastelun yhteydessä.

Sekä pakottaminen että ylikuormitus luokitellaan tapauskohtaisiksi, koska kumpikin tarjoaa polymorfisen käyttäytymisen vain rajoitetussa mielessä. Vaikka nämä lajikkeet kuuluvat polymorfismin laajan määritelmän piiriin, nämä lajikkeet ovat ensisijaisesti kehittäjien mukavuuksia. Pakottaminen estää hankalat eksplisiittiset tyyppikomennot tai tarpeettomat kääntäjätyyppivirheet. Toisaalta ylikuormitus tarjoaa syntaktisen sokerin, jolloin kehittäjä voi käyttää samaa nimeä erillisissä menetelmissä.

Parametrinen

Parametrinen polymorfismi sallii yhden abstraktin käytön useissa tyypeissä. Esimerkiksi a Lista abstraktio, joka edustaa luetteloa homogeenisista kohteista, voidaan tarjota yleisenä moduulina. Voit käyttää abstraktia uudelleen määrittämällä luettelossa olevien objektien tyypit. Koska parametrisoitu tyyppi voi olla mikä tahansa käyttäjän määrittämä tietotyyppi, geneeriselle abstraktille on mahdollisesti ääretön määrä käyttötapoja, mikä tekee tästä väitetysti tehokkaimman polymorfismin tyypin.

Ensi silmäyksellä edellä Lista abstraktio voi tuntua olevan luokan hyöty java.util.List. Java ei kuitenkaan tue todellista parametrista polymorfismia tyyppiturvallisella tavalla, minkä vuoksi java.util.List ja java.utilmuut kokoelmaluokat on kirjoitettu Java-alkuluokan mukaan, java.lang.objekti. (Katso lisätietoja artikkelistani "Alkuperäinen käyttöliittymä?".) Javan yhden juuren toteutusperintö tarjoaa osittaisen ratkaisun, mutta ei parametrisen polymorfismin todellista voimaa. Eric Allenin erinomainen artikkeli "Katso parametrisen polymorfismin voima" kuvaa yleisten tyyppien tarvetta Java-ohjelmassa ja ehdotuksia Sunin Java-määrityspyynnön # 000014 "Lisää yleisiä tyyppejä Java-ohjelmointikieleen" vastaamiseksi. (Katso linkki Resursseista.)

Osallisuus

Inkluusiopolymorfismi saavuttaa polymorfisen käyttäytymisen tyypin tai arvojoukon välisen inkluusiosuhteen kautta. Monille olio-kielille, Java mukaan lukien, sisällytyssuhde on alatyyppisuhde. Joten Javassa inkluusiopolymorfismi on alatyyppipolymorfismi.

Kuten aiemmin todettiin, kun Java-kehittäjät viittaavat yleisesti polymorfismiin, ne tarkoittavat poikkeuksetta alatyyppistä polymorfismia. Alatyyppisen polymorfismin voiman vakaan arvostuksen saaminen edellyttää polymorfisen käyttäytymisen aikaansaavien mekanismien tarkastelua tyyppisuuntautuneesta näkökulmasta. Tämän artikkelin loppuosassa tarkastellaan tätä näkökulmaa tarkasti. Lyhyyden ja selkeyden vuoksi käytän termiä polymorfismi tarkoittamaan alatyyppistä polymorfismia.

Tyyppikohtainen näkymä

Kuvan 1 UML-luokkakaavio näyttää yksinkertaisen tyypin ja luokkahierarkian, jota käytetään havainnollistamaan polymorfismin mekaniikkaa. Malli kuvaa viittä tyyppiä, neljä luokkaa ja yhden käyttöliittymän. Vaikka mallia kutsutaan luokkakaavaksi, ajattelen sitä tyyppikaaviona. Kuten "Kiitotyyppi ja lempeä luokka" -kohdassa kerrotaan, jokainen Java-luokka ja käyttöliittymä ilmoittaa käyttäjän määrittelemän tietotyypin. Joten toteutuksesta riippumattomasta näkökulmasta (ts. Tyyppikeskeisestä näkymästä) kukin kuvion viidestä suorakulmiosta edustaa tyyppiä. Toteutuksen näkökulmasta neljä näistä tyyppeistä määritellään luokkarakenteilla ja yksi rajapinnalla.

Seuraava koodi määrittelee ja toteuttaa jokaisen käyttäjän määrittelemän tietotyypin. Pidän tarkoituksella toteutuksen mahdollisimman yksinkertaisena:

/ * Base.java * / public class Base {public String m1 () {return "Base.m1 ()"; } julkinen merkkijono m2 (merkkijono s) {return "Base.m2 (" + s + ")"; }} / * IType.java * / käyttöliittymä IType {String m2 (String s); Merkkijono m3 (); } / * Derived.java * / public class Derived extends Base toteuttaa IType {public String m1 () {return "Johdettu.m1 ()"; } public String m3 () {return "Johdettu.m3 ()"; }} / * Johdettu2.java * / julkinen luokka Johdettu2 jatkuu Johdettu {julkinen merkkijono m2 (String s) {return "Johdettu2.m2 (" + s + ")"; } public String m4 () {return "Johdettu2.m4 ()"; }} / * Separate.java * / public class Separate toteuttaa IType {public String m1 () {return "Separate.m1 ()"; } public String m2 (String s) {return "Erota.m2 (" + s + ")"; } public String m3 () {return "Separate.m3 ()"; }} 

Näitä tyyppideklaraatioita ja luokan määritelmiä käyttämällä kuva 2 kuvaa Java-käskyn käsitteellisen kuvan:

Johdettu2 johdettu2 = uusi Johdettu2 (); 

Yllä oleva lause julistaa nimenomaisesti kirjoitetun viitemuuttujan, johdettu 2ja liittää viittauksen uuteen luotuun Johdettu 2 luokan esine. Kuvan 2 yläpaneeli kuvaa Johdettu 2 viittaus joukkoina valoaukkoina, joiden kautta taustalla oleva Johdettu 2 esine voidaan tarkastella. Jokaiselle on yksi reikä Johdettu 2 tyypin toiminta. Todellinen Johdettu 2 objekti kartoittaa kukin Johdettu 2 toiminta asianmukaiseen toteutuskoodiin, kuten yllä olevassa koodissa määritelty toteutushierarkia edellyttää. Esimerkiksi Johdettu 2 objektikartat m1 () luokassa määriteltyyn toteutuskoodiin Johdettu. Lisäksi tämä toteutuskoodi ohittaa m1 () menetelmä luokassa Pohja. A Johdettu 2 viitemuuttuja ei voi käyttää ohitettua m1 () toteutus luokassa Pohja. Tämä ei tarkoita, että todellinen toteutuskoodi luokassa Johdettu ei voi käyttää Pohja luokan toteutus kautta super.m1 (). Mutta niin pitkälle kuin viitemuuttuja johdettu 2 kyseiseen koodiin ei pääse. Toisen kuvaukset Johdettu 2 toiminnot näyttävät samalla tavoin kullekin tyyppitoiminnolle suoritetun toteutuskoodin.

Nyt kun sinulla on Johdettu 2 objekti, voit viitata siihen millä tahansa tyypin mukaisella muuttujalla Johdettu 2. Kuvan 1 UML-kaavion tyyppihierarkia paljastaa sen Johdettu, Pohjaja ITyyppi ovat kaikki supertyyppejä Johdettu 2. Joten esimerkiksi a Pohja viite voidaan liittää esineeseen. Kuva 3 kuvaa seuraavan Java-käskyn käsitteellisen kuvan:

Pohjapohja = johdettu2; 

Taustalla ei ole mitään muutosta Johdettu 2 esine tai mikä tahansa toimintokartoitus, vaikka menetelmiä m3 () ja m4 () eivät ole enää käytettävissä Pohja viite. Kutsumus m1 () tai m2 (merkkijono) käyttämällä kumpaakin muuttujaa johdettu 2 tai pohja johtaa saman toteutuskoodin suorittamiseen:

Merkkijono tmp; // Johdettu2-viite (kuva 2) tmp = johdettu2.m1 (); // tmp on "johdettu.m1 ()" tmp = johdettu2.m2 ("Hei"); // tmp on "johdettu2.m2 (Hei)" // Perusviite (kuva 3) tmp = pohja.m1 (); // tmp on "Johdettu.m1 ()" tmp = pohja.m2 ("Hei"); // tmp on "johdettu2.m2 (Hei)" 

Identtisen käyttäytymisen toteuttaminen molempien viitteiden avulla on järkevää, koska Johdettu 2 objekti ei tiedä kutsuu kutakin menetelmää. Kohde tietää vain, että kun se kutsutaan, se noudattaa toteutushierarkian määrittelemiä marssijärjestyksiä. Näissä tilauksissa määrätään, että menetelmä m1 (), Johdettu 2 object suorittaa koodin luokassa Johdettuja menetelmä m2 (merkkijono), se suorittaa koodin luokassa Johdettu 2. Taustalla olevan objektin suorittama toiminto ei riipu viitemuuttujan tyypistä.

Kaikki eivät kuitenkaan ole yhtä suuria käytettäessä viitemuuttujia johdettu 2 ja pohja. Kuten kuvassa 3 on esitetty, a Pohja tyyppiviittaus voi nähdä vain Pohja tyypin operaatiot taustalla olevan objektin. Joten vaikka Johdettu 2 on kartoituksia menetelmille m3 () ja m4 (), muuttuja pohja ei voi käyttää näitä menetelmiä:

Merkkijono tmp; // Johdettu2-viite (kuva 2) tmp = johdettu2.m3 (); // tmp on "johdettu.m3 ()" tmp = johdettu2.m4 (); // tmp on "johdettu2.m4 ()" // Perusviite (kuva 3) tmp = pohja.m3 (); // Kääntöaikavirhe tmp = base.m4 (); // Kääntöajan virhe 

Ajonaika

Johdettu 2

esine pystyy täysin hyväksymään joko

m3 ()

tai

m4 ()

menetelmäpuhelut. Tyyppirajoitukset, jotka estävät soittoyritykset

Pohja