Generics i Java 1.5
Förord
I den här artikeln beskriver jag vad generics är och hur det fungerar i Java 1.5.
Vad är generics?
Vad är generics?
Generics är den nyhet i Java 1.5 som är mest efterlängtad. På svenska heter det "typparametrisering", det hela handlar om att skapa en ny typ genom att ange en typ som parameter till en annan typ. Men låt oss börja med något begripligt, nämligen hur vi har klarat oss utan generics hittills.
Sedan länge innehåller paketet java.util många bra datastrukturer att lagra objekt med, som exempelvis ArrayList, HashMap, HashSet, LinkedList och Stack. Alla dessa olika datastrukturer för lagring är bra och nödvändiga. Dessutom kommer det alltid finnas behov av att hitta på nya applikationsspecifika datastrukturer. Före version 1.5 är datastrukturerna dåliga på att hålla ording på vad de lagrar. De kan lagra allt som är subklasser till Object, och eftersom alla klasser i Java är subklasser till Object kan de lagra vilka objekt som helst. För att lägga till och hämta ut objekt ur en List kan man använda följande metoder:
void add(Object o)
Object get(int index)
Eftersom add-metoden accepterar alla subklasser till Object som parameter går det utmärkt att lägga in bil-objekt i listan. Om man vill använda get-metoden till att hämta ut en bil är det inte riktigt lika enkelt. Eftersom get-metoden har returvärde av typen Object måste man typkonvertera returvärdet innan det kan användas som en bil. Exempel:
Bil b = (Bil) billistan.get(42);
Att med parentesen (Bil) tala om för datorn att det man får ut ut billistan verkligen är ett bilobjekt kan ju kännas lite löjligt om man själv vet att man bara stoppat in just bilar. Ännu löjligare känns det eftersom man vet att kompilatorn inte vet någonting om vad som finns i listan utan nöjer sig med en parentes som säger vad programmeraren påstår finns där. Kompilatorn har alltså ingen chans att protestera med ett "Hörrödudu, det kan faktiskt finnas cyklar och spårvagnar i din lista". Istället upptäcks sådana fel först när programmet körs, ett exception kastas då och äntligen får man veta att där kryllar av cyklar och spårvagnar. Att typfel inte upptäcks vid kompileringen är ett problem eftersom det ökar arbetet med att hitta en sådan bugg samtidigt som risken att en den inte upptäcks alls under utvecklingsarbetet ökar.
Ett sätt att lösa problemet är att skapa en klass som heter BilLista och som klarar av att lagra just bilar i en lista. Denna blir helt typsäker och det kan verifieras av kompilatorn. Typsäkerheten kommer av att metoderna för att lägga till och hämta ut bilar deklareras för att ta emot respektive returnera just bil-objekt:
void add(Bil b)
Bil get(int index)
Om någon övermodig programmerare nu försöker stoppa in en ynka liten spårvagn i listan kommer kompilatorn straffa denne direkt. En spårvagn är inte en bil och metoden add fungerar bara med bilar! Den stora nackdelen med att skapa en sådan klass är att det är jobbigt och sedan är det ännu jobbigare att underhålla den. Det skulle väl gå an om det bara behövdes några stycken sådana datastrukturer i ett system, men så är det inte. Datastrukturer för att lagra objekt är vanliga. Jättevanliga.
Arrayer, som till exempel Bil[], är ju också typsäkra. Kan man inte använda dem om nu typsäkerhet är så viktigt? Den enda nackdelen med dem är ju att de är just arrayer, med allt vad det innebär: det går inte att ändra antalet element som kan lagras, det är besvärligt att skjuta in ett objekt i mitten, för att hitta ett objekt kan man tvingas söka igenom hela arrayen etc. Det går inte att byta detta till lagring i länkad-lista, hashtabell, mängd etc. Arrayer härstammar från en tid då komplicerade datastrukturer var en lyx och det var en stor sak att skapa en hashtabell. Idag är hashtabeller vardagsmat.
OK, sammanfattning av problemet: Man vill på ett lätt sätt kunna skapa lagringsplatser för objekt. Hur objekten ska lagras ska kunna bestämmas från fall till fall och det ska vara lätt att ändra och att hitta på nya lagringssätt. Det ska också gå att bestämma vilken sorts objekt som ska lagras och inget annat än sådana objekt ska kunna stoppas in av misstag. Vi har alltså två dimensioner som ska kunna varieras oberoende av varandra: datastruktur respektive typ på det som ska lagras. Och det är alltså detta problem som kan lösas med generics i Java 1.5.
Java 1.5 löser problemet
Java 1.5 löser problemet
Så här gör man med Java 1.5:
ArrayList minLista = new ArrayList();
Genom att låta ett klassnamn följas av större-än-och-mindre-än-hakar med ett typnamn mellan skapar man en ny så kallad parametriserad typ. Här skapar vi en ArrayList som kan lagra bilar. Det fiffiga är att för den nya typen som skapats kan man använda följande metoder:
void add(Bil b)
Bil get(int index)
Det innebär att det bara går att fylla listan med bilar. Vi har statisk typkontroll. Hurra! Kompilatorn kan säga till när vi gjort fel och vi slipper jaga buggar lika ofta som förut. Det går att använda de parametriserade typerna ungefär som vilka typer som helst, till exempel är följande metoddeklarationer möjliga:
public void parkera(ArrayList b) {...}
public List getEvents() {...}
Även om det mest kommer vara utvecklare av klassbibliotek som skapar parametriserade typer så kan det vara kul att se hur det går till. Låt oss skapa ett liten fånig typparametriserad klass som kan lagra ett endaste litet objekt. Så här kan den se ut:
class SillyStorage {
private T theObject;
public void set(T o) {theObject = o;}
public T get() {return theObject;}
}
Att klassen deklareras som class SillyStorage
SillyStorage s;
s = new SillyStorage();
s.set(new Bil());
Bil b = s.get();
Annat än datastrukturer?
Annat än datastrukturer?
Hittills har jag låtsats som om typparametrisering är något som bara är till för att kunna använda olika datastrukturer för att lagra data. Och det kommer utan tvekan vara det vanligaste användningsområdet för generics i Java. Jag kan avslöja att det går att typparametrisera vilka klasser som helst. Men generics i Java är inte speciellt användbara om man vill göra något spännande med sin typparameter, som till exempel att skapa ett objekt med den. Det är inte tillåtet att ändra den första raden i SillyStorage så här:
private T theObject = new T(); // Ej tillåtet
Om man vill anropa metoder på theObject är man begränsad till metoder som finns i klassen Object, eftersom kompilatorn inte förstår när andra anrop är korrekta. Det går att mildra detta problem genom att sätta begränsningar på vilken typ T får vara så här:
class SillyStorage {
...
Detta betyder att typparametern måste vara Fordon eller någon subklass till Fordon. Nu är man bara begränsad till metoder som finns i klassen Fordon. Dessa begränsningar gör att generics i Java lämpar sig bäst för klasser som inte behöver veta speciellt mycket om typparametern. Detta begränsar. Jag har svårt att se hur det kan användas till mycket annat än till datastrukturer och andra saker som hanterar objekt utan att göra något roligt med dem.
Generics är komplext
Generics är komplext
I den här beskrivningen har jag bara förklarat grundprinciperna för generics. Det finns mycket mer att veta, som till exempel att man kan typparametrisera metoder och att man kan använda jokertecken i vid typparametrisering (en variabel som deklareras som ArrayList extends Fordon> kan referera till alla sorts ArrayListor av Fordon och dess subtyper). När man leker vidare med generics inser man att det kan bli ganska komplext. Mina exempel hittills har ju varit ganska enkla, men det finns gott om möjligheter att bli förvirrad. Till exempel kan man intuitivt tro att en metod som förväntar sig något av typen ArrayList
public static > T
max(Collection extends T> coll) {...}
Att få starkare typkontroll är bra. Det låter oss hitta vissa fel redan vid kompileringstillfället. Frågan är om man inte på köpet fått ett annat stort problem: komplexitet. Kod som innehåller generics kan vara svårare att läsa och förstå än kod utan. Jag ser en risk att den ökade komplexiteten i språket kommer leda till att färre programmerare verkligen kommer behärska Java. Något som kan straffa sig i form av fler fel i programmen, vilket ju är precis vad generics ska motverka. Men förhoppningsvis räcker det för vanliga dödliga programmerare att förstå hur man skapar länkade listor med bilar och spårvagnar. De besvärliga detaljerna blir då bara något extra spännande att bita i för klassbibliotekskonstruktörer. Man kan ju alltid hoppas.
Titta gärna in till min blogg: www.jensgustavsson.se
Simon Dahlbacka
Mycket intressant ämne, och väldigt välskriven. Misstänker att jag återkommer till denna när jag jobbar med java nästa gång.
Johan Kristensen
Jag håller inte riktigt med om att komplexiteten skulle vara ett problem. Välskrivna generiska klassbibliotek gör istället klientkoden enklare att läsa. Och t.o.m exemplet på metoddeklaration du visar i slutet är ganska logisk. Jag undrar dock om det verkligen är nödvändigt att ange att T är en subklass till Object.