Programmering

Avslør magien bak undertypen polymorfisme

Ordet polymorfisme kommer fra gresk for "mange former." De fleste Java-utviklere forbinder begrepet med et objekts evne til å magisk utføre riktig metodeadferd på passende punkter i et program. Imidlertid fører det implementeringsorienterte synet til bilder av trolldom, snarere enn en forståelse av grunnleggende konsepter.

Polymorfisme i Java er alltid undertype polymorfisme. Å undersøke nøye mekanismene som genererer den mangfoldet av polymorf oppførsel krever at vi forkaster våre vanlige implementeringsproblemer og tenker type. Denne artikkelen undersøker et typeorientert perspektiv på objekter, og hvordan det perspektivet skiller seg hva oppførsel et objekt kan uttrykke fra hvordan objektet uttrykker faktisk den oppførselen. Ved å frigjøre begrepet polymorfisme fra implementeringshierarkiet, oppdager vi også hvordan Java-grensesnitt letter polymorf oppførsel på tvers av grupper av objekter som ikke har noen implementeringskode i det hele tatt.

Quattro polymorphi

Polymorfisme er et bredt objektorientert begrep. Selv om vi vanligvis likestiller det generelle konseptet med subtypesorten, er det faktisk fire forskjellige typer polymorfisme. Før vi undersøker undertypen polymorfisme i detalj, presenterer følgende avsnitt en generell oversikt over polymorfisme i objektorienterte språk.

Luca Cardelli og Peter Wegner, forfattere av "On Understanding Types, Data Abstraction, and Polymorphism," (se Resources for link to article) deler polymorfisme i to hovedkategorier - ad hoc og universal - og fire varianter: tvang, overbelastning, parametrisk og inkludering. Klassifiseringsstrukturen er:

 | - tvang | - ad hoc - | | - overbelastning av polymorfisme - | | - parametrisk | - universell - | | - inkludering 

I den generelle ordningen representerer polymorfisme en enhets evne til å ha flere former. Universell polymorfisme refererer til en ensartethet av typestruktur, der polymorfismen virker over et uendelig antall typer som har et felles trekk. Jo mindre strukturert ad hoc polymorfisme handler over et endelig antall muligens ikke-relaterte typer. De fire variantene kan beskrives som:

  • Tvang: en enkelt abstraksjon tjener flere typer gjennom implisitt typekonvertering
  • Overbelastning: en enkelt identifikator betegner flere abstraksjoner
  • Parametrisk: en abstraksjon fungerer jevnt på tvers av forskjellige typer
  • Inkludering: en abstraksjon opererer gjennom et inkluderingsforhold

Jeg vil kort diskutere hver sort før jeg spesifikt henvender meg til undertype polymorfisme.

Tvang

Tvang representerer implisitt parameterkonvertering til typen som forventes av en metode eller en operatør, og unngår dermed typefeil. For de følgende uttrykkene må kompilatoren bestemme om en passende binær + operatør eksisterer for typer operander:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Det første uttrykket legger til to dobbelt operander; Java-språket definerer spesifikt en slik operatør.

Imidlertid legger det andre uttrykket til a dobbelt og en int; Java definerer ikke en operatør som godtar disse operand-typene. Heldigvis konverterer kompilatoren implisitt den andre operanden til dobbelt og bruker operatøren definert for to dobbelt operander. Det er utrolig praktisk for utvikleren; uten den implisitte konverteringen, ville det oppstå en kompileringsfeil, eller programmereren måtte eksplisitt kaste int til dobbelt.

Det tredje uttrykket legger til en dobbelt og en String. Nok en gang definerer Java-språket ikke en slik operatør. Så kompilatoren tvinger den dobbelt operand til en String, og pluss-operatøren utfører strengkombinasjon.

Tvang skjer også ved påkallelse av metoden. Anta klasse Avledet utvider klassen Utgangspunkt, og klasse C har en metode med signatur m (Base). For metodeanropet i koden nedenfor konverterer kompilatoren implisitt avledet referansevariabel, som har type Avledet, til Utgangspunkt type foreskrevet av metodesignaturen. Den implisitte konvertering tillater m (Base) metodens implementeringskode for å bare bruke typeoperasjonene definert av Utgangspunkt:

 C c = ny C (); Avledet avledet = nytt Avledet (); c.m (avledet); 

Igjen hindrer implisitt tvang under metodeinnkallelse en tungvint type rollebesetning eller en unødvendig kompileringstidfeil. Selvfølgelig verifiserer kompilatoren fortsatt at alle typekonverteringer samsvarer med det definerte typehierarkiet.

Overbelastning

Overbelastning tillater bruk av samme operatør eller metodenavn for å betegne flere, forskjellige programbetydninger. De + operatøren som ble brukt i forrige avsnitt, viste to former: en for å legge til dobbelt operander, en for sammenkobling String gjenstander. Det finnes andre former for å legge til to heltall, to lengder og så videre. Vi ringer operatøren overbelastet og stole på at kompilatoren velger riktig funksjonalitet basert på programkontekst. Som tidligere bemerket konverterer kompilatoren implisitt operand-typene om nødvendig for å matche operatørens eksakte signatur. Selv om Java spesifiserer visse overbelastede operatører, støtter den ikke brukerdefinert overbelastning av operatører.

Java tillater brukerdefinert overbelastning av metodenavn. En klasse kan ha flere metoder med samme navn, forutsatt at metodesignaturene er forskjellige. Det betyr at enten antall parametere må være forskjellige, eller at minst en parameterposisjon må ha en annen type. Unike signaturer lar kompilatoren skille mellom metoder som har samme navn. Kompilatoren mangler metodenavnene ved hjelp av de unike signaturene, og skaper effektivt unike navn. I lys av det fordamper enhver tilsynelatende polymorf oppførsel ved nærmere inspeksjon.

Både tvang og overbelastning er klassifisert som ad hoc fordi hver gir polymorf oppførsel bare i begrenset forstand. Selv om de faller under en bred definisjon av polymorfisme, er disse variantene først og fremst utviklerkomfort. Tvang fjerner tungvint eksplisitte typekast eller unødvendige kompilertypefeil. Overbelastning gir derimot syntaktisk sukker, slik at en utvikler kan bruke samme navn for forskjellige metoder.

Parametrisk

Parametrisk polymorfisme tillater bruk av en enkelt abstraksjon på tvers av mange typer. For eksempel, a Liste abstraksjon, som representerer en liste over homogene objekter, kan gis som en generisk modul. Du vil bruke abstraksjonen på nytt ved å spesifisere hvilke typer objekter som finnes i listen. Siden den parameteriserte typen kan være hvilken som helst brukerdefinert datatype, er det et potensielt uendelig antall bruksområder for den generiske abstraksjonen, noe som gjør dette uten tvil den kraftigste typen polymorfisme.

Ved første øyekast ovenfor Liste abstraksjon kan synes å være nytten av klassen java.util.Liste. Imidlertid støtter Java ikke ekte parametrisk polymorfisme på en typesikker måte, og det er derfor java.util.Liste og java.utilsine andre samlingsklasser er skrevet i samsvar med den opprinnelige Java-klassen, java.lang.Objekt. (Se artikkelen min "Et primordialt grensesnitt?" For mer informasjon.) Java's enkeltrotte implementeringsarv gir en delvis løsning, men ikke den virkelige kraften til parametrisk polymorfisme. Eric Allens utmerkede artikkel, "Behold the Power of Parametric Polymorphism", beskriver behovet for generiske typer i Java og forslagene til å adressere Suns Java-spesifikasjonsforespørsel nr. 000014, "Legg til generiske typer til Java-programmeringsspråket." (Se Ressurser for en lenke.)

Inkludering

Inkluderingspolymorfisme oppnår polymorf oppførsel gjennom et inkluderingsforhold mellom typer eller sett av verdier. For mange objektorienterte språk, inkludert Java, er inkluderingsrelasjonen en undertypeforhold. Så i Java er inkludering polymorfisme undertype polymorfisme.

Som nevnt tidligere, når Java-utviklere generelt refererer til polymorfisme, betyr de alltid undertype polymorfisme. Å få en solid forståelse av subtypen polymorfismens kraft krever å se på mekanismene som gir polymorf oppførsel fra et typeorientert perspektiv. Resten av denne artikkelen undersøker dette perspektivet nøye. For korthet og klarhet bruker jeg begrepet polymorfisme for å bety undertype polymorfisme.

Type-orientert visning

UML-klassediagrammet i figur 1 viser den enkle typen og klassehierarkiet som brukes til å illustrere mekanikken til polymorfisme. Modellen skildrer fem typer, fire klasser og ett grensesnitt. Selv om modellen kalles et klassediagram, tenker jeg på det som et typediagram. Som beskrevet i "Thanks Type and Gentle Class", erklærer hver Java-klasse og grensesnitt en brukerdefinert datatype. Så fra et implementeringsuavhengig syn (dvs. en typeorientert visning) representerer hver av de fem rektanglene i figuren en type. Fra et implementeringssynspunkt er fire av disse typene definert ved hjelp av klassekonstruksjoner, og en er definert ved hjelp av et grensesnitt.

Følgende kode definerer og implementerer hver brukerdefinert datatype. Jeg holder bevisst implementeringen så enkel som mulig:

/ * Base.java * / public class Base {public String m1 () {return "Base.m1 ()"; } offentlig streng m2 (streng s) {return "Base.m2 (" + s + ")"; }} / * IType.java * / grensesnitt IType {String m2 (String s); Streng m3 (); } / * Derived.java * / public class Derived extends Base implementerer IType {public String m1 () {return "Derived.m1 ()"; } offentlig streng m3 () {retur "Derived.m3 ()"; }} / * Derived2.java * / public class Derived2 utvider Derived {public String m2 (String s) {return "Derived2.m2 (" + s + ")"; } public String m4 () {return "Derived2.m4 ()"; }} / * Separate.java * / public class Separate implementerer IType {public String m1 () {return "Separate.m1 ()"; } offentlig streng m2 (streng s) {retur "Separat.m2 (" + s + ")"; } offentlig streng m3 () {retur "Separat.m3 ()"; }} 

Ved å bruke disse typedeklarasjonene og klassedefinisjonene, viser figur 2 et konseptuelt syn på Java-setningen:

Avledet2 avledet2 = nytt Avledet2 (); 

Ovennevnte uttalelse erklærer en eksplisitt skrevet referansevariabel, avledet2, og legger ved den referansen til en nylig opprettet Avledet2 klasseobjekt. Toppanelet i figur 2 viser Avledet2 referanse som et sett med koøyer, gjennom hvilke den underliggende Avledet2 objektet kan sees. Det er ett hull for hver Avledet2 type operasjon. Den faktiske Avledet2 objekt kartlegger hver Avledet2 drift til riktig implementeringskode, som foreskrevet av implementeringshierarkiet definert i ovennevnte kode. For eksempel Avledet2 objektkart m1 () til implementeringskode definert i klassen Avledet. Videre overstyrer implementeringskoden m1 () metode i klassen Utgangspunkt. EN Avledet2 referansevariabelen har ikke tilgang til det overstyrte m1 () implementering i klassen Utgangspunkt. Det betyr ikke at den faktiske implementeringskoden i klassen Avledet kan ikke bruke Utgangspunkt klasseimplementering via super.m1 (). Men så langt som referansevariabelen avledet2 er bekymret, at koden er utilgjengelig. Kartleggingen til den andre Avledet2 operasjoner viser på samme måte implementeringskoden som er utført for hver type operasjon.

Nå som du har en Avledet2 objekt, kan du referere til den med en hvilken som helst variabel som samsvarer med typen Avledet2. Typehierarkiet i figur 1s UML-diagram avslører det Avledet, Utgangspunkt, og IType er alle super typer Avledet2. Så for eksempel en Utgangspunkt referanse kan festes til objektet. Figur 3 viser det konseptuelle synet på følgende Java-setning:

Base base = avledet2; 

Det er absolutt ingen endring i det underliggende Avledet2 objekt eller noen av operasjonskartleggingene, men metoder m3 () og m4 () er ikke lenger tilgjengelig gjennom Utgangspunkt henvisning. Ringer m1 () eller m2 (streng) bruker begge variablene avledet2 eller utgangspunkt resulterer i kjøring av samme implementeringskode:

String tmp; // Derived2 referanse (figur 2) tmp = derivert2.m1 (); // tmp er "Derived.m1 ()" tmp = derivated2.m2 ("Hello"); // tmp er "Derived2.m2 (Hello)" // Basereferanse (figur 3) tmp = base.m1 (); // tmp er "Derived.m1 ()" tmp = base.m2 ("Hello"); // tmp er "Derived2.m2 (Hello)" 

Å realisere identisk atferd gjennom begge referanser er fornuftig fordi Avledet2 objektet vet ikke hva som kaller hver metode. Objektet vet bare at når det kalles på, følger det marsjordrene definert av implementeringshierarkiet. Disse ordrene bestemmer at for metode m1 (), den Avledet2 objektet utfører koden i klassen Avledet, og for metode m2 (streng), den utfører koden i klassen Avledet2. Handlingen utført av det underliggende objektet avhenger ikke av referansevariabelens type.

Alt er imidlertid ikke likt når du bruker referansevariablene avledet2 og utgangspunkt. Som vist i figur 3, a Utgangspunkt type referanse kan bare se Utgangspunkt skriv operasjoner av det underliggende objektet. Så selv om Avledet2 har kartlegginger for metoder m3 () og m4 (), variabel utgangspunkt har ikke tilgang til disse metodene:

String tmp; // Derived2 referanse (figur 2) tmp = derivated2.m3 (); // tmp er "Derived.m3 ()" tmp = deriv2.m4 (); // tmp er "Derived2.m4 ()" // Base referanse (figur 3) tmp = base.m3 (); // Compile-time error tmp = base.m4 (); // Kompileringstidsfeil 

Kjøretiden

Avledet2

objektet er fortsatt fullt i stand til å godta enten

m3 ()

eller

m4 ()

metode samtaler. Typebegrensningene som ikke tillater de forsøkte samtalene gjennom

Utgangspunkt

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