Ohjelmointi

Java-polymorfismi ja sen tyypit

Polymorfismi viittaa joidenkin entiteettien kykyyn esiintyä eri muodoissa. Sitä edustaa yleisesti perhonen, joka morfoi toukasta pupaan imagoon. Polymorfismi esiintyy myös ohjelmointikielissä mallintamistekniikkana, jonka avulla voit luoda yhden käyttöliittymän useille operandeille, argumenteille ja objekteille. Java-polymorfismi johtaa koodiin, joka on ytimekkäämpi ja helpompi ylläpitää.

Vaikka tämä opetusohjelma keskittyy alatyypin polymorfismiin, sinun on tiedettävä useita muita tyyppejä. Aloitamme yleiskatsauksella kaikista neljästä polymorfismin tyypistä.

lataa Hanki koodi Lataa lähdekoodi esimerkiksi sovelluksiin tässä opetusohjelmassa. Luonut Jeff Friesen JavaWorldille.

Javan polymorfismin tyypit

Javalassa on neljän tyyppistä polymorfismia:

  1. Pakko on operaatio, joka palvelee useita tyyppejä implisiittisen tyypin muunnoksen avulla. Voit esimerkiksi jakaa kokonaisluvun toisella kokonaisluvulla tai liukulukuarvon toisella liukulukuarvolla. Jos yksi operandi on kokonaisluku ja toinen operandi on liukuluku, kääntäjä pakotteet (muuntaa implisiittisesti) kokonaisluvun liukulukuarvoksi tyypin virheen estämiseksi. (Ei ole yhtään jakamisoperaatiota, joka tukisi kokonaislukuoperandia ja liukulukuoperandia.) Toinen esimerkki on aliluokan objektiviitteen välittäminen menetelmän superluokan parametriin. Kääntäjä pakottaa aliluokan tyypin yliluokkatyyppiin rajoittaakseen toiminnot yliluokan toimintoihin.
  2. Ylikuormitus viittaa saman operaattorisymbolin tai menetelmän nimen käyttämiseen eri tilanteissa. Voit esimerkiksi käyttää + suorittaa kokonaisluku-, liukuluku- tai merkkijono-ketjutus sen operandityypistä riippuen. Myös useita menetelmiä, joilla on sama nimi, voi esiintyä luokassa (ilmoituksen ja / tai perinnön kautta).
  3. Parametrinen polymorfismi määrää, että luokan ilmoituksessa kentän nimi voi liittyä eri tyyppiin ja menetelmän nimi voi liittyä erilaisiin parametri- ja palautustyyppeihin. Kenttä ja menetelmä voivat sitten ottaa erilaisia ​​tyyppejä kussakin luokan ilmentymässä (objektissa). Esimerkiksi kenttä voi olla tyypiltään Kaksinkertainen (Java: n tavallisen luokan kirjaston jäsen, joka kääri a kaksinkertainen arvo) ja menetelmä saattaa palauttaa a Kaksinkertainen yhdessä objektissa, ja sama kenttä voi olla tyypiltään Merkkijono ja sama menetelmä saattaa palauttaa a Merkkijono toisessa objektissa. Java tukee parametrista polymorfismia geneeristen lääkkeiden kautta, josta keskustelen tulevassa artikkelissa.
  4. Alatyyppi tarkoittaa, että tyyppi voi toimia toisen tyypin alatyyppinä. Kun alatyypin ilmentymä ilmestyy supertyyppikontekstissa, supertyyppitoiminnon suorittaminen alatyyppiesimerkissä johtaa kyseisen aliohjelman suorittavan version. Harkitse esimerkiksi fragmenttia koodista, joka piirtää mielivaltaisia ​​muotoja. Voit ilmaista tämän piirustuskoodin tiiviimmin ottamalla käyttöön a Muoto luokka a piirtää () menetelmä; esittelemällä Ympyrä, Suorakulmioja muut ohittavat alaluokat piirtää (); ottamalla käyttöön tyypin taulukko Muoto jonka elementit tallentavat viittauksia Muoto alaluokan esiintymät; ja soittamalla Muotoon piirtää () menetelmä. Kun soitat piirtää (), se on Ympyrä, Suorakulmiotai muuta Muoto esimerkiksi piirtää () menetelmä, jota kutsutaan. Sanomme, että on monia muotoja Muotoon piirtää () menetelmä.

Tämä opetusohjelma esittelee alatyypin polymorfismin. Opit päivittämisestä ja myöhäisestä sitomisesta, abstrakteista luokista (joita ei voida instantisoida) ja abstrakteista menetelmistä (joita ei voida kutsua). Opit myös downcastingista ja ajonaikaisesta tyyppitunnistuksesta ja saat ensin katsauksen kovariaanisiin palautustyyppeihin. Tallennan parametrisen polymorfismin tulevaa opetusohjelmaa varten.

Ad-hoc vs universaali polymorfismi

Kuten monet kehittäjät, luokittelen pakottamisen ja ylikuormituksen ad-hoc-polymorfismiksi ja parametri- ja alatyypin universaaliksi polymorfismiksi. Vaikka arvokkaat tekniikat, en usko, että pakko ja ylikuormitus ovat todellista polymorfiaa; ne ovat enemmän tyyppimuunnoksia ja syntaktista sokeria.

Alatyypin polymorfismi: Murtuminen ja myöhäinen sitoutuminen

Alatyypin polymorfismi perustuu nousuun ja myöhään sitoutumiseen. Ylitys on suoratoistomuoto, jossa perimishierarkia asetetaan alatyypistä supertyypiksi. Valitusoperaattoria ei ole mukana, koska alatyyppi on erikoistyyppi supertyyppiin. Esimerkiksi, Muoto s = uusi ympyrä (); Uutiset lähteestä Ympyrä että Muoto. Tämä on järkevää, koska ympyrä on eräänlainen muoto.

Ylöslaskun jälkeen Ympyrä että Muoto, et voi soittaa Ympyrä- spesifiset menetelmät, kuten a getRadius () menetelmä, joka palauttaa ympyrän säteen, koska Ympyrä- spesifiset menetelmät eivät ole osa Muotokäyttöliittymä. Alatyypin ominaisuuksien menettäminen sen jälkeen, kun aliluokka on kavennettu sen yläluokkaan, näyttää turhalta, mutta on välttämätöntä alatyypin polymorfismin saavuttamiseksi.

Olettaa, että Muoto julistaa a piirtää () menetelmä, sen Ympyrä alaluokka ohittaa tämän menetelmän, Muoto s = uusi ympyrä (); on juuri suoritettu, ja seuraava rivi määrittää s.piirrä ();. Mikä piirtää () menetelmää kutsutaan: Muotoon piirtää () menetelmä tai Ympyräon piirtää () menetelmä? Kääntäjä ei tiedä mikä piirtää () tapa soittaa. Ainoa mitä se voi tehdä, on varmistaa, että menetelmä on olemassa yliluokassa, ja varmistaa, että metodikutsun argumenttiluettelo ja palautustyyppi vastaavat superluokan menetelmäilmoitusta. Kääntäjä lisää kuitenkin käännettyyn koodiin myös käskyn, joka ajon aikana hakee ja käyttää mitä tahansa viitteitä s kutsua oikeita piirtää () menetelmä. Tämä tehtävä tunnetaan nimellä myöhäinen sitominen.

Myöhäinen sitoutuminen vs. varhainen sitoutuminen

Myöhäistä sidontaa käytetään puheluihinlopullinen esiintymämenetelmät. Kaikissa muissa menetelmäpuheluissa kääntäjä tietää minkä menetelmän kutsutaan. Se lisää käännetyn koodin käskyn, joka kutsuu muuttujan tyyppiin liittyvän menetelmän eikä sen arvoon. Tämä tekniikka tunnetaan nimellä varhainen sitominen.

Olen luonut sovelluksen, joka osoittaa alatyypin polymorfismin upcastingissa ja myöhässä sitoutumisessa. Tämä sovellus koostuu Muoto, Ympyrä, Suorakulmioja Muodot luokat, joissa jokainen luokka on tallennettu omaan lähdetiedostoonsa. Luettelossa 1 esitetään kolme ensimmäistä luokkaa.

Luettelointi 1. Muotojen hierarkian julistaminen

luokan muoto {void draw () {}} luokan piiri laajentaa muotoa {yksityinen int x, y, r; Ympyrä (int x, int y, int r) {tämä.x = x; tämä.y = y; tämä.r = r; } // Lyhyesti sanottuna olen jättänyt pois getX () -, getY () - ja getRadius () -menetelmät. @Override void draw () {System.out.println ("Piirustusympyrä (" + x + "," + y + "," + r + ")"); }} luokan Suorakulmio ulottuu Muoto {yksityinen int x, y, w, h; Suorakulmio (int x, int y, int w, int h) {tämä.x = x; tämä.y = y; tämä.w = w; tämä. h = h; } // Lyhyesti sanottuna olen jättänyt pois getX (), getY (), getWidth () ja getHeight () // -metodit. @Override void draw () {System.out.println ("Piirustus suorakulmio (" + x + "," + y + "," + w + "," + h + ")"); }}

Luettelossa 2 esitetään Muodot sovellusluokka, jonka main () menetelmä ajaa sovellusta.

Listaus 2. Murtuminen ja myöhäinen sitoutuminen alatyyppipolymorfismissa

luokan muodot {public static void main (String [] args) {Shape [] shape = {uusi ympyrä (10, 20, 30), uusi suorakulmio (20, 30, 40, 50)}; for (int i = 0; i <muodot.pituus; i ++) -muodoille [i] .piirrä (); }}

Ilmoitus muodot taulukko osoittaa ylivirtauksen. Ympyrä ja Suorakulmio viitteet tallennetaan muodot [0] ja muodot [1] ja ovat ylöspäin kirjoitettavia Muoto. Jokainen muodot [0] ja muodot [1] pidetään a Muoto ilmentymä: muodot [0] ei pidetä Ympyrä; muodot [1] ei pidetä Suorakulmio.

Myöhäinen sitoutuminen osoitetaan muodot [i] .piirrä (); ilmaisu. Kun i on yhtä suuri 0, kääntäjän luoma käsky aiheuttaa Ympyräon piirtää () menetelmä kutsutaan. Kun i on yhtä suuri 1tämä ohje kuitenkin aiheuttaa Suorakulmioon piirtää () menetelmä kutsutaan. Tämä on alatyyppisen polymorfismin ydin.

Olettaen, että kaikki neljä lähdetiedostoa (Shapes.java, Muoto.java, Suorakulmio. Javaja Circle.java) sijaitsevat nykyisessä hakemistossa, käännä ne jommallakummalla seuraavista komentoriveistä:

javac * .java javac Shapes.java

Suorita tuloksena oleva sovellus:

java-muodot

Ota huomioon seuraava tulos:

Piirrosympyrä (10, 20, 30) Piirustus suorakulmio (20, 30, 40, 50)

Abstraktit luokat ja menetelmät

Suunnitellessasi luokkahierarkioita huomaat, että luokat, jotka ovat lähempänä näiden hierarkioiden yläosaa, ovat yleisempiä kuin luokat, jotka ovat alempana. Esimerkiksi a Ajoneuvo superluokka on yleisempi kuin a Kuorma-auto alaluokka. Samoin a Muoto superluokka on yleisempi kuin a Ympyrä tai a Suorakulmio alaluokka.

Geneerisen luokan luominen ei ole järkevää. Loppujen lopuksi, mitä a Ajoneuvo esine kuvaa? Samoin, minkälaista muotoa edustaa a Muoto esine? Sen sijaan, että koodisit tyhjän piirtää () menetelmä Muoto, voimme estää tämän menetelmän kutsumisen ja tämän luokan ilmentämisen julistamalla molemmat entiteetit abstrakteiksi.

Java tarjoaa abstrakti varattu sana ilmoittaakseen luokan, jota ei voida instantisoida. Kääntäjä ilmoittaa virheestä, kun yrität instantisoida tätä luokkaa. abstrakti käytetään myös julistamaan menetelmä ilman runkoa. piirtää () menetelmä ei tarvitse runkoa, koska se ei pysty piirtämään abstraktia muotoa. Listaus 3 osoittaa.

Listaus 3. Shape-luokan ja sen draw () -menetelmän tiivistäminen

abstrakti luokka Muoto {abstract void draw (); // vaaditaan puolipiste

Tiivistelmä varoitukset

Kääntäjä ilmoittaa virheestä, kun yrität julistaa luokkaa abstrakti ja lopullinen. Esimerkiksi kääntäjä valittaa abstrakti viimeisen luokan muoto koska abstraktia luokkaa ei voida instantisoida ja lopullista luokkaa ei voida pidentää. Kääntäjä ilmoittaa myös virheestä, kun ilmoitat menetelmän abstrakti mutta älä ilmoita sen luokkaa abstrakti. Poistaminen abstrakti alkaen Muoto luokan otsikko luettelossa 3 johtaisi esimerkiksi virheeseen. Tämä olisi virhe, koska ei-abstraktia (konkreettista) luokkaa ei voida instantisoida, kun se sisältää abstraktin menetelmän. Lopuksi, kun laajennat abstraktia luokkaa, laajentavan luokan on korvattava kaikki abstraktit menetelmät, tai muuten laajentava luokka on itse julistettava abstraktiksi; muuten kääntäjä ilmoittaa virheestä.

Abstrakti luokka voi ilmoittaa kentät, konstruktorit ja ei-abstraktit menetelmät abstraktien menetelmien lisäksi tai sijaan. Esimerkiksi abstrakti Ajoneuvo luokka saattaa ilmoittaa kentät, jotka kuvaavat sen merkkiä, mallia ja vuotta. Se voi myös julistaa rakentajan aloittamaan nämä kentät ja konkreettiset menetelmät palauttamaan arvot. Tutustu luetteloon 4.

Luettelo 4. Ajoneuvon tiivistelmä

abstrakti luokka Ajoneuvo {yksityinen Jousimerkki, malli; yksityinen kansainvälinen vuosi; Ajoneuvo (merkkijono, kielimalli, vuosimalli) {this.make = merkki; tämä malli = malli; tämä.vuosi = vuosi; } Merkkijono getMake () {return make; } String getModel () {palautusmalli; } int getYear () {paluuvuosi; } abstrakti tyhjä siirto (); }

Huomaa se Ajoneuvo julistaa tiivistelmän liikkua() menetelmä ajoneuvon liikkeen kuvaamiseksi. Esimerkiksi auto rullaa tietä pitkin, vene purjehtii veden yli ja kone lentää ilman läpi. Ajoneuvoalaluokat kumoaisivat liikkua() ja antaa asianmukainen kuvaus. He perivät myös menetelmät ja niiden rakentajat kutsuvat Ajoneuvorakentaja.

Downcasting ja RTTI

Luokkahierarkian ylöspäin siirtäminen upcastingin kautta merkitsee alityypin ominaisuuksien menettämistä. Esimerkiksi a Ympyrä objekti Muoto muuttuja s tarkoittaa, että et voi käyttää s soittaa Ympyräon getRadius () menetelmä. Pääsy on kuitenkin mahdollista uudelleen Ympyräon getRadius () menetelmä suorittamalla nimenomainen suoratoisto niinkuin tämä: Ympyrä c = (ympyrä) s;.

Tämä tehtävä tunnetaan nimellä alentaminen koska heität perintöhierarkian supertyypistä alatyypiksi ( Muoto superluokka Ympyrä alaluokka). Vaikka upcast on aina turvallinen (yliluokan käyttöliittymä on alaluokan käyttöliittymän osajoukko), downcast ei ole aina turvallista. Luettelosta 5 näkyy, millaista ongelmaa voi syntyä, jos käytät downcastingia väärin.

Listaus 5. Downcasting-ongelma

class Superclass {} class Subclass laajentaa Superclass {void method () {}} public class BadDowncast {public static void main (String [] args) {Superclass superclass = new Superclass (); Aliluokan alaluokka = (aliluokka) yliluokka; alaluokka.menetelmä (); }}

Luettelossa 5 on luokkahierarkia, joka koostuu Superclass ja Alaluokka, joka ulottuu Superclass. Lisäksi, Alaluokka julistaa menetelmä(). Kolmas luokka nimeltä BadDowncast tarjoaa a main () menetelmä, joka ilmentää Superclass. BadDowncast yrittää sitten alittaa tämän kohteen Alaluokka ja määritä tulos muuttujalle alaluokka.

Tällöin kääntäjä ei valittaa, koska supistaminen yliluokasta aliluokkaan samantyyppisessä hierarkiassa on laillista. Tästä huolimatta, jos tehtävän antaminen sallitaan, sovellus kaatuu, kun se yritti suorittaa alaluokka.menetelmä ();. Tässä tapauksessa JVM yrittää kutsua olematonta menetelmää, koska Superclass ei julista menetelmä(). Onneksi JVM varmistaa, että näyttelijät ovat laillisia, ennen kuin he suorittavat heittotoiminnon. Sen havaitseminen Superclass ei julista menetelmä(), se heittäisi a ClassCastException esine. (Keskustelen poikkeuksista tulevassa artikkelissa.)

Koosta Listaus 5 seuraavasti:

javac BadDowncast.java

Suorita tuloksena oleva sovellus:

java BadDowncast