Polymorfisme refererer til muligheten til noen enheter å forekomme i forskjellige former. Den er populært representert av sommerfuglen, som forandrer seg fra larve til puppe til imago. Polymorfisme eksisterer også i programmeringsspråk, som en modelleringsteknikk som lar deg lage et enkelt grensesnitt til forskjellige operander, argumenter og objekter. Java polymorfisme resulterer i kode som er mer kortfattet og lettere å vedlikeholde.
Mens denne opplæringen fokuserer på undertype polymorfisme, er det flere andre typer du bør vite om. Vi starter med en oversikt over alle fire typer polymorfisme.
last ned Få koden Last ned kildekoden for eksempel applikasjoner i denne opplæringen. Skapt av Jeff Friesen for JavaWorld.Typer polymorfisme i Java
Det er fire typer polymorfisme i Java:
- Tvang er en operasjon som serverer flere typer gjennom implisitt konvertering. For eksempel deler du et heltall med et annet heltall eller en flytende verdi med en annen flytende verdi. Hvis en operand er et helt tall og den andre operand er en flytende punktverdi, kompilatoren tvinger (konverterer implisitt) heltallet til en flytende verdi for å forhindre en typefeil. (Det er ingen delingsoperasjon som støtter et heltallsoperand og en operand med flytende punkt.) Et annet eksempel er å sende en underklasseobjektreferanse til en metodes superklasseparameter. Kompilatoren tvinger underklassetypen til superklassetypen for å begrense operasjonene til superklassens.
- Overbelastning refererer til å bruke det samme operatørsymbolet eller metodenavnet i forskjellige sammenhenger. For eksempel kan du bruke
+
for å utføre heltallsaddisjon, flytende punktaddisjon eller strengsammenslåing, avhengig av type operander. Flere metoder med samme navn kan også vises i en klasse (gjennom erklæring og / eller arv). - Parametrisk polymorfisme bestemmer at innen en klassedeklarasjon kan et feltnavn knytte seg til forskjellige typer og et metodenavn kan knytte seg til forskjellige parameter- og returtyper. Feltet og metoden kan deretter ta på seg forskjellige typer i hver klasseinstans (objekt). For eksempel kan et felt være av typen
Dobbelt
(et medlem av Java sitt standard klassebibliotek som pakker inn endobbelt
verdi) og en metode kan returnere aDobbelt
i ett objekt, og det samme feltet kan være av typenString
og den samme metoden kan returnere aString
i et annet objekt. Java støtter parametrisk polymorfisme via generiske, som jeg vil diskutere i en fremtidig artikkel. - Undertype betyr at en type kan tjene som undertypen til en annen type. Når en undertypeforekomst vises i en supertypekontekst, vil kjøring av en supertypeoperasjon på undertypeforekomsten føre til at subtypeversjonen av den operasjonen kjøres. Tenk for eksempel på et fragment av kode som tegner vilkårlige former. Du kan uttrykke denne tegningskoden mer konsist ved å introdusere en
Form
klasse med entegne()
metode; ved å introdusereSirkel
,Rektangel
, og andre underklasser som overstyrertegne()
; ved å introdusere en rekke typerForm
hvis elementer lagrer referanser tilForm
underklasseforekomster; og ved å ringeForm
stegne()
metode for hver forekomst. Når du ringertegne()
, det erSirkel
's,Rektangel
eller annetForm
forekomst ertegne()
metoden som blir kalt. Vi sier at det er mange former forForm
stegne()
metode.
Denne opplæringen introduserer polymorfisme av undertype. Du lærer om upcasting og sen binding, abstrakte klasser (som ikke kan instantieres) og abstrakte metoder (som ikke kan kalles). Du vil også lære om downcasting og kjøretidsidentifikasjon, og du får en første titt på kovariante returtyper. Jeg lagrer parametrisk polymorfisme for en fremtidig opplæring.
Ad-hoc vs universell polymorfisme
Som mange utviklere klassifiserer jeg tvang og overbelastning som ad-hoc polymorfisme, og parametrisk og undertype som universell polymorfisme. Selv om det er verdifulle teknikker, tror jeg ikke tvang og overbelastning er ekte polymorfisme; de er mer som typekonvertering og syntaktisk sukker.
Undertype polymorfisme: Upcasting og sen binding
Undertype polymorfisme er avhengig av oppkast og sen binding. Upcasting er en form for avstøpning der du kaster opp arvshierarkiet fra en undertype til en supertype. Ingen rolleoperatører er involvert fordi undertypen er en spesialisering av supertypen. For eksempel, Form s = ny sirkel ();
oppkast fra Sirkel
til Form
. Dette gir mening fordi en sirkel er en slags form.
Etter oppkasting Sirkel
til Form
, du kan ikke ringe Sirkel
-spesifikke metoder, som f.eks getRadius ()
metode som returnerer sirkelens radius, fordi Sirkel
-spesifikke metoder er ikke en del av Form
grensesnitt. Å miste tilgang til subtype-funksjoner etter å ha begrenset en underklasse til superklassen virker meningsløs, men er nødvendig for å oppnå undertype polymorfisme.
Anta at Form
erklærer en tegne()
metoden, dens Sirkel
underklasse overstyrer denne metoden, Form s = ny sirkel ();
har nettopp utført, og neste linje spesifiserer s. tegne ();
. Hvilken tegne()
metoden kalles: Form
s tegne()
metode eller Sirkel
s tegne()
metode? Kompilatoren vet ikke hvilken tegne()
metode for å ringe. Alt det kan gjøre er å verifisere at det finnes en metode i superklassen, og kontrollere at metodekallets argumentliste og returtype samsvarer med superklassens metodedeklarasjon. Imidlertid setter kompilatoren også inn en instruksjon i den kompilerte koden som på kjøretid henter og bruker den referansen som er i s
å kalle det riktige tegne()
metode. Denne oppgaven er kjent som sen binding.
Sen binding vs tidlig binding
Sen binding brukes til samtaler til ikke-endelig
eksempelmetoder. For alle andre metodeanrop vet kompilatoren hvilken metode han skal ringe. Den setter inn en instruksjon i den kompilerte koden som kaller metoden som er knyttet til variabelens type og ikke dens verdi. Denne teknikken er kjent som tidlig binding.
Jeg har laget et program som demonstrerer undertype polymorfisme når det gjelder oppkast og sen binding. Denne applikasjonen består av Form
, Sirkel
, Rektangel
, og Former
klasser, der hver klasse er lagret i sin egen kildefil. Oppføring 1 presenterer de tre første klassene.
Oppføring 1. Deklarere et hierarki av figurer
class Shape {void draw () {}} class Circle extends Shape {private int x, y, r; Sirkel (int x, int y, int r) {this.x = x; this.y = y; this.r = r; } // For kortfattethet har jeg utelatt getX (), getY () og getRadius () -metodene. @ Override ugyldig tegning () {System.out.println ("Tegnesirkel (" + x + "," + y + "," + r + ")"); }} klasse Rektangel utvider 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 kortfattethet har jeg utelatt getX (), getY (), getWidth () og getHeight () // metoder. @ Override ugyldig tegning () {System.out.println ("Tegningsrektangel (" + x + "," + y + "," + w + "," + h + ")"); }}
Oppføring 2 presenterer Former
søknadsklasse hvis hoved()
metoden driver applikasjonen.
Oppføring 2. Upcasting og sen binding i undertype polymorfisme
class 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. De Sirkel
og Rektangel
referanser lagres i former [0]
og former [1]
og er oppkast for å skrive Form
. Hver av former [0]
og former [1]
regnes som en Form
forekomst: former [0]
blir ikke sett på som en Sirkel
; former [1]
blir ikke sett på som en Rektangel
.
Sen binding er demonstrert av figurer [i]. tegne ();
uttrykk. Når Jeg
er lik 0
, forårsaker den kompilatorgenererte instruksjonen Sirkel
s tegne()
metoden som skal kalles. Når Jeg
er lik 1
imidlertid forårsaker denne instruksjonen Rektangel
s tegne()
metoden som skal kalles. Dette er essensen av undertype polymorfisme.
Forutsatt at alle fire kildefilene (Shapes.java
, Form.java
, Rektangel.java
, og Circle.java
) ligger i den gjeldende katalogen, kompilerer du dem via en av følgende kommandolinjer:
javac * .java javac Shapes.java
Kjør den resulterende applikasjonen:
java figurer
Du bør følge følgende utdata:
Tegnesirkel (10, 20, 30) Tegningsrektangel (20, 30, 40, 50)
Abstrakte klasser og metoder
Når du designer klassehierarkier, vil du oppdage at klasser nærmere toppen av disse hierarkiene er mer generiske enn klasser som er lavere nede. For eksempel, a Kjøretøy
superklasse er mer generisk enn en Lastebil
underklasse. Tilsvarende a Form
superklasse er mer generisk enn en Sirkel
eller a Rektangel
underklasse.
Det gir ikke mening å sette i gang en generisk klasse. Tross alt, hva ville a Kjøretøy
objektet beskriver? Tilsvarende hva slags form er representert med en Form
gjenstand? Snarere enn å kode en tom tegne()
metode i Form
, kan vi forhindre at denne metoden blir kalt og at denne klassen blir instantiert ved å erklære begge enhetene for å være abstrakte.
Java tilbyr abstrakt
reservert ord for å erklære en klasse som ikke kan instantieres. Kompilatoren rapporterer en feil når du prøver å sette i gang denne klassen. abstrakt
brukes også til å erklære en metode uten kropp. De tegne()
metoden trenger ikke en kropp fordi den ikke klarer å tegne en abstrakt form. Oppføring 3 viser.
Oppføring 3. Abstrstrahere Shape-klassen og dens draw () -metode
abstrakt klasse Form {abstrakt ugyldig tegning (); // semikolon kreves}
Abstrakte advarsler
Kompilatoren rapporterer en feil når du prøver å erklære en klasse abstrakt
og endelig
. For eksempel klager kompilatoren over abstrakt sluttklasse Form
fordi en abstrakt klasse ikke kan instantiseres og en siste klasse ikke kan utvides. Kompilatoren rapporterer også om en feil når du erklærer en metode abstrakt
men ikke erklære sin klasse abstrakt
. Fjerner abstrakt
fra Form
klassens overskrift i oppføring 3 vil for eksempel resultere i en feil. Dette ville være en feil fordi en ikke-abstrakt (konkret) klasse ikke kan instantiseres når den inneholder en abstrakt metode. Til slutt, når du utvider en abstrakt klasse, må den utvidende klassen overstyre alle de abstrakte metodene, ellers må den utvidende klassen selv erklæres som abstrakt; Ellers rapporterer kompilatoren en feil.
En abstrakt klasse kan erklære felt, konstruktører og ikke-abstrakte metoder i tillegg til eller i stedet for abstrakte metoder. For eksempel et abstrakt Kjøretøy
klasse kan erklære felt som beskriver merke, modell og år. Det kan også erklære at en konstruktør skal initialisere disse feltene og konkrete metoder for å returnere verdiene. Sjekk ut oppføring 4.
Oppføring 4. Abstrakt et kjøretøy
abstrakt klasse Vehicle {private String make, model; privat int år; Vehicle (String make, String model, int year) {this.make = make; this.model = modell; dette.år = år; } String getMake () {return make; } String getModel () {returmodell; } int getYear () {returår; } abstrakt ugyldig trekk (); }
Du vil merke det Kjøretøy
erklærer et abstrakt bevege seg()
metode for å beskrive bevegelsen til et kjøretøy. For eksempel ruller en bil nedover veien, en båt seiler over vannet, og et fly flyr gjennom luften. Kjøretøy
sine underklasser vil overstyre bevege seg()
og gi en passende beskrivelse. De ville også arve metodene og deres konstruktører ville kalle Kjøretøy
sin konstruktør.
Nedkastning og RTTI
Å flytte opp i klassehierarkiet, via oppkast, innebærer å miste tilgangen til undertypefunksjoner. For eksempel tildele en Sirkel
ha innvendinger Form
variabel s
betyr at du ikke kan bruke s
å ringe Sirkel
s getRadius ()
metode. Det er imidlertid mulig å få tilgang til igjen Sirkel
s getRadius ()
metode ved å utføre en eksplisitt rollebesetning Som denne: Sirkel c = (Sirkel) s;
.
Denne oppgaven er kjent som nedkasting fordi du kaster ned arvshierarkiet fra en supertype til en undertype (fra Form
superklasse til Sirkel
underklasse). Selv om en upcast alltid er trygg (superklassens grensesnitt er en delmengde av subclassens grensesnitt), er en downcast ikke alltid trygg. Oppføring 5 viser hva slags problemer som kan oppstå hvis du bruker nedkastning feil.
Oppføring 5. Problemet med nedkasting
class Superclass {} class Subclass extends Superclass {void method () {}} public class BadDowncast {public static void main (String [] args) {Superclass superclass = new Superclass (); Underklasse underklasse = (Underklasse) superklasse; underklasse.metode (); }}
Oppføring 5 presenterer et klassehierarki bestående av Superklasse
og Underklasse
, som strekker seg Superklasse
. Dessuten, Underklasse
erklærer metode()
. En tredje klasse kalt Dårlig nedkastning
gir en hoved()
metode som instantierer Superklasse
. Dårlig nedkastning
prøver deretter å nedstøte dette objektet til Underklasse
og tilordne resultatet til variabel underklasse
.
I dette tilfellet vil ikke kompilatoren klage fordi nedkastning fra en superklasse til en underklasse i samme type hierarki er lovlig. Når det er sagt, hvis oppdraget var tillatt, ville applikasjonen krasje når den prøvde å utføre underklasse.metode ();
. I dette tilfellet ville JVM prøve å kalle en ikke-eksisterende metode, fordi Superklasse
erklærer ikke metode()
. Heldigvis bekrefter JVM at en rollebesetning er lovlig før den utfører en rollebesetning. Oppdager det Superklasse
erklærer ikke metode()
, det ville kaste a ClassCastException
gjenstand. (Jeg vil diskutere unntak i en fremtidig artikkel.)
Sammensett oppføring 5 som følger:
javac BadDowncast.java
Kjør den resulterende applikasjonen:
java BadDowncast