Programmering

Java polymorfisme og dens typer

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:

  1. 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.
  2. 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).
  3. 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 en dobbelt verdi) og en metode kan returnere a Dobbelt i ett objekt, og det samme feltet kan være av typen String og den samme metoden kan returnere a String i et annet objekt. Java støtter parametrisk polymorfisme via generiske, som jeg vil diskutere i en fremtidig artikkel.
  4. 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 en tegne() metode; ved å introdusere Sirkel, Rektangel, og andre underklasser som overstyrer tegne(); ved å introdusere en rekke typer Form hvis elementer lagrer referanser til Form underklasseforekomster; og ved å ringe Forms tegne() metode for hver forekomst. Når du ringer tegne(), det er Sirkel's, Rektangeleller annet Form forekomst er tegne() metoden som blir kalt. Vi sier at det er mange former for Forms tegne() 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 Formgrensesnitt. Å 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: Forms tegne() metode eller Sirkels 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 Sirkels tegne() metoden som skal kalles. Når Jeg er lik 1imidlertid forårsaker denne instruksjonen Rektangels 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øysine underklasser vil overstyre bevege seg() og gi en passende beskrivelse. De ville også arve metodene og deres konstruktører ville kalle Kjøretøysin 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 Sirkels getRadius () metode. Det er imidlertid mulig å få tilgang til igjen Sirkels 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
$config[zx-auto] not found$config[zx-overlay] not found