Den här artikeln innehåller en introduktion till konceptet autoboxing i Java 1.5. Men först värmer vi upp med Javas primitiva datatyper.
I Java finns det åtta primitiva datatyper som har en särställning i språket: boolean, short, int, long, char, byte, float och double. De kan användas som operander till de inbyggda operatorerna i språket som till exempel +, -, %, && och <=. Det finns ingen möjlighet att som programmerare lägga till nya datatyper som fungerar med dessa operatorer (vill man absolut göra det så kan man använda språket C++ istället för Java).
Värden av de primitiva datatyperna är inte objekt och det finns inget arv mellan datatyperna. Typen int är alltså inte subtyp till klassen Object. Ibland kan det dock vara användbart att hantera sanningsvärden, heltal och flyttal som objekt. Det vanligaste exemplet på när detta är användbart är för att lagra sådana värden i en lista eller hashtabell från Javas klassbibliotek. Dessa kan bara lagra objekt och det är därför omöjligt att stoppa in värdet från exempelvis en float-variabel. Som tur är finns det en så kallad wrapper-klass för var och en av de primitiva datatyperna. I följande tabell finns de primitiva datatyperna till vänster och deras respektive wrapper-klasser till höger.
boolean
|
Boolean
|
char
|
Character
|
byte
|
Byte
|
short
|
Short
|
int
|
Integer
|
long
|
Long
|
float
|
Float
|
double
|
Double
|
De flesta namnen på wrapper-klasserna är som synes lika som namnet på motsvarande datatyp, med skillnaden att klassens namn börjar med stor bokstav. Varför wrapper-klassen till int heter "Integer" istället för "Int" och wrapper-klassen till char heter "Character" istället för "Char" har jag aldrig riktigt förstått. Jag gissar att livet som programmerare skulle vara lite för lätt annars. Förmodligen har det bara råkat bli så någon gång i Javas barndom. Ett objekt av klassen Integer representerar alltså ett heltalsvärde mellan -2147483648 och 2147483647 och kan skapas så här:
Integer i = new Integer(12);
i = i + 1; // Ej tillåtet i Java 1.4
I exemplets andra rad försöker jag öka värdet på s med 1, men det går inte att göra så i Java 1.4, eftersom operatorn + inte kan användas på objekt. Istället får man göra så här:
i = new Integer(i.intValue() + 1);
Metoden intValue returnerar värdet på i som en int. Ganska klumpigt eller hur? Det viktiga att tänka på är att den primitiva datatypen int är en sak och klassen Integer är en helt annan. Och de båda används på olika sätt.
Java 1.5 gör livet lättare
Autoboxing i Java 1.5 är en mekanism som underlättar konvertering mellan primitiva datatyper och deras respektive wrapper-klasser. Lite förenklat kan man säga att man som programmerare kan sluta bry sig om skillnaden. När man försöker göra något som kräver en wrapper-klass med en primitiv datatyp konverteras den automatiskt. Och behövs konvertering åt andra hållet så görs även det automatiskt. Så här kan man göra:
Integer i = 12; // 12 (som är en int) konverteras automatiskt till ett Integer-objekt
int j = i + 1; // i konverteras automatiskt till primitiva datatypen int
myArrayList.add(j); // j konverteras automatiskt till ett Integer-objekt
Så enkelt kan det vara att använda autoboxing. Men det finns några små lurigheter som gör det nödvändigt att hålla ordning på vad som är objekt och vad som är primitiva datatyper.
Läckande abstraktioner
Joel Spolsky är en vis man som skrivit mycket nyttigt om systemutveckling. Han har formulerat lagen om läckande abstraktioner. Den säger att alla icke-triviala abstraktioner läcker i någon mån, vilket betyder att man måste förstå de underliggande mekanismerna för att kunna använda abstraktionen fullt ut.
Autoboxing i Java 1.5 är en läckande abstraktion. Abstraktionen är att programmeraren inte ska behöva bry sig om skillnaden mellan primitiva datatyper och wrapper-klasser. Men abstraktionen läcker. Programmeraren måste förstå hur autoboxing fungerar och vara medveten om skillnader mellan primitiva datatyper och wrapper-klasser för att kunna använda den. Det går ganska bra att blanda Integer och int, utan att tänka så mycket på det, men det finns några viktiga skillnader att känna till. Låt oss titta på ett exempel.
Bosses mystiska program
Programmeraren Bosse ska skriva ett program som kontrollerar om två tal är lika. Den första versionen av programmet ser ut så här:
int i1 = 500;
int i2 = 500;
if(i1 == i2) {
System.out.println("lika");
} else {
System.out.println("olika");
}
När Bosse kör programmet blir utskriften "lika". Glad av att ha skrivit ett så nyttigt program bestämmer han sig för att använda klassen Integer istället för den primitiva typen int. I övrigt lämnar han programmet orört.
Integer i1 = 500;
Integer i2 = 500;
if(i1 == i2) {
System.out.println("lika");
} else {
System.out.println("olika");
}
När han kör denna andra version av programmet blir dock utskriften "olika" och Bosse blir konfunderad. Han får då för sig att det är talet 500 för med sig otur och skriver därför en tredje version av programmet där han byter till talet 50. I övrigt är allt sig likt:
Integer i1 = 50;
Integer i2 = 50;
if(i1 == i2) {
System.out.println("lika");
} else {
System.out.println("olika");
}
Nu blir utskriften "lika" igen. En förvirrad Bosse bestämmer sig för att aldrig använda autoboxing igen. Det verkar ju bara för skumt.
Objekt har identitet
Varför blir det då så här? Jo, för att objekt har identitet. Det har inte de primitiva datatyperna. Att objekt har identitet innebär att det går att skapa flera objekt som representerar samma värde men som ändå är unika enheter. I version två av Bosses program skapas två stycken Integer-objekt som i1 och i2 refererar till. Objekten innehåller förvisso samma värde, men de är ändå unika enheter. Det som jämförs med operatorn == är huruvida i1 och i2 refererar till samma objekt, vilket de alltså inte gör. Om man vill jämföra om i1 och i2 innehåller samma värden får man använda metoden equals istället:
Integer i1 = 500;
Integer i2 = 500;
if(i1.equals(i2)) {
System.out.println("lika");
} else {
System.out.println("olika");
}
Men den tredje versionen av programmet gav ju utskriften "lika". Hur kan det komma sig? Jo, det finns ingen garanti för att olika Integer-objekt skapas vid autoboxning. Och för små tal (mellan -128 och +127) skapas inte unika objekt. Syftet med detta är i första hand att det inte ska skapas en massa olika Integer-objekt i onödan. På så sätt sparar man tid.
Referensvariabler kan ha värdet null
En annan viktig skillnad mellan primitiva typer och wrapper-klasser är att referensvariabler kan ha värdet null, vilket inte de primitiva datatyperna kan. Frågan är hur null då ska kunna konverteras till en int. Så här alltså:
Integer i = null;
int j = i; // i ska konverteras till en int, hur ska det gå?
Det som händer är att ett NullPointerException kastas. Helt rimligt.
En klurighet
En annan klurighet uppstår i och med att Java automatiskt konverterar mellan vissa primitiva datatyper. Följande exempel illustrerar detta:
long s = 0; // Fungerar, 0 konverteras från int till long
Integer i = 0; // Fungerar, 0 konverteras från int till Integer
Long s = 0; // Fungerar inte, kompileringsfel "incompatible types"
Long s = 0L; // Fungerar, 0L konverteras från long till Long
Det tredje fallet fungerar inte eftersom kompilatorn inte förstår att talet 0, som är av typen int, först kan konverteras till typen long och sedan till Long.
Prestanda
När ett primitivt värde autoboxas skapas ett wrapper-objekt. Detta tar tid och det kommer många använda som argument för att använda primitiva typer. Jag har gjort ett minitest för att se skillnaden. Först har vi en loop med primitiva typer:
long s = 0L;
for(int i = 0 ; i < 100000000 ; i++) {
s = s + i;
}
Och sedan samma loop med wrapper-klasser:
Long s = 0L;
for(Integer i = 0 ; i < 100000000 ; i++) {
s = s + i;
}
Den första versionen tar på min dator ca 0,58 sekunder och den andra tar 9,1 sekunder. Att använda de primitiva typerna var alltså 16 gånger snabbare i det här fallet.
Jag vet inte om det var Tony Hoare eller Donald Knuth som var först med att säga att "För tidig optimering är roten till allt ont", men jag vet att jag håller med. Naturligtvis ska man skapa en arkitektur som möjliggör den prestanda man behöver, men att jaga millisekunder vid additioner är oftast bara dumt. Det är något man bör göra där prestanda visat sig bli ett problem. Ett typiskt exempel skulle kunna vara i en loop som ska snurra 100000000 varv.
Slutsatser
Jag skulle vilja programmera som om allt var objekt och primitiva datatyper inte existerade. Som ett experiment skulle det vara kul att programmera Java 1.5 helt utan att deklarera några variabler eller parametrar som primitiva typer. Det borde inte ge några problem om man bara ser till att använda metoden equals för jämförelser. Den största risken med det är nog att konservativa programmerare kommer tycka att man är galen.
När jag tittar in i min kristallkula ser jag att framtidens språk som kommer ta över efter Java och C# inte har några primitiva datatyper. Allt är objekt. Listiga optimeringstekniker ger automatiskt god prestanda ändå, kanske genom att ha något som liknar primitiva typer under skalet.
Läs mer
Sun beskriver autoboxing här: http://java.sun.com/j2se/1.5.0/docs/guide/language/autoboxing.html.
Mitt favoritcitat från den artikeln är: "autoboxing and unboxing blur the distinction between primitive types and reference types, but they do not eliminate it".