© 1999-2003, Flemming Koch Jensen
Alle rettigheder forbeholdt
Serialisering af objekter

 

 

Seriel = Sekventiel Serialisering af et objekt, er en afbildning af et objekt over i en sekvens af bytes. Vi kan bruge serialisering til at gemme objekter som data og dermed opnå persistens. Ikke alene får vi mulighed for at gemme objekter i filer, men vi kan også sende objekter over et netværk ved at overføre sekvensen af bytes.
Det er relativ enkelt at serialisere et objekt. Det kræver blot at klassen implementerer interfacet Serializable (fra java.io).
import java.io.*;

class A implements Serializable {
  ...
}
Dette interface indeholder ingen metoder, og tjener kun som en markering af, at instanser af klassen må serialiseres.
Kun navne på klasser Man skal også være opmærksom på, at det er objekter der serialiseres - ikke klasser! Det betyder at den der læser/modtager et serialiseret objekt selv skal kende klassen, dvs. have den tilhørende class-file. De eneste informationer vedr. klasser, der optræder i serialiseringen af et objekt, er navnet på de klasser som objekterne er instanser af. Da klasser ikke inkluderes i serialiseringen bliver klasse-variable (static variable) heller ikke serialiseret.
Selve serialiseringen foretages af JVM, og man skal normalt ikke bekymre sig om dette, med mindre der er specielle egenskaber ved klassen, der gør Java's egen serialisering uhensigtsmæssig. Det er muligt selv at implementere serialiseringen, men først vil vi se hvordan man kan bruge Java's egen default-serialisering.

 

1. Default-serialisering

Lad os se et eksempel hvor vi serialiserer et objekt, gemmer det i en file og indlæser det igen.
Først har vi klassen A, som vi vil gemme en instans af:
import java.io.*;

class A implements Serializable {
  private int x;
  private String s;
  private Node list;
  
  public A( int x, String s, Node list ) {
    this.x = x;
    this.s = s;
    this.list = list;
  }
  
  public String toString() {
    return "[A: x=" + x + ", s=\"" + s + "\", list=" + list + "]";
  }
}
Det interessante er datakernen. Vi har valgt en instansvariabel af en primitiv type, en instans af en af Java's egne klasser: String, og en linket liste af knuder vi selv implementerer. Disse tre er valgt for at illustrere styrken i Java's serialisering.
Dernæst har vi Node-klassen:
import java.io.*;

class Node implements Serializable {
  private int x;
  private Node next;
  
  public Node( int x, Node next ) {
    this.x = x;
    this.next = next;
  }
  
  public String toString() {
    String s = "[Node: x=" + x + "]";
    
    if ( next != null )
      s += next;
    
    return s;
  }
}
Deep copy Vi har gjort Node Serializable. Når JVM serialiserer et objekt, stopper den nemlig ikke med objektet selv, der fortsættes rekursivt med de objekter som objektet har referencer til. Dette kan i termer af Prototype Pattern betegnes som deep copy. Man kunne tænke sig denne propagering af kopieringsprocessen lavet vha. Composite Pattern, men da JVM kan håndtere objektsystemer med cycler, er der formodentlig ikke tale om en distribueret kopiering.
Støder JVM på et objekt, under serialiseringen, som ikke er Serializable kastes der en NotSerializableException.
Lad os se en testanvendelse, der bla. demonstrerer deep copy:
import java.io.*;

public class Main {

  public static void main( String[] argv ) {
    
    Node list=null;
    for ( int i=0; i<5; i++ )
      list = new Node( i, list );
  
    A a = new A( 5, "fem", list );
    
    try {
      writeObjectToFile( "A.ser", a );
      
      Object obj = readObjectFromFile( "A.ser" );
      
      System.out.println( a );
      System.out.println( obj );
    }
    catch ( Exception e ) {
      e.printStackTrace();
    }
  }
  
  private static void writeObjectToFile( String filename, Serializable obj )
    throws Exception
  {
    FileOutputStream fileOut = new FileOutputStream( filename );
    ObjectOutputStream objectOut = new ObjectOutputStream( fileOut );
    
    objectOut.writeObject( obj );
    
    fileOut.close();
  }
  
  private static Object readObjectFromFile( String filename ) throws Exception {
    FileInputStream fileIn = new FileInputStream( filename );
    ObjectInputStream objectIn = new ObjectInputStream( fileIn );
  
    Object obj = objectIn.readObject();
  
    fileIn.close();
    
    return obj;
  }
}
[A: x=5, s="fem", list=[Node: x=4][Node: x=3][Node: x=2][Node: x=1][Node: x=0]]
[A: x=5, s="fem", list=[Node: x=4][Node: x=3][Node: x=2][Node: x=1][Node: x=0]]
Object-Streams Vi har lavet to generelle metoder til henholdsvis at udskrive og indlæse et objekt fra en file. Til selve serialiseringen anvender man en ObjectOutputStream, og en ObjectInputStream (til de-serialiseringen). Disse streams har read- og write-metoder der er vores interface til serialisering.
Listen bevaret Bemærk specielt at hele den linkede liste er bevaret, selvom vi kun direkte har bedt vores ObjectOutputStream om at serialiserer A-objektet. Deep copy har bevaret hele listen.
Deep copy kan være en farlig ting. F.eks. kan det gå gruelig galt hvis man begynder at serialisere objekter der er en del af Swing - f.eks. en JFrame. Alt hvad denne frame indirekte refererer til vil blive kopieret med - det kan være meget!
Versions-problemer Ændrer man i en klasse, er det ikke længere muligt at indlæse persistente objekter, der var instanser af en tidligere version af klassen. Dette kan speciel få betydning for nye releases af ens program. I praksis betyder det at man kan være nød til at supplere releases med konverteringsprogrammer, der kan konvertere filer til den nye udgave. Dette er specielt en udfordring fordi de to udgaver af klassen normalt vil hedde det samme, være i den samme package osv.!
 
Lad os se et hex-dump af filen A.res:
 
Jeg er ikke bekendt med detaljerne i formatet for serialisering i Java, men med det blotte øje genkender man visse ord, som optræder i kildeteksten: list, Node, java/lang/String, next og fem. Afslutningen af filen ligner en liste (gentagelsen af bla. sq), og er formodentlig en beskrivelse af den linkede liste.
Ikke til backup Mht. formatet, der anvendes i forbindelse med serialisering, skal man være opmærksom på, at der adskillige steder i dokumentationen til JDK advares om at dette løbende vil undergå forandringer efterhånden som der bliver released nye versioner af JDK'en. Persistensen er derfor ikke nødvendigvis kompatibel mellem forskellige versioner af JDK'en, og man bør derfor ikke bruge serialisering til backup af data.

 

1.1 transient

I specielle situationer kan man ønske, at en del af datakernen ikke bliver serialiseret. Det vil normalt dreje sig om en del af objektets tilstand som ikke vil give mening, enten senere eller i en anden kontekst (hvis den f.eks. sendes over et netværk til en anden maskine). Man kan markere at instans-variable ikke skal indgå i serialiseringen med det reserverede ord: transient (dk.: forbigående, flygtig eller forgængelig).
For eksemplets skyld kunne vi sige, at vi ikke ønskede at den linkede liste skulle serialiseres sammen med A-objekter:
import java.io.*;

class A implements Serializable {
  private int x;
  private String s;
  private transient Node list;
  
  ...
}
Hvilket ændrer udskriften til:
[A: x=5, s="fem", list=[Node: x=4][Node: x=3][Node: x=2][Node: x=1][Node: x=0]]
[A: x=5, s="fem", list=null] 
Vi ser at transient'e instansvariable får den initielle default-værdi for deres type (en 0-ækvivalent værdi). Det betyder samtidig, at man ved de-serialisering skal tage stilling til hvilken værdi transient'e instansvariable skal have.

 

2. Egen serialisering

Ønsker man selv at implementere serialiseringen, kan dette gøres på forskellige niveauer. Enten kan man selv styre hvad man vil have serialiseret (dette kan man til dels også med transient) eller man kan gå ned på laveste niveau og selv bestemme formatet.

 

2.1 Styret serialisering

Ønsker man at styre hvad der bliver serialiseret, skal man implementere følgende to metoder:
private void readObject( ObjectInputStream in )
private void writeObject( ObjectOutputStream out )
Metodernes tilstedeværelse er speciel, da de ikke i normal forstand er en del af Serializable-interfacet - man behøver ikke implementere dem, men gør man, spiller de en særlig rolle. Man bemærker også, at de er private; hvilket er usædvanligt.
De to streams metoderne giver os at arbejde med er Object-Streams, og den form for serialisering vi kan foretage, er den som Java selv udfører. Forskellen er, at vi selv styrer hvad der bliver serialiseret.
Lad os se et eksempel; hvor vi vælger at serialisere ekstra oplysninger sammen de tre instansvariable i klassen A:
import java.io.*;
import java.util.*;

class A implements Serializable {
  private static final int classVersion=1;

  private int x;
  private String s;
  private Node list;
  
  public A( int x, String s, Node list ) {
    this.x = x;
    this.s = s;
    this.list = list;
  }
  
  private void writeObject( ObjectOutputStream out ) throws IOException {
    out.writeInt( classVersion );
    out.writeObject( new Date() );
    
    out.writeInt( x );
    out.writeObject( s );
    out.writeObject( list );
  }

  private void readObject( ObjectInputStream in )
    throws IOException, ClassNotFoundException
  {
    if ( in.readInt() != classVersion ) {
      System.out.println( "Forkert version af class A" );
    } else {
      System.out.println( "Serialiseret: " + in.readObject() );
      
      x = in.readInt();
      s = (String) in.readObject();
      list = (Node) in.readObject();
    }
  }
  
  public String toString() {
    return "[A: x=" + x + ", s=\"" + s + "\", list=" + list + "]";
  }
}
Serialiseret: Fri Sep 14 21:56:58 CEST 2001
[A: x=5, s="fem", list=[Node: x=4][Node: x=3][Node: x=2][Node: x=1][Node: x=0]]
[A: x=5, s="fem", list=[Node: x=4][Node: x=3][Node: x=2][Node: x=1][Node: x=0]] 
Med rødt er markeret vores anvendelse af metoder, der serialiserer vha. de to Object-Streams.
Ud over datakernen serialiserer vi to andre oplysninger:
Versions-problem Den første, er en angivelse af hvilken version af klassen A, der er tale om. På den måde vil de-serialiseringen kunne opfange forsøg på at indlæse objekter, der er instanser af en anden version af klassen A - et problem vi som nævnt skal tage stilling til. Det er ikke nogen særlig god løsning, da vi kan lave ændringer i klassen og glemme at ændre versionnummeret.
Ekstra information Den anden, er en angivelse af hvornår objektet er blevet serialiseret, og er blot endnu et eksempel på hvordan vi ved anvendelse af de to metoder writeObject og readObject kan serialisere ekstra information - en mulighed som transient ikke giver os.
Lokal styring Bemærk at serialiseringen af den linkede liste forløber på samme måde som før. Med writeObject og readObject går vi blot ind og styrer serialiseringen lokalt i en klasse, det får ikke nogen betydning for serialiseringen af de objekter vi vælger at gemme.

 

2.2 Serialisering med eget format

Det er muligt selv at implementere konverteringen til og fra et eget format. Ønsker man dette, skal man i stedet for Serializable implementere Externalizable. Externalizable-interfacet forpligter os til at implementere følgende metoder:
public void readExternal( ObjectInput in )
public void writeExternal( ObjectOutput out )
ObjectInput og -Output er sub-interfaces af DataInput og - Output. De udvider med metoder til håndtering af byte-arrays og Object's. Det betyder at man med metoderne kan implementere sit eget format, uden nogen begrænsninger. Det betyder også, at man kan kombinere det med brug af default-serialisering af objekter, som det ovenfor er gjort med styret serialisering.
At man kan gøre stort set det samme med Externalizable som med styret serialisering under Serializable, giver anledning til undren over, at man også har valgt den uskønne løsning med metoder (writeObject og readObject), der på den ene side er tilknyttet interfacet Serializable, men samtidig ikke er en del af det (dvs. ikke erklæret i det).