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.util
sine 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