Indhold:
Hente og gemme objekter i en fil
Gemme egne klasser – interfacet Serializable og nøgleordet transient
Kapitlet forudsættes i kapitel 19, RMI.
Forudsætter kapitel 15, Datastrømme og filhåndtering (kapitel 12, Interfaces og kapitel 7 om klassemetoder er en fordel).
Når et program afslutter, kan det være, at man ønsker at gemme data til næste gang, programmet starter.
Man kan selvfølgelig skrive programkode, der gemmer og indlæser alle variablerne i de objekter, der skal huskes, men der findes en nemmere måde.
Java har en mekanisme, kaldet serialisering, der består i, at objekter kan omdannes til en byte-sekvens (med datastrømmen ObjectOutputStream), der f.eks. kan skrives til en fil1. Denne bytesekvens kan senere, når man har brug for objekterne igen, deserialiseres (gendannes i hukommelsen med datastrømmen ObjectInputStream). Dette kunne f.eks. ske, når programmet starter næste gang.
Her er en klasse med to klassemetoder, der henter og gemmer objekter i en fil:
import java.io.*;
public class Serialisering
{
public static void gem(Object obj, String filnavn) throws IOException
{
FileOutputStream datastrøm = new FileOutputStream(filnavn);
ObjectOutputStream objektstrøm = new ObjectOutputStream(datastrøm);
objektstrøm.writeObject(obj);
objektstrøm.close();
}
public static Object hent(String filnavn) throws Exception
{
FileInputStream datastrøm = new FileInputStream(filnavn);
ObjectInputStream objektstrøm = new ObjectInputStream(datastrøm);
Object obj = objektstrøm.readObject();
objektstrøm.close();
return obj;
}
}
Du kan benytte klassen fra dine egne programmer. Her er et program, der læser en liste fra filen venner.ser2, tilføjer en indgang og gemmer listen i filen igen.
import java.util.ArrayList;
public class HentOgGem
{
public static void main(String[] arg) throws Exception
{
ArrayList<String> l;
try {
l = (ArrayList<String>) Serialisering.hent("venner.ser");
System.out.println("Læst: "+l);
} catch (Exception e) {
l = new ArrayList();
l.add("Jacob");
l.add("Brian");
l.add("Preben");
System.out.println("Oprettet: "+l);
}
l.add("Ven"+l.size());
Serialisering.gem(l,"venner.ser");
System.out.println("Gemt: "+l);
}
}
Oprettet: [Jacob, Brian, Preben]
Gemt: [Jacob, Brian, Preben, Ven3]
Første gang, programmet kører, opstår der en exception, fordi filen ikke findes. Den fanger vi og føjer "Jacob", "Brian" og "Preben" til listen. Derpå tilføjer vi "Ven3" og gemmer listen.
Næste gang er uddata:
Læst: [Jacob, Brian, Preben, Ven3]
Gemt: [Jacob, Brian, Preben, Ven3, Ven4]
Køres programmet igen, ser man, at der hver gang tilføjes en indgang:
Læst: [Jacob, Brian, Preben, Ven3, Ven4]
Gemt: [Jacob, Brian, Preben, Ven3, Ven4, Ven5]
Læst: [Jacob, Brian, Preben, Ven3, Ven4, Ven5]
Gemt: [Jacob, Brian, Preben, Ven3, Ven4, Ven5, Ven6]
Læst: [Jacob, Brian, Preben, Ven3, Ven4, Ven5, Ven6]
Gemt: [Jacob, Brian, Preben, Ven3, Ven4, Ven5, Ven6, Ven7]
Læst: [Jacob, Brian, Preben, Ven3, Ven4, Ven5, Ven6, Ven7]
Gemt: [Jacob, Brian, Preben, Ven3, Ven4, Ven5, Ven6, Ven7, Ven8]
Hvis nogle af de serialiserede objekter indeholder datafelter, der er referencer til andre objekter, serialiseres disse også. Ovenfor så vi, at hvis man serialiserer en liste, bliver elementerne i listen også serialiseret. Dette gælder også, hvis disse elementer selv indeholder eller er lister og så fremdeles og så kan et helt netværk af objekter, med indbyrdes referencer, blive serialiseret. Man skal derfor være lidt påpasselig i sine egne programmer, det kan være, at man får for meget med.
Det er ikke alle klasser, der må/kan serialiseres. For eksempel giver det ikke mening at serialisere en datastrøm til en forbindelse over netværket (eller bare til en fil). Hvordan skulle systemet genskabe en netværksforbindelse, der har været gemt på harddisken i tre uger? Den anden ende af netværksforbindelsen vil formentlig være væk på det tidspunkt.
Serializable bruges til at markere, at objekter godt må serialiseres. Hvis en klasse implementerer Serializable, ved Java, at objekter af denne type godt kan serialiseres.
Derfor implementerer f.eks. ArrayList, Point, String og andre objekter beregnet til at holde data Serializable-interfacet, mens f.eks. FileWriter og Socket ikke gør, da de netop ikke må serialiseres (en Socket repræsenterer jo en netværksforbindelse til et program på en anden maskine og denne forbindelse ville alligevel være tabt, når objektet blev deserialiseret).
Prøver man at serialisere et objekt, der ikke implementerer Serializable, kastes NotSerializableException og serialiseringen afbrydes.
I interfacet Serializable er der ikke nogen metoder erklæret og det er derfor helt uforpligtende at implementere. Sådan et interface kaldes også et markeringsinterface, da det kun tjener til at markere klasser som, at man kan (eller ikke kan) gøre noget bestemt med dem.
Ud over, at der kan findes objekt-typer, som overhovedet ikke kan serialiseres, kan det også ske, at der er visse dele af et objekts data, man ikke ønsker serialiseret. Hvis et objekt indeholder midlertidige data (f.eks. fortrydelses-information i et tekstbehandlingsprogram), kan man markere de midlertidige datafelter i klassen med nøgleordet transient.
Hvis du senere ændrer klassen og prøver at indlæse objekter gemt med den gamle udgave af klassen vil det gå galt! Det kan du løse ved at indsætte et serialVersionUID i klassen, som vist nedenfor. Så vil Java indlæse objektet og sætte evt. nye variabler til nul.
Eksemplet herunder viser en klasse, der kan serialiseres (implements Serializable), med en transient variabel (tmp). Hvis objektet serialiseres, vil a blive gemt, men tmp vil ikke.
Af bekvemmelighedsgrunde er der også lavet en toString()-metode.
import java.io.*;
public class Data implements Serializable
{
public int a;
public transient int tmp; // transiente data bliver ikke serialiseret
// Vigtigt: Sæt versionsnummer så objekt kan læses selvom klassen er ændret!
private static final long serialVersionUID = 12345; // bare et eller andet nr.
public String toString()
{
return "Data: a="+a+" tmp="+tmp;
}
}
Her er et program, der læser en liste af Data-objekter, tilføjer et og gemmer den igen:
import java.util.ArrayList;
public class HentOgGemData
{
public static void main(String[] arg) throws Exception
{
ArrayList<Data> l;
try {
l = (ArrayList<Data>) Serialisering.hent("data.ser");
System.out.println("Læst: "+l);
} catch (Exception e) {
l = new ArrayList<Data>();
System.out.println("Oprettet: "+l);
}
Data d = new Data();
d.a = (int) (Math.random()*100);
d.tmp = (int) (Math.random()*100);
l.add(d);
System.out.println("Gemt: "+l);
Serialisering.gem(l,"data.ser");
}
}
Oprettet: []
Gemt: [Data: a=88 tmp=2]
Køres programmet igen, fås:
Læst: [Data: a=88 tmp=0]
Gemt: [Data: a=88 tmp=0, Data: a=10 tmp=10]
Læst: [Data: a=88 tmp=0, Data: a=10 tmp=0]
Gemt: [Data: a=88 tmp=0, Data: a=10 tmp=0, Data: a=52 tmp=96]
Læst: [Data: a=88 tmp=0, Data: a=10 tmp=0, Data: a=52 tmp=0]
Gemt: [Data: a=88 tmp=0, Data: a=10 tmp=0, Data: a=52 tmp=0, Data: a=78 tmp=88]
Læg mærke til, at den transiente variabel tmp ikke bliver husket fra gang til gang.
Kør HentOgGemData nogle gange og se,
at den husker data i en fil. Kig i venner.ser.
Tilføj et
ekstra felt til Data.java, oversæt og kør programmet.
Hvad sker der? Hvorfor?
Slet serialVersionUID
og prøv igen. Hvad sker der? Hvorfor?
Ændr matadorspillet afsnit 5.3 sådan, at felterne og de to spillere gemmes i en fil (serialiseret ned i samme datastrøm), når de 20 runder er gået. Lav mulighed for at indlæse den serialiserede fil, så man kan spille videre på et senere tidspunkt.
Udvid programmet til, at brugeren angiver filnavnet, der skal hentes/gemmes i.
1Eller netværket for den sags skyld.
2Man bruger ofte filendelsen .ser til serialiserede objekter.