Programmering

Afslør magien bag undertype polymorfisme

Ordet polymorfisme kommer fra græsk for "mange former." De fleste Java-udviklere forbinder udtrykket med et objekts evne til magisk at udføre korrekt metodeadfærd på passende punkter i et program. Imidlertid fører den implementeringsorienterede opfattelse til billeder af trolddom snarere end en forståelse af grundlæggende begreber.

Polymorfisme i Java er altid undertype polymorfisme. At undersøge nøje de mekanismer, der genererer den mangfoldighed af polymorf adfærd, kræver, at vi kasserer vores sædvanlige implementeringsproblemer og tænker i form af type. Denne artikel undersøger et typeorienteret perspektiv af objekter, og hvordan dette perspektiv adskiller sig hvad adfærd et objekt kan udtrykke fra hvordan objektet udtrykker faktisk denne adfærd. Ved at frigøre vores koncept for polymorfisme fra implementeringshierarkiet opdager vi også, hvordan Java-grænseflader letter polymorf adfærd på tværs af grupper af objekter, der slet ikke har nogen implementeringskode.

Quattro polymorphi

Polymorfisme er et bredt objektorienteret udtryk. Selvom vi normalt sidestiller det generelle koncept med undertypesorten, er der faktisk fire forskellige former for polymorfisme. Inden vi undersøger undertypen polymorfisme detaljeret, præsenterer det følgende afsnit en generel oversigt over polymorfisme i objektorienterede sprog.

Luca Cardelli og Peter Wegner, forfattere af "On Understanding Types, Data Abstraction, and Polymorphism," (se Resources for link to article) opdeler polymorfisme i to hovedkategorier - ad hoc og universal - og fire sorter: tvang, overbelastning, parametrisk og inklusion. Klassifikationsstrukturen er:

 | - tvang | - ad hoc - | | - overbelastning af polymorfisme - | | - parametrisk | - universel - | | - inkludering 

I den generelle ordning repræsenterer polymorfisme en enheds kapacitet til at have flere former. Universel polymorfisme henviser til ensartethed af typestruktur, hvor polymorfismen virker over et uendeligt antal typer, der har et fælles træk. Jo mindre struktureret ad hoc polymorfisme handler over et begrænset antal muligvis ikke-relaterede typer. De fire sorter kan beskrives som:

  • Tvang: en enkelt abstraktion tjener flere typer gennem implicit typekonvertering
  • Overbelastning: en enkelt identifikator betegner flere abstraktioner
  • Parametrisk: en abstraktion fungerer ensartet på tværs af forskellige typer
  • Inkludering: en abstraktion fungerer gennem et inklusionsforhold

Jeg vil kort diskutere hver sort, inden jeg specifikt henvender mig til undertype polymorfisme.

Tvang

Tvang repræsenterer implicit parametertypekonvertering til den type, der forventes af en metode eller en operator, hvorved typefejl undgås. For de følgende udtryk skal kompilatoren bestemme, om en passende binær + operatør findes for de typer operander:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Det første udtryk tilføjer to dobbelt operander; Java-sproget definerer specifikt en sådan operatør.

Imidlertid tilføjer det andet udtryk a dobbelt og en int; Java definerer ikke en operatør, der accepterer disse operandtyper. Heldigvis konverterer compileren implicit den anden operand til dobbelt og bruger operatøren defineret til to dobbelt operander. Det er utroligt praktisk for udvikleren; uden den implicitte konvertering ville der opstå en kompileringstidsfejl, eller programmøren skulle eksplicit kaste int til dobbelt.

Det tredje udtryk tilføjer en dobbelt og en Snor. Endnu en gang definerer Java-sproget ikke en sådan operatør. Så compileren tvinger den dobbelt operand til en Snor, og plus-operatøren udfører streng sammenkædning.

Tvang forekommer også ved metodeopkald. Antag klasse Afledt udvider klassen Grundlagog klasse C har en metode med underskrift m (base). For metodeopfordring i nedenstående kode konverterer compileren implicit afledt referencevariabel, som har typen Afledt, til Grundlag type foreskrevet af metodesignaturen. Denne implicitte konvertering tillader m (base) metodens implementeringskode til kun at bruge de typehandlinger, der er defineret af Grundlag:

 C c = ny C (); Afledt afledt = nyt Afledt (); c.m (afledt); 

Igen undgår implicit tvang under metodeindkaldelse en besværlig rollebesætning eller en unødvendig kompileringstidsfejl. Selvfølgelig verificerer compileren stadig, at alle typekonverteringer stemmer overens med det definerede typehierarki.

Overbelastning

Overbelastning tillader brug af samme operatør eller samme navn til at betegne flere forskellige programbetydninger. Det + operatør brugt i det foregående afsnit udstillet to former: en til tilføjelse dobbelt operander, en til sammenkædning Snor genstande. Der findes andre former for tilføjelse af to heltal, to længder osv. Vi ringer til operatøren overbelastet og stole på, at compileren vælger den passende funktionalitet baseret på programkontekst. Som tidligere bemærket konverterer kompilatoren, hvis det er nødvendigt, implicit operandtyperne for at matche operatørens nøjagtige signatur. Selvom Java angiver visse overbelastede operatører, understøtter den ikke brugerdefineret overbelastning af operatører.

Java tillader brugerdefineret overbelastning af metodenavne. En klasse kan have flere metoder med samme navn, forudsat at metodens signaturer er forskellige. Det betyder, at enten antallet af parametre skal være forskelligt, eller mindst en parameterposition skal have en anden type. Unikke signaturer gør det muligt for kompilatoren at skelne mellem metoder, der har samme navn. Compileren mangler metodens navne ved hjælp af de unikke signaturer og skaber effektivt unikke navne. På baggrund af dette fordamper enhver tilsyneladende polymorf adfærd ved nærmere inspektion.

Både tvang og overbelastning klassificeres som ad hoc, fordi hver kun giver polymorf adfærd i begrænset forstand. Selvom de falder ind under en bred definition af polymorfisme, er disse sorter primært udviklerbekvemmeligheder. Tvang undgår besværlige eksplicit typekaster eller unødvendige kompilertypefejl. På den anden side giver overbelastning syntaktisk sukker, så en udvikler kan bruge det samme navn til forskellige metoder.

Parametrisk

Parametrisk polymorfisme tillader brug af en enkelt abstraktion på tværs af mange typer. F.eks Liste abstraktion, der repræsenterer en liste over homogene objekter, kunne leveres som et generisk modul. Du vil genbruge abstraktionen ved at specificere de typer objekter, der findes på listen. Da den parametriserede type kan være en hvilken som helst brugerdefineret datatype, er der et potentielt uendeligt antal anvendelser til den generiske abstraktion, hvilket gør dette uden tvivl den mest kraftfulde type polymorfisme.

Ved første øjekast ovenstående Liste abstraktion kan synes at være klassens nytte java.util.Liste. Java understøtter imidlertid ikke ægte parametrisk polymorfisme på en typesikker måde, hvorfor det er java.util.Liste og java.util's andre samlingsklasser er skrevet i form af den oprindelige Java-klasse, java.lang.Objekt. (Se min artikel "A Primordial Interface?" For flere detaljer.) Java's enkelt-rodede implementeringsarv tilbyder en delvis løsning, men ikke den virkelige kraft ved parametrisk polymorfisme. Eric Allens fremragende artikel, "Se kraften i parametrisk polymorfisme", beskriver behovet for generiske typer i Java og forslagene til at adressere Suns Java-specifikationsanmodning nr. 000014, "Tilføj generiske typer til Java-programmeringssprog." (Se ressourcer for et link.)

Inkludering

Inklusionspolymorfisme opnår polymorf adfærd gennem en inklusionsrelation mellem typer eller sæt værdier. For mange objektorienterede sprog, inklusive Java, er inkluderingsrelationen en subtype-relation. Så i Java er inkluderingspolymorfisme undertype polymorfisme.

Som tidligere nævnt, når Java-udviklere generisk henviser til polymorfisme, betyder de altid undertype polymorfisme. At få en solid forståelse af undertype polymorfismens styrke kræver, at man ser de mekanismer, der giver polymorf adfærd fra et typeorienteret perspektiv. Resten af ​​denne artikel undersøger dette perspektiv nøje. For kortfattethed og klarhed bruger jeg udtrykket polymorfisme til at betyde undertype polymorfisme.

Typeorienteret visning

UML-klassediagrammet i figur 1 viser den enkle type og klassehierarki, der bruges til at illustrere polymorfismens mekanik. Modellen skildrer fem typer, fire klasser og en grænseflade. Selvom modellen kaldes et klassediagram, tænker jeg på det som et typediagram. Som beskrevet i "Thanks Type and Gentle Class" erklærer hver Java-klasse og interface en brugerdefineret datatype. Så fra en implementeringsuafhængig visning (dvs. en typeorienteret visning) repræsenterer hver af de fem rektangler i figuren en type. Fra et implementeringssynspunkt defineres fire af disse typer ved hjælp af klassekonstruktioner, og en defineres ved hjælp af en grænseflade.

Følgende kode definerer og implementerer hver brugerdefineret datatype. Jeg holder bevidst implementeringen så enkel som muligt:

/ * Base.java * / public class Base {public String m1 () {return "Base.m1 ()"; } offentlig streng m2 (streng s) {return "Base.m2 (" + s + ")"; }} / * IType.java * / interface 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 () {return "Derived.m3 ()"; }} / * Derived2.java * / public class Derived2 udvider 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) {return "Separat.m2 (" + s + ")"; } offentlig streng m3 () {return "Separat.m3 ()"; }} 

Ved hjælp af disse typedeklarationer og klassedefinitioner viser figur 2 et konceptuelt billede af Java-udsagnet:

Afledt2 afledt2 = nyt Afledt2 (); 

Ovenstående erklæring erklærer en eksplicit indtastet referencevariabel, afledt2, og vedhæfter henvisningen til en nyoprettet Afledt2 klasse objekt. Toppanelet i figur 2 viser Afledt2 reference som et sæt koøjer, hvorigennem den underliggende Afledt2 objekt kan ses. Der er et hul til hver Afledt2 type operation. Den aktuelle Afledt2 objekt kort hver Afledt2 operation til passende implementeringskode, som foreskrevet af implementeringshierarkiet defineret i ovenstående kode. F.eks Afledt2 objektkort m1 () til implementeringskode defineret i klassen Afledt. Desuden tilsidesætter denne implementeringskode m1 () metode i klassen Grundlag. EN Afledt2 referencevariabel har ikke adgang til den tilsidesatte m1 () implementering i klassen Grundlag. Det betyder ikke, at den faktiske implementeringskode i klassen Afledt kan ikke bruge Grundlag klasseimplementering via super.m1 (). Men for så vidt angår referencevariablen afledt2 er bekymret for, at koden er utilgængelig. Kortlægningen af ​​den anden Afledt2 operationer viser tilsvarende implementeringskoden, der udføres for hver type operation.

Nu hvor du har en Afledt2 objekt, kan du henvise til det med enhver variabel, der overholder typen Afledt2. Typehierarkiet i UML-diagram i figur 1 afslører det Afledt, Grundlagog IType er alle super typer af Afledt2. Så for eksempel en Grundlag reference kan vedhæftes objektet. Figur 3 viser den konceptuelle opfattelse af følgende Java-sætning:

Base base = afledt2; 

Der er absolut ingen ændring i det underliggende Afledt2 objekt eller en hvilken som helst af operationens kortlægninger, selvom metoder m3 () og m4 () er ikke længere tilgængelige via Grundlag reference. Ringer m1 () eller m2 (streng) ved hjælp af en variabel afledt2 eller grundlag resulterer i udførelse af den samme implementeringskode:

String tmp; // Afledt2-reference (figur 2) tmp = afledt2.m1 (); // tmp er "Afledt.m1 ()" tmp = afledt2.m2 ("Hej"); // tmp er "Derived2.m2 (Hello)" // Base reference (Figur 3) tmp = base.m1 (); // tmp er "Derived.m1 ()" tmp = base.m2 ("Hello"); // tmp er "Derived2.m2 (Hej)" 

At realisere identisk adfærd gennem begge referencer giver mening, fordi Afledt2 objektet ved ikke, hvad der kalder hver metode. Objektet ved kun, at når det kaldes, følger det marcherende ordrer defineret af implementeringshierarkiet. Disse ordrer bestemmer det for metode m1 (), det Afledt2 objekt udfører koden i klassen Afledtog til metode m2 (streng), det udfører koden i klassen Afledt2. Handlingen, der udføres af det underliggende objekt, afhænger ikke af referencevariabelens type.

Alt er dog ikke ens, når du bruger referencevariablerne afledt2 og grundlag. Som afbildet i figur 3, a Grundlag type reference kan kun se Grundlag skriv operationer for det underliggende objekt. Så selvom Afledt2 har tilknytninger til metoder m3 () og m4 ()variabel grundlag kan ikke få adgang til disse metoder:

String tmp; // Afledt2-reference (figur 2) tmp = afledt2.m3 (); // tmp er "Afledt.m3 ()" tmp = afledt2.m4 (); // tmp er "Derived2.m4 ()" // Base reference (Figur 3) tmp = base.m3 (); // Compile-time error tmp = base.m4 (); // Compile-time error 

Kørselstiden

Afledt2

objekt forbliver fuldt ud i stand til at acceptere enten

m3 ()

eller

m4 ()

metodeopkald. De typebegrænsninger, der ikke tillader de forsøgte opkald gennem

Grundlag