Serialisering

En kort repetition av paketet java.io och en introduktion till de vanligast använda delarna av serialiserings-API:et.

Litteratur

Mycket kort repetition av strömmar och filhantering

Teckenströmmar och byteströmmar

Paketet java.io innehåller två nästan likadana API:er. Ett hanterar 16-bitars tecken (char) och ett hanterar 8-bitars tecken (byte).

Det första API:et är lämpligt att använda för strömmar som hanterar text eftersom det klarar alla Unicode-tecken. Klasserna i detta API heter XXXReader och XXXWriter.De abstrakta basklasserna för alla strömmar i det är Reader och Writer. Dessa innehåller bland annat följande metoder, vilka som synes hanterar typen char:
Metoder i Reader, samtliga dessa hänger tills åtminstone ett tecken finns tillgängligt att läsa eller EOF nås (-1 returneras):
int read()  //Read a single character.
int read(char[] cbuf) //Read characters into an array.
int read(char[] cbuf, int off, int len)  //Read characters into a portion of an array.
Metoder i Writer:
void write(char[] cbuf)  //Write an array of characters.
void write(char[] cbuf, int off, int len)  //Write a portion of an array of characters.
void write(int c) //Write a single character.
void write(String str)  //Write a string.
void write(String str, int off, int len)  //Write a portion of a string.

Det andra API:et är lämpligast att använda för binärfiler (till exempel bilder och ljud). Klasserna heter XXXInputStream och XXXOutputStream. De abstrakta basklasserna av vilka de andra klasserna ärver heter InputStream och OutputStream. Som framgår nedan hanterar dessa typen byte.
Metoder i InputStream, samtliga dessa hänger tills åtminstone en byte finns tillgänglig att läsa eller EOF nås (-1 returneras):
int read()  //Reads the next byte of data from the input stream.
int read(byte[] b)  //Reads some number of bytes from the input stream and stores them into the buffer array b.
int read(byte[] b, int off, int len)  //Reads up to len bytes of data from the input stream into an array of bytes.
Metoder i OutputStream:
void write(byte[] b)  //Writes b.length bytes from the specified byte array to this output stream.
void write(byte[] b, int off, int len)  //Writes len bytes starting at offset off to this output stream.
void write(int b)  //Writes the specified byte to this output stream.

Strömmar som hanterar källor och sänkor kontra filterströmmar

En filterström är en ström som processar datat på något sätt och sedan skickar det vidare till en annan ström. Exempel på sådana strömmar är BufferedReader/BufferedInputStream som innehåller funktionalitet för att hantera mer än ett tecken/byte åt gången och PrintWriter/PrintStream som innehåller metoder lämpliga för utskrift.

En källa eller sänka är ett fysiskt lagringsutrymme dit programmet kan skicka data (sänka) eller hämta data från (källa). Det kan vara till exempel en fil, en pipe, minnet eller nätverket. För varje typ av källa/sänka finns det en specifik Reader och en InputStream som kan läsa data samt en Writer och en OutputStream som kan skriva data. 

Filhantering

En fil eller katalog kan representeras av klassen File.  Den innehåller en mängd metoder för att manipulera en fil eller katalog som till exempel skapa, ta bort , flytta ta reda på fullständig path. Dessutom innehåller den en del information om själva filsystemet, exempelvis name-separator ("/" i UNIX och "\" i Win32).

En inström från en fil består av objekt av klasserna FileReader eller FileInputStream. De innehåller huvudsakligen de metoder som ovan angetts för Reader och InputStream. Deras konstruktorer tar en källa i form av en File eller String som innehåller filens path. När en ström skapas pekar den på filens början, ska läsningen ske längre fram i filen kan ett angivet antal tecken hoppas över med hjälp av metoden skip(long n).

En utström till en fil består av objekt av klasserna FileWriter eller FileOutputStream. De innehåller huvudsakligen de metoder som ovan angetts för Writer och OutputStream, men även metoder för att flusha och stänga filen. Om filen inte tycks innehålla allt som programmet skrivit till den beror det ofta på att ingen av metoderna flush() eller close() anropats och att en del av utskriften därför faktiskt inte har kommit ut till filen utan ligger kvar i strömmens utbuffert. Konstruktorerna till FileWriter och FileOutputStream tar en källa i form av en File eller String som innehåller filens path. Om det redan finns en fil med angivet namn raderas den normalt och en ny skapas när ett objekt av dessa klasser instansieras. Om detta inte är önskvärt finns det konstruktorer som i stället öppnar den gamla filen och lägger till utskriften sist i den.

Vi avslutar denna repetition med ett program som läser från en fil (det första kommandoradsargumentet) och skriver samma sak till en annan (det andra kommandoradsargumentet), FileCopy.java.

Serialisering

Serielisering handlar om att skriva/läsa ett objekts tillstånd till/från en ström. Det som skrivs/läses är samtliga fält i objektet och i alla objekt som det skrivna/lästa objektets fält innehåller referenser till. Fält som är deklarerade static eller transient tas inte med. Syftet med att deklarera ett fält transient är just att undvika att det tas med om objektet streamas. För att ett objekt ska kunna streamas måste det implementera interfacet Serializable. Det är helt tomt, dess syfte är endast att fungera som bevis på att objektet får streamas.

Ett objekt skrivs/läses med hjälp av strömmarna ObjectOutputStream respektive ObjectInputStream. Eftersom det handlar om binärströmmar och aldrig kan vara fråga om textströmmar finns det inga "ObjectReader" eller "ObjectWriter". ObjectOutputStream och ObjectInputStream innehåller metoder för att skriva/läsa alla sorters primitiva typer samt metoderna writeObject() och readObject() vilka skriver respektive läser ett helt objekt. Deras konstruktorer tar en OutputStream respektive InputStream vilken är kopplad till önskad källa/sänka (de är alltså filterströmmar, se ovan).

Här kommer ett första exempel, det består av en länkad lista vilken sparas till en fil och läses igen. Observera att endast det första objektet i listan streamas genom ett explicit anrop av writeObject()/readObject(), de övriga objekten i listan kommer med automatiskt. WriteReadList.java

Styra hur serialiseringen sker

Om en klass innehåller metoden private void writeObject(java.io.ObjectOutputStream stream) throws IOException (den måste deklareras exakt så) kommer den att anropas när ett objekt av klassen ska skrivas. Den ObjectOutputStream metoden får som inparameter är den ström som objektet håller på att skrivas till. Det som skrivs till utströmmen i denna metod är alltså det enda som kommer att skrivas. Det finns möjlighet att skriva dels primitiva typer och andra objekt , dels att utföra den vanliga serialiseringen av det objekt metoden ligger i . Detta görs genom att anropa ObjectOuputStream.defaultWriteObject().

Motsvarande hantering kan ske när ett objekt läses om det objekt som läses innehåller metoden private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException. Här kommer ett exempel, CustomSerialization.java.

Det finns flera ytterligare möjligheter att styra hur ett objekt serialiseras, men det ingår inte i kursen. En utförlig redogörelse finns i specifikationen, se länk överst på denna sida.

Vad som egentligen händer

I mycket grova drag händer följande när ett objekt skrivs till en ström:
  1. Om objektet har skrivits förut skrivs bara en referens till det.
  2. Om det är första gången ett objekt av klassen streamas skrivs först information om själva klassen. Det som skrivs är bland annat klassens namn, en förteckning över dess serialiserbara fält och en long som räknas ut utifrån klassens definition (dvs definitionen av alla dess metoder och fält och av själva klassen).
  3. Om objektet är Serializable, dvs om någon av de klasser det tillhör implementerar Serializable, sparas fälten som är deklarerade i den klass högst upp i arvshierarkin som implementerar Serializable och i alla dess subklasser.
  4. I de klasser som har metoden writeObject(), se ovan, anropas den när fälten ska sparas enligt föregående punkt och det är då klassen själv som ansvarar för hur den sparas. Om inte anropas defaultWriteObject() i ObjectOutputStream och fälten skrivs till strömmen.
Läsning av ett objekt sker på motsvarande sätt.

Av detta kan flera intressanta slutsatser dras: