Programmering

Java polymorfisme og dens typer

Polymorfisme henviser til nogle enheders evne til at forekomme i forskellige former. Det er populært repræsenteret af sommerfuglen, der skifter fra larve til puppe til imago. Polymorfisme findes også i programmeringssprog som en modelleringsteknik, der giver dig mulighed for at oprette en enkelt grænseflade til forskellige operander, argumenter og objekter. Java polymorfisme resulterer i kode, der er mere kortfattet og lettere at vedligeholde.

Mens denne vejledning fokuserer på undertype polymorfisme, er der flere andre typer, du bør vide om. Vi starter med en oversigt over alle fire typer polymorfisme.

download Hent koden Download kildekoden for eksempel applikationer i denne vejledning. Oprettet af Jeff Friesen til JavaWorld.

Typer af polymorfisme i Java

Der er fire typer polymorfisme i Java:

  1. Tvang er en operation, der tjener flere typer gennem implicit-konvertering. For eksempel deler du et heltal med et andet heltal eller en flydende punktværdi med en anden flydende punktværdi. Hvis den ene operand er et heltal, og den anden operand er en flydende punktværdi, kompilatoren tvinger (konverterer implicit) heltal til en flydende punktværdi for at forhindre en typefejl. (Der er ingen delingsoperation, der understøtter et heltal-operand og en floand-point-operand.) Et andet eksempel er at sende en underklasse-objektreference til en metodes superklasseparameter. Compileren tvinger underklassetypen til superklassetypen for at begrænse operationerne til superklassens.
  2. Overbelastning henviser til at bruge det samme operatørsymbol eller metodenavn i forskellige sammenhænge. For eksempel kan du bruge + for at udføre tilføjelse af heltal, tilføjelse med flydende punkt eller sammenkædning af streng afhængigt af typerne af dets operander. Flere metoder med samme navn kan også vises i en klasse (gennem erklæring og / eller arv).
  3. Parametrisk polymorfisme bestemmer, at inden for en klassedeklaration kan et feltnavn associeres med forskellige typer, og et metodenavn kan associeres med forskellige parameter- og returtyper. Feltet og metoden kan derefter påtage sig forskellige typer i hver klasseinstans (objekt). For eksempel kan et felt være af typen Dobbelt (et medlem af Java's standard klassebibliotek, der omslutter en dobbelt værdi) og en metode kan returnere en Dobbelt i et objekt, og det samme felt kan være af typen Snor og den samme metode kan returnere en Snor i et andet objekt. Java understøtter parametrisk polymorfisme via generiske lægemidler, som jeg vil diskutere i en fremtidig artikel.
  4. Undertype betyder, at en type kan tjene som en anden type undertype. Når en undertype-forekomst vises i en supertypekontekst, resulterer udførelsen af ​​en supertype-handling på undertype-forekomsten i, at undertypens version af den operation udføres. Overvej f.eks. Et fragment af kode, der tegner vilkårlige former. Du kan udtrykke denne tegningskode mere kortfattet ved at indføre en Form klasse med en tegne() metode; ved at introducere Cirkel, Rektangelog andre underklasser, der tilsidesætter tegne(); ved at introducere en række typer Form hvis elementer gemmer henvisninger til Form underklasseforekomster; og ved at ringe Form's tegne() metode på hver forekomst. Når du ringer tegne(), Det er Cirkel's, Rektangeleller andet Form instans tegne() metode, der kaldes. Vi siger, at der er mange former for Form's tegne() metode.

Denne tutorial introducerer undertype polymorfisme. Du lærer om opkastning og sen binding, abstrakte klasser (som ikke kan instantieres) og abstrakte metoder (som ikke kan kaldes). Du lærer også om downcasting og identifikation af runtime-typen, og du får et første kig på kovariante returtyper. Jeg gemmer parametrisk polymorfisme til en fremtidig tutorial.

Ad-hoc vs universel polymorfisme

Som mange udviklere klassificerer jeg tvang og overbelastning som ad hoc polymorfisme og parametrisk og undertype som universel polymorfisme. Selvom værdifulde teknikker, tror jeg ikke, at tvang og overbelastning er ægte polymorfisme; de er mere som typeomdannelser og syntaktisk sukker.

Undertype polymorfisme: Upcasting og sen binding

Undertype polymorfisme er afhængig af opkastning og sen binding. Upcasting er en form for casting, hvor du caster arvshierarkiet fra en undertype til en supertype. Ingen rollebesætningsmedlemmer er involveret, fordi undertypen er en specialisering af supertypen. For eksempel, Form s = ny cirkel (); upcasts fra Cirkel til Form. Dette giver mening, fordi en cirkel er en slags form.

Efter opkastning Cirkel til Form, du kan ikke ringe Cirkel-specifikke metoder, såsom en getRadius () metode, der returnerer cirkelens radius, fordi Cirkel-specifikke metoder er ikke en del af Forminterface. At miste adgang til undertypefunktioner efter at indsnævre en underklasse til sin superklasse virker meningsløs, men er nødvendig for at opnå undertype polymorfisme.

Antag at Form erklærer en tegne() metode, dens Cirkel underklasse tilsidesætter denne metode, Form s = ny cirkel (); har netop udført, og den næste linje specificerer s. tegne ();. Hvilken tegne() metoden kaldes: Form's tegne() metode eller Cirkel's tegne() metode? Compileren ved ikke hvilken tegne() metode til at ringe til. Alt, hvad det kan gøre, er at kontrollere, at der findes en metode i superklassen, og kontrollere, at metodekaldets argumenteliste og returtype matcher superklassens metodedeklaration. Imidlertid indsætter compileren også en instruktion i den kompilerede kode, der ved kørsel henter og bruger den reference, der er i s at kalde det rigtige tegne() metode. Denne opgave er kendt som sen binding.

Sen binding vs tidlig binding

Sen binding bruges til opkald til ikke-endelig instansmetoder. For alle andre metodeopkald ved compileren, hvilken metode der skal kaldes. Den indsætter en instruktion i den kompilerede kode, der kalder den metode, der er knyttet til variabelens type og ikke dens værdi. Denne teknik er kendt som tidlig binding.

Jeg har oprettet en applikation, der demonstrerer undertype polymorfisme med hensyn til opkastning og sen binding. Denne applikation består af Form, Cirkel, Rektangelog Former klasser, hvor hver klasse er gemt i sin egen kildefil. Liste 1 viser de første tre klasser.

Fortegnelse 1. Erklæring om et hierarki af figurer

class Shape {void draw () {}} class Circle extends Shape {private int x, y, r; Cirkel (int x, int y, int r) {this.x = x; this.y = y; this.r = r; } // For kortfattethed har jeg udeladt getX (), getY () og getRadius () metoder. @ Override ugyldig tegning () {System.out.println ("Tegningscirkel (" + x + "," + y + "," + r + ")"); }} klasse Rektangel udvider figur {privat int x, y, w, h; Rektangel (int x, int y, int w, int h) {this.x = x; this.y = y; this.w = w; this.h = h; } // For kortfattethed har jeg udeladt getX (), getY (), getWidth () og getHeight () // metoder. @ Override ugyldig tegning () {System.out.println ("Tegningsrektangel (" + x + "," + y + "," + w + "," + h + ")"); }}

Liste 2 præsenterer Former ansøgningsklasse hvis hoved () metode driver applikationen.

Listing 2. Upcasting og sen binding i undertype polymorfisme

klasse Shapes {public static void main (String [] args) {Shape [] shapes = {new Circle (10, 20, 30), new Rectangle (20, 30, 40, 50)}; for (int i = 0; i <shapes.length; i ++) former [i] .draw (); }}

Erklæringen fra former array viser upcasting. Det Cirkel og Rektangel referencer er gemt i figurer [0] og figurer [1] og er upcast for at skrive Form. Hver af figurer [0] og figurer [1] betragtes som en Form eksempel: figurer [0] betragtes ikke som en Cirkel; figurer [1] betragtes ikke som en Rektangel.

Sen binding demonstreres af figurer [i] .draw (); udtryk. Hvornår jeg lige med 0, forårsager den compiler-genererede instruktion Cirkel's tegne() metode, der skal kaldes. Hvornår jeg lige med 1dog forårsager denne instruktion Rektangel's tegne() metode, der skal kaldes. Dette er essensen af ​​undertype polymorfisme.

Forudsat at alle fire kildefiler (Shapes.java, Form.java, Rektangel.javaog Cirkel.java) findes i den aktuelle mappe, kompiler dem via en af ​​følgende kommandolinjer:

javac * .java javac Shapes.java

Kør den resulterende applikation:

java figurer

Du skal overholde følgende output:

Tegningscirkel (10, 20, 30) Tegningsrektangel (20, 30, 40, 50)

Abstrakte klasser og metoder

Når du designer klassehierarkier, finder du ud af, at klasser nærmere toppen af ​​disse hierarkier er mere generiske end klasser, der er lavere. F.eks Køretøj superklasse er mere generisk end en Lastbil underklasse. Tilsvarende er en Form superklasse er mere generisk end en Cirkel eller a Rektangel underklasse.

Det giver ikke mening at starte en generisk klasse. Når alt kommer til alt, hvad ville en Køretøj objekt beskrive? Tilsvarende, hvilken slags form er repræsenteret af en Form objekt? I stedet for at kode et tomt tegne() metode i Form, kan vi forhindre, at denne metode kaldes, og at denne klasse instantieres ved at erklære begge enheder for at være abstrakte.

Java leverer abstrakt reserveret ord til at erklære en klasse, der ikke kan instantieres. Compileren rapporterer en fejl, når du prøver at instantiere denne klasse. abstrakt bruges også til at erklære en metode uden lig. Det tegne() metoden behøver ikke en krop, fordi den ikke er i stand til at tegne en abstrakt form. Liste 3 viser.

Listing 3. Abstraktion af Shape-klassen og dens draw () -metode

abstrakt klasse Form {abstrakt ugyldigt tegn (); // semikolon er påkrævet}

Abstrakte advarsler

Compileren rapporterer en fejl, når du forsøger at erklære en klasse abstrakt og endelig. For eksempel klager compileren over abstrakt endelig klasse Form fordi en abstrakt klasse ikke kan instantieres, og en endelig klasse ikke kan udvides. Compileren rapporterer også en fejl, når du erklærer en metode abstrakt men erklær ikke sin klasse abstrakt. Fjerner abstrakt fra Form klassens overskrift i liste 3 vil f.eks. resultere i en fejl. Dette ville være en fejl, fordi en ikke-abstrakt (konkret) klasse ikke kan instantieres, når den indeholder en abstrakt metode. Endelig, når du udvider en abstrakt klasse, skal den udvidende klasse tilsidesætte alle de abstrakte metoder, ellers skal den udvidende klasse selv erklæres for at være abstrakt; Ellers rapporterer compileren en fejl.

En abstrakt klasse kan erklære felter, konstruktører og ikke-abstrakte metoder ud over eller i stedet for abstrakte metoder. For eksempel et abstrakt Køretøj klasse kan erklære felter, der beskriver mærke, model og år. Det kan også erklære en konstruktør til at initialisere disse felter og konkrete metoder til at returnere deres værdier. Tjek Listing 4.

Notering 4. Abstraktion af et køretøj

abstrakt klasse Vehicle {private String mærke, model; privat int år; Vehicle (String make, String model, int year) {this.make = make; this.model = model; dette.år = år; } String getMake () {return make; } String getModel () {returmodel; } int getYear () {returår; } abstrakt ugyldigt træk (); }

Du bemærker det Køretøj erklærer et abstrakt bevæge sig() metode til at beskrive et køretøjs bevægelse. For eksempel ruller en bil ned ad vejen, en båd sejler over vandet, og et fly flyver gennem luften. Køretøj's underklasser ville tilsidesætte bevæge sig() og give en passende beskrivelse. De ville også arve metoderne, og deres konstruktører ville kalde Køretøjs konstruktør.

Downcasting og RTTI

At bevæge sig op i klassehierarkiet via upcasting indebærer at miste adgangen til undertypefunktioner. For eksempel tildele en Cirkel protesterer mod Form variabel s betyder, at du ikke kan bruge s at ringe Cirkel's getRadius () metode. Det er dog muligt igen at få adgang Cirkel's getRadius () metode ved at udføre en eksplicit rollebesætning som denne: Cirkel c = (Cirkel) s;.

Denne opgave er kendt som nedkastning fordi du kaster ned arvshierarkiet fra en supertype til en undertype (fra Form superklasse til Cirkel underklasse). Selvom en upcast altid er sikker (superklassens interface er en delmængde af subklassens interface), er en downcast ikke altid sikker. Fortegnelse 5 viser, hvilken slags problemer der kan opstå, hvis du bruger downcasting forkert.

Liste 5. Problemet med downcasting

klasse Superklasse {} klasse Underklasse udvider Superklasse {ugyldig metode () {}} offentlig klasse BadDowncast {offentlig statisk ugyldig hoved (String [] args) {Superklasse superklasse = ny Superklasse (); Underklasse underklasse = (Underklasse) superklasse; underklasse. metode (); }}

Listing 5 præsenterer et klassehierarki bestående af Superklasse og Underklasse, som strækker sig Superklasse. Desuden, Underklasse erklærer metode(). En tredje klasse navngivet Dårlig udsendelse giver en hoved () metode, der instantierer Superklasse. Dårlig udsendelse forsøger derefter at nedsky dette objekt til Underklasse og tildel resultatet til variabel underklasse.

I dette tilfælde vil compileren ikke klage, fordi downcasting fra en superklasse til en underklasse i samme typehierarki er lovlig. Når det er sagt, hvis opgaven var tilladt, ville applikationen gå ned, når den forsøgte at udføre underklasse. metode ();. I dette tilfælde vil JVM forsøge at kalde en ikke-eksisterende metode, fordi Superklasse erklærer ikke metode(). Heldigvis verificerer JVM, at en rollebesætning er lovlig, før den udfører en rollebesætning. At opdage det Superklasse erklærer ikke metode(), det ville kaste en ClassCastException objekt. (Jeg diskuterer undtagelser i en fremtidig artikel.)

Kompilér liste 5 som følger:

javac BadDowncast.java

Kør den resulterende applikation:

java BadDowncast