Anta at du vil implementere en listeklasse i Java. Du starter med en abstrakt klasse, Liste
, og to underklasser, Tømme
og Ulemper
, som representerer henholdsvis tomme og ikke-frie lister. Siden du planlegger å utvide funksjonaliteten til disse listene, designer du en ListVisitor
grensesnitt, og gi aksepterer(...)
kroker for ListVisitor
s i hver av underklassene dine. Videre din Ulemper
klassen har to felt, først
og hvile
, med tilsvarende tilgangsmetoder.
Hva vil typene av disse feltene være? Helt klart, hvile
skal være av typen Liste
. Hvis du på forhånd vet at listene dine alltid vil inneholde elementer fra en gitt klasse, vil oppgaven med koding bli betydelig lettere på dette punktet. Hvis du vet at listeelementene dine alle vil være heltall
s, for eksempel, kan du tilordne først
å være av typen heltall
.
Men hvis du ikke, som ofte er tilfelle, ikke kjenner til denne informasjonen på forhånd, må du nøye deg med den minst vanlige superklassen som har alle mulige elementer i listene dine, som vanligvis er den universelle referansetypen. Gjenstand
. Derfor har koden din for lister med forskjellige elementelementer følgende skjema:
abstrakt klasseliste {offentlig abstrakt Objekt aksepter (ListVisitor det); } grensesnitt ListVisitor {public Object _case (Tøm det); public Object _case (Cons that); } klasse Empty extends List {public Object accept (ListVisitor that) {return that._case (this); }} class Cons utvider List {private Object først; privat liste resten; Ulemper (Object _first, List _rest) {first = _first; hvile = _rest; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }}
Selv om Java-programmerere ofte bruker den minst vanlige superklassen for et felt på denne måten, har tilnærmingen sine ulemper. Anta at du oppretter en ListVisitor
som legger til alle elementene i en liste over Heltall
s og returnerer resultatet, som illustrert nedenfor:
klasse AddVisitor implementerer ListVisitor {private Integer zero = new Integer (0); public Object _case (Empty that) {return zero;} public Object _case (Cons that) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (dette)). intValue ()); }}
Legg merke til de eksplisitte rollebesetningene til Heltall
i det andre _sak(...)
metode. Du utfører gjentatte ganger kjøretidstester for å kontrollere egenskapene til dataene; ideelt sett bør kompilatoren utføre disse testene for deg som en del av programtypekontroll. Men siden du ikke er garantert det AddVisitor
blir bare brukt på Liste
s av Heltall
s, kan Java-type kontrolløren ikke bekrefte at du faktisk legger til to Heltall
s med mindre rollebesetningene er til stede.
Du kan potensielt oppnå mer presis typekontroll, men bare ved å ofre polymorfisme og duplisere kode. Du kan for eksempel lage en spesiell Liste
klasse (med tilsvarende Ulemper
og Tømme
underklasser, samt en spesiell Besøkende
grensesnitt) for hver klasse element du lagrer i en Liste
. I eksemplet ovenfor vil du opprette en Heltalliste
klasse hvis elementer er alle Heltall
s. Men hvis du ville lagre, si, Boolsk
s et annet sted i programmet, må du opprette en Boolsk liste
klasse.
Det er klart at størrelsen på et program skrevet med denne teknikken vil øke raskt. Det er også flere stilistiske spørsmål; et av de viktigste prinsippene for god programvareteknikk er å ha ett enkelt kontrollpunkt for hvert funksjonelle element i programmet, og duplisering av kode på denne måten å kopiere og lime bryter med dette prinsippet. Dette fører ofte til høye programvareutvikling og vedlikeholdskostnader. For å se hvorfor, bør du vurdere hva som skjer når en feil blir funnet: programmereren må gå tilbake og rette den feilen separat i hver kopi. Hvis programmereren glemmer å identifisere alle dupliserte nettsteder, vil en ny feil bli introdusert!
Men som eksemplet ovenfor illustrerer, vil du finne det vanskelig å samtidig beholde et enkelt kontrollpunkt og bruke statiske type brikker for å garantere at visse feil aldri vil oppstå når programmet kjøres. I Java, slik den eksisterer i dag, har du ofte ikke annet valg enn å duplisere kode hvis du vil ha presis statisk typekontroll. For å være sikker, kan du aldri eliminere dette aspektet av Java helt. Enkelte postulater av automatteori, tatt til deres logiske konklusjon, innebærer at ingen lydtypesystemer kan bestemme nøyaktig settet med gyldige innganger (eller utganger) for alle metodene i et program. Derfor må alle typer systemer finne en balanse mellom sin egen enkelhet og uttrykksevnen til det resulterende språket; Java-typesystemet lener seg litt for mye i retning av enkelhet. I det første eksemplet ville et litt mer uttrykksfullt typesystem la deg opprettholde presis typekontroll uten å måtte duplisere koden.
Et slikt uttrykksfullt system ville tilføyet generiske typer til språket. Generiske typer er typevariabler som kan instantieres med en passende spesifikk type for hver forekomst av en klasse. I forbindelse med denne artikkelen vil jeg erklære typevariabler i vinkelparenteser over klasse- eller grensesnittdefinisjoner. Omfanget av en typevariabel vil da bestå av hoveddelen av definisjonen som den ble erklært for (ikke inkludert strekker
klausul). Innenfor dette omfanget kan du bruke typevariabelen hvor som helst du kan bruke en vanlig type.
For eksempel, med generiske typer, kan du skrive om Liste
klasse som følger:
abstrakt klasseliste {offentlig abstrakt T godta (ListVisitor at); } grensesnitt ListVisitor {public T _case (Tøm det); public T _case (Cons that); } klasse Empty extends List {public T accept (ListVisitor that) {return that._case (this); }} klasse Ulemper utvider Liste {privat T først; privat liste resten; Ulemper (T _forst, List _rest) {first = _first; hvile = _rest; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }}
Nå kan du skrive om AddVisitor
for å dra nytte av generiske typer:
klasse AddVisitor implementerer ListVisitor {private Integer zero = new Integer (0); public Integer _case (Empty that) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }}
Legg merke til at de eksplisitte kastene til Heltall
er ikke lenger nødvendig. Argumentet at
til det andre _sak(...)
metoden er erklært å være Ulemper
, instantere typevariabelen for Ulemper
klasse med Heltall
. Derfor kan den statiske typen kontrolløren bevise det that.first ()
vil være av typen Heltall
og det that.rest ()
vil være av typen Liste
. Lignende instantiations ville bli gjort hver gang en ny forekomst av Tømme
eller Ulemper
blir erklært.
I eksemplet ovenfor kan typevariablene instantieres med hvilken som helst Gjenstand
. Du kan også gi en mer spesifikk øvre grense til en typevariabel. I slike tilfeller kan du spesifisere denne avgrensningen ved typevariabelens erklæringspunkt med følgende syntaks:
strekker
For eksempel hvis du vil ha din Liste
s bare å inneholde Sammenlignelig
objekter, kan du definere de tre klassene dine som følger:
klasseliste {...} klassefall {...} klasse tom {...}
Selv om det å legge parametrerte typer til Java til å gi deg fordelene som er vist ovenfor, ville det ikke være verdt å gjøre det hvis det betydde å ofre kompatibilitet med eldre kode i prosessen. Heldigvis er ikke et slikt offer nødvendig. Det er mulig å automatisk oversette kode, skrevet i en utvidelse av Java som har generiske typer, til bytekode for eksisterende JVM. Flere kompilatorer gjør allerede dette - Pizza- og GJ-kompilatorene, skrevet av Martin Odersky, er spesielt gode eksempler. Pizza var et eksperimentelt språk som tilførte flere nye funksjoner til Java, hvorav noen ble innlemmet i Java 1.2; GJ er en etterfølger til Pizza som bare legger til generiske typer. Siden dette er den eneste funksjonen som er lagt til, kan GJ-kompilatoren produsere bytekode som fungerer jevnt med eldre kode. Den kompilerer kilde til bytekode ved hjelp av sletting, som erstatter hver forekomst av hver type variabel med den variabelens øvre grense. Det lar også typevariabler deklareres for spesifikke metoder, snarere enn for hele klasser. GJ bruker den samme syntaksen for generiske typer som jeg bruker i denne artikkelen.
Arbeid pågår
Ved Rice University implementerer programmeringsspråk teknologigruppen der jeg jobber en kompilator for en oppoverkompatibel versjon av GJ, kalt NextGen. NextGen-språket ble utviklet i fellesskap av professor Robert Cartwright fra Rices informatikkavdeling og Guy Steele fra Sun Microsystems; det legger til muligheten til å utføre kjøretidskontroller av typevariabler til GJ.
En annen potensiell løsning på dette problemet, kalt PolyJ, ble utviklet på MIT. Den utvides på Cornell. PolyJ bruker en litt annen syntaks enn GJ / NextGen. Det skiller seg også litt ut i bruken av generiske typer. For eksempel støtter den ikke typeparameterisering av individuelle metoder, og støtter foreløpig ikke indre klasser. Men i motsetning til GJ eller NextGen, tillater det typevariabler å bli instantiert med primitive typer. I likhet med NextGen støtter PolyJ kjøretidsoperasjoner på generiske typer.
Sun har gitt ut en Java Specification Request (JSR) for å legge til generiske typer til språket. Ikke overraskende er et av nøkkelmålene som er oppført for innlevering, å opprettholde kompatibilitet med eksisterende klassebiblioteker. Når generiske typer legges til Java, er det sannsynlig at et av forslagene diskutert ovenfor vil tjene som prototype.
Det er noen programmerere som er imot å legge til generiske typer i alle former, til tross for fordelene. Jeg vil referere til to vanlige argumenter for slike motstandere som "malene er onde" -argumentet og "det er ikke objektorientert" -argumentet, og ta opp hver av dem etter tur.
Er maler onde?
C ++ bruker maler å gi en form for generiske typer. Maler har oppnådd et dårlig rykte blant noen C ++ - utviklere fordi deres definisjoner ikke er typekontrollert i parameterisert form. I stedet blir koden replikert ved hver instantiering, og hver replikering er typekontrollert separat. Problemet med denne tilnærmingen er at det kan forekomme typefeil i den opprinnelige koden som ikke vises i noen av de første instantieringene. Disse feilene kan manifestere seg senere hvis programrevisjoner eller utvidelser introduserer nye instantiations. Se for deg frustrasjonen til en utvikler som bruker eksisterende klasser som skriver sjekk når de er samlet av seg selv, men ikke etter at han legger til en ny, helt legitim underklasse! Enda verre, hvis malen ikke kompileres sammen sammen med de nye klassene, vil slike feil ikke bli oppdaget, men vil i stedet ødelegge kjøringsprogrammet.
På grunn av disse problemene, rynker noen mennesker på å bringe maler tilbake, og forventer at ulempene med maler i C ++ vil gjelde for et generisk system i Java. Denne analogien er misvisende, fordi de semantiske grunnlagene til Java og C ++ er radikalt forskjellige. C ++ er et usikkert språk der statisk typekontroll er en heuristisk prosess uten matematisk grunnlag. I motsetning til dette er Java et trygt språk der den statiske typen kontrolløren bokstavelig talt viser at visse feil ikke kan oppstå når koden kjøres. Som et resultat lider C ++ - programmer som involverer maler, utallige sikkerhetsproblemer som ikke kan oppstå i Java.
Videre utfører alle de fremtredende forslagene til en generisk Java eksplisitt statisk typekontroll av de parametriserte klassene, i stedet for bare å gjøre det ved hver instantiering av klassen. Hvis du er bekymret for at en slik eksplisitt kontroll vil redusere typekontrollen, kan du være trygg på at det faktisk er motsatt: siden typekontrollen bare gir ett pass over den parametriserte koden, i motsetning til et pass for hver instantiering av parametrerte typer, blir typekontrollprosessen fremskyndet. Av disse grunner gjelder ikke de mange innvendinger mot C ++ - maler for generiske forslag til Java. Faktisk, hvis du ser utover det som er mye brukt i bransjen, er det mange mindre populære, men veldig godt utformede språk, som Objective Caml og Eiffel, som støtter parametriserte typer til stor fordel.
Er generiske typesystemer objektorientert?
Til slutt motsetter noen programmerere seg mot et hvilket som helst system av generisk type med den begrunnelsen at fordi slike systemer opprinnelig ble utviklet for funksjonelle språk, er de ikke objektorientert. Denne innsigelsen er falsk. Generiske typer passer veldig naturlig inn i et objektorientert rammeverk, som eksemplene og diskusjonen ovenfor viser. Men jeg mistenker at denne innvendingen er forankret i mangel på forståelse av hvordan man kan integrere generiske typer med Javas arvspolymorfisme. En slik integrering er faktisk mulig, og er grunnlaget for implementeringen av NextGen.