PO Serializacja

From Studia Informatyczne

<<< Powrót do przedmiotu Programowanie obiektowe

Spis treści

Wprowadzenie

Omówione na poprzednim wykładzie rodzaje strumieni udostępniają metody do zapisywania/odczytywania jedynie wartości typów podstawowych, tablic ze znakami oraz napisów String. Przy ich pomocy można przygotować kod wypisujący i odczytujący dowolne obiekty. Często jest to pracochłonne zadanie, gdyż atrybutami obiektów mogą być przecież inne obiekty. Java, tak jak większość języków obiektowych, zawiera udogodnienia pozwalające przesyłać przez strumienie obiekty praktycznie bez żadnej dodatkowej pracy. Przekształcanie obiektów na postać binarną lub znakową (np. XML) w sposób, który umożliwia ich późniejsze odtworzenie nazywa się serializacją (ang. serialization), a proces odwrotny określa się jako deserializację (ang. deserialization) [1].

Zastosowania

Lekka trwałość

W większości aplikacji zachodzi potrzeba utrwalania danych. Może być na przykład spowodowana koniecznością zachowania tych danych pomiędzy uruchomieniami aplikacji, nie mieszczeniem się wszystkich danych na raz w pamięci operacyjnej lub chęcią zabezpieczenia się przed ich utratą. Do zapewnienia trwałości zazwyczaj stosuje się bazy danych, którym poświęcony jest inny cykl wykładów. W sytuacjach, gdy zapewniana przez bazy danych możliwość łatwego wyszukiwania informacji nie jest niezbędna (np. zapisywanie dokumentów do pliku – za organizację plików w strukturze katalogów i ich wyszukiwanie odpowiada użytkownik), serializacja może okazać się wystarczająca.

Programowanie rozproszone

W obiektowych aplikacjach rozproszonych najwygodniej przez sieć przesyłać całe obiekty. Serializacja może być używana do przesyłania obiektów przez strumienie oparte na gniazdach sieciowych oraz do przekazywania parametrów i wartości zwrotnych przy zdalnym wywoływaniu metod. Serializacja leży u podstaw dostępnego w Javie mechanizmu zdalnego wywoływania metod Remote Method Invocation (RMI), który jest wykorzystywany w wielu technologiach np. Enterprise Java Beans.

Klasy ObjectOutputStream, ObjectInputStream oraz interfejs Serializable

Do serializacji obiektów na postać binarną służą klasy ObjectOutputStream i ObjectInputStream, które posiadają odpowiednio metody writeObject(Object) oraz Object readObject(). Aby obiekt był poddany automatycznej serializacji musi implementować interfejs java.io.Serializable. Nie posiada on żadnych metod i pełni jedynie rolę znacznika. Większość klas ze standardowych bibliotek Javy, m.in., wszystkie klasy opakowujące, kolekcje oraz napisy String implementuje ten interfejs. Poniższy przykład pokazuje, że serializacji poddawany jest cały graf obiektów – obiekt przekazywany jako parametr metody writeObject() oraz wszystkie obiekty dostępne bezpośrednio lub pośrednio przez zawarte w nim referencje. Taki graf nazywany jest często domknięciem (ang. closure). Pozostałe obiekty należące do domknięcia również powinny implementować interfejs java.io.Serializable.

import java.io.*;

class Osoba implements Serializable {
  String nazwisko;
  String imię;
  Adres adresZameldowania;
    
  Osoba(String nazwisko, String imię, Adres adresZameldowania) {
    this.nazwisko = nazwisko;
    this.imię = imię;
    this.adresZameldowania = adresZameldowania;
    System.out.println("wywołanie konstruktora klasy Osoba");
  }
    
  public String toString() {
    String adrPamięć = super.toString();
    return adrPamięć+"(" + nazwisko + ", " +
                           imię + ", " +
                           adresZameldowania + ")";
  }
}

class Adres implements Serializable {
  String miasto;
  String ulica;
  String nrDomu;
  String nrLokalu;   
  
  Adres(String miasto, String ulica, String nrDomu, String nrLokalu) {
    this.miasto = miasto;
    this.ulica = ulica;
    this.nrDomu = nrDomu;
    this.nrLokalu = nrLokalu;
    System.out.println("wywołanie konstruktora klasy Adres");
  }
  
  public String toString() {
    String adrPamięć = super.toString();
    return adrPamięć + "(" + miasto + ", " +
                             ulica + ", " +
                             nrDomu + ", " +
                             nrLokalu + ")";
  }
}

public class TestSerializacji {
  public static void main(String[] args) throws Exception {
    Adres alternatywy4 = new Adres("Warszawa",  "Alternatywy", "4", "9");
    Osoba kotek = new Osoba("Kotek", "Zygmunt", alternatywy4);
    System.out.println(kotek);
    // wersja dla Linuxa
    String nazwaPliku = "/tmp/lista.ser";
    // wersja dla Windows
    //String nazwaPliku = "c:\\lista.ser";
    ObjectOutputStream out = new ObjectOutputStream(
                               new BufferedOutputStream(
                                 new FileOutputStream(nazwaPliku)));
    out.writeObject("Lista lokatorów");
    out.writeObject(kotek);
    out.close();
    
    ObjectInputStream in = new ObjectInputStream(
                             new BufferedInputStream(
                               new FileInputStream(nazwaPliku)));
    String nagłówek = (String) in.readObject();
    kotek = (Osoba) in.readObject();
    in.close();
    System.out.println(kotek);
  } 
}

Podczas deserializacji nie dochodzi do wywołania żadnych konstruktorów mimo, że tworzone są nowe egzemplarze obiektów. Potwierdza to wyjście powyższego programu:

wywołanie konstruktora klasy Adres
wywołanie konstruktora klasy Osoba
Osoba@1372a1a(Kotek, Zygmunt, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9))
Osoba@1c39a2d(Kotek, Zygmunt, Adres@bf2d5e(Warszawa, Alternatywy, 4, 9))

Kilka referencji wskazujących ten sam obiekt

Algorytm użyty do serializacji grafu obiektów jest przygotowany na występowanie w tym grafie cykli. Jeżeli przed serializacją jakiś obiekt był przypisany na kilka różnych referencji, po deserializacji nadal będzie na te referencje przypisany jeden obiekt. Co więcej własność ta jest zachowana nie tylko przy serializacji jednego grafu obiektów, ale w sumie dla wszystkich grafów serializowanych do jednego strumienia. To znaczy, że jeżeli w kilku serializowanych do tego samego strumienia grafach obiektów występował wspólny obiekt, to po deserializacji grafy nadal będą posiadały część wspólną. Poniższy przykład pokazuje serializację do jednego strumienia dwóch osób posiadających ten sam adres.

Adres alternatywy4 = new Adres("Warszawa",  "Alternatywy", "4", "9");
Osoba kotek = new Osoba("Kotek", "Zygmunt", alternatywy4);
Osoba kołek= new Osoba("Kołek", "Zdzisław", alternatywy4);
System.out.println(kotek);
System.out.println(kołek);
// wersja dla Linuxa
String nazwaPliku = "/tmp/lista.ser";
// wersja dla Windows
//String nazwaPliku = "c:\\lista.ser";
ObjectOutputStream out = new ObjectOutputStream(
                           new BufferedOutputStream(
                             new FileOutputStream(nazwaPliku)));
out.writeObject("Lista lokatorów");
out.writeObject(kotek);
out.writeObject(kołek);    
out.close();

ObjectInputStream in = new ObjectInputStream(
                         new BufferedInputStream(
                           new FileInputStream(nazwaPliku)));
String nagłówek = (String) in.readObject();
kotek = (Osoba) in.readObject();
kołek = (Osoba) in.readObject();
in.close();
System.out.println(kotek);
System.out.println(kołek);

Po deserializacji adresy obu osób nie tylko będą mieć te same atrybuty, ale nadal będą tym samym obiektem.

wywołanie konstruktora klasy Adres
wywołanie konstruktora klasy Osoba
wywołanie konstruktora klasy Osoba
Osoba@1372a1a(Kotek, Zygmunt, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9))
Osoba@126b249(Kołek, Zdzisław, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9))
Osoba@df8ff1(Kotek, Zygmunt, Adres@1632c2d(Warszawa, Alternatywy, 4, 9))
Osoba@1e97676(Kołek, Zdzisław, Adres@1632c2d(Warszawa, Alternatywy, 4, 9))

Sterowanie serializacją

Domyślnie serializowane są wszystkie niestatyczne składowe każdego obiektu, nawet jeżeli są oznaczone jako prywatne[2]. W niektórych sytuacjach nie jest to wskazane. Po pierwsze, obiekty mogą zawierać dane poufne, np. hasła, których nie chcemy wysyłać do strumienia. Po drugie, serializaowanie niektórych obiektów nie ma sensu, np. strumieni lub wątków. Po trzecie, nie wszystkie obiekty implementują interfejs java.io.Serializable i nie ma możliwości tego zmienić, a przy próbie serializacji grafu obiektów, który zawiera obiekt nieimplementujący tego interfejsu zgłaszany jest wyjątek java.io.NotSerializableException. Są dwa sposoby żeby sobie z tym poradzić: wskazanie atrybutów, które powinny być pomijane przez domyślny mechanizm serializacji oraz rezygnacja z domyślnego mechanizmu serializacji i podanie kodu, które ma go zastąpić.

Słowo kluczowe transient

Atrybuty, które mają być wyłączone z domyślnego mechanizmu serializacji należy oznaczyć przy pomocy modyfikatora transient. Deserializacja pozostawia na takich atrybutach wartość domyślną dla danego typu, np. dla typów obiektowych jest to null. W poniższym przykładzie modyfikatorem transient oznaczono atrybut adres klasy Osoba.

class Osoba implements Serializable {
  String nazwisko;
  String imię;
  transient Adres adresZameldowania;
    
  Osoba(String nazwisko, String imię, Adres adresZameldowania) {
    this.nazwisko = nazwisko;
    this.imię = imię;
    this.adresZameldowania = adresZameldowania;
    System.out.println("wywołanie konstruktora klasy Osoba");   
  }
    
  public String toString() {
    String adrPamięć = super.toString();
    return adrPamięć+"(" + nazwisko + ", " +
                           imię + ", " +
                           adresZameldowania + ")";
  }
}

Po deserializacji w każdym obiekcie atrybut adres będzie miał domyślną dla typu obiektowego wartość null.

wywołanie konstruktora klasy Adres
wywołanie konstruktora klasy Osoba
wywołanie konstruktora klasy Osoba
Osoba1@1372a1a(Kotek, Zygmunt, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9))
Osoba1@126b249(Kołek, Zdzisław, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9))
Osoba1@750159(Kotek, Zygmunt, null)
Osoba1@1abab88(Kołek, Zdzisław, null)

Metody writeObject() i readObject()

Można zupełnie zrezygnować z domyślnego mechanizmu serializacji i podać kod go zastępujący. Jeżeli klasa definiuje metody:

  • private void writeObject(ObjectOutputStream oos) throws IOException;
  • private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException;

będą one używane do serializacji i deserializacji jej obiektów. To nie jest pomyłka, że metody są oznaczone jako prywatne[2]! Jeżeli nie zależy nam na zupełnym zastąpieniu, ale na rozszerzeniu domyślnego mechanizmu serializacji, w metodach writeObject() i readObject() można wywołać metody, odpowiednio, defaultWriteObject() oraz defaultReadObject() przekazanego jako parametr strumienia. W poniższym przykładzie domyślny mechanizm został rozszerzony o serializację atrybutu adres, który normalnie nie był serializowany ze względu na oznaczenie go modyfikatorem transient.

class Osoba implements Serializable {
  String nazwisko;
  String imię;
  transient Adres adresZameldowania;
  
  private void writeObject(ObjectOutputStream oos) throws IOException {
    System.out.println("writeObject()");
    oos.defaultWriteObject();
    oos.writeObject(adresZameldowania);
  }

  private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    System.out.println("readObject()");
    ois.defaultReadObject();
    adresZameldowania = (Adres) ois.readObject();
  }
  
  Osoba(String nazwisko, String imię, Adres adresZameldowania) {
    this.nazwisko = nazwisko;
    this.imię = imię;
    this.adresZameldowania = adresZameldowania;
    System.out.println("wywołanie konstruktora klasy Osoba");   
  }
   
  public String toString() {
    String adrPamięć = super.toString();
    return adrPamięć+"(" + nazwisko + ", " +
                           imię + ", " +
                           adresZameldowania + ")";
  }
}

Interfejs Externalizable

Istnieje jeszcze jeden sposób zastąpienia domyślnego mechanizmu serializacji. Klasa może implementować interfejs java.io.Externalizable i definiować wymagane przez niego metody

  • void writeExternal(ObjectOutput out) throws IOException;
  • void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

Ponieważ java.io.Externalizable jest podinterfejsem java.io.Serializable, implementujące go obiekty można przekazywać jako parametr metody writeObject() oraz mogą występować w serializowanym grafie obiektów. Za serializację odpowiada metoda writeExternal(), a za deserializację readExternal(). W wypadku klas implementujących Externalizable nie ma możliwości skorzystania z domyślnego mechanizmu serializacji. Ponadto w odróżnieniu od klas implementujących jedynie java.io.Serializable w trakcie deserializacji dochodzi do wywołania bezparametrowego konstruktora. Jeżeli klasa takiego nie posiada (musi mieć dostęp publiczny), zgłaszany jest wyjątek java.io.InvalidClassException.

Przy podejmowaniu decyzji, czy domyślny mechanizm serializacji zastąpić dodając metody writeObject() i readObject(), czy implementując interfejs java.io.Externalizable, warto wziąć pod uwagę kwestie związane z przesłanianiem metod i korzystaniem z metod nadklasy przez podklasę. writeObject() i readObject() są prywatne, a writeExternal() i readExternal() muszą być publiczne ponieważ są wymuszone przez interfejs. W obu przypadkach wiążą się z tym pewne wady i zalety.

Dostępność definicji klasy podczas deserializacji

Aby deserializacja obiektu się powiodła, maszyna wirtualna, na której jest podejmowana taka próba, musi mieć dostęp do definicji klasy. Można to sprawdzić wykonując poniższy przykład:

import java.io.*;

class KlasaONiedostępnejDefinicji implements Serializable {}

public class TestWersjonowania {
  public static void main(String[] args) throws Exception {
    // wersja dla Linuxa
    String nazwaPliku = "/tmp/test.ser";
    // wersja dla Windows
    //String nazwaPliku = "c:\\test.ser";
    ObjectOutputStream oos = new ObjectOutputStream(
                               new BufferedOutputStream(
                                 new FileOutputStream(nazwaPliku)));
    oos.writeObject(new KlasaONiedostępnejDefinicji());
    oos.close();
  }   
}

Spowoduje to serializację obiektu klasy KlasaONiedostępnejDefinicji do pliku test.ser. Jeżeli podczas próby deserializacji klasa KlasaONiedostępnejDefinicji nie będzie (przez zmienną CLASSHPATH) dostępna dla maszyny wirtualnej, zostanie zgłoszony wyjątek java.lang.ClassNotFoundException. Można się o tym przekonać uruchamiając poniższą klasę:

import java.io.*;

public class PróbaOdczytu {
  public static void main(String[] args) throws Exception {
    ObjectInputStream ois = null;
    try {
      // wersja dla Linuxa
      String nazwaPliku = "/tmp/test.ser";
      // wersja dla Windows
      //String nazwaPliku = "c:\\test.ser";
      ois = new ObjectInputStream(
              new BufferedInputStream(
                new FileInputStream(nazwaPliku)));
      Object obj = ois.readObject();
    } catch (Exception e) {
      System.out.println(e);
    } finally {
      try {
        ois.close();
      } catch (Exception e) {
        System.out.println("Nie udało się zamknąć strumienia");
      }
    } 
  }  
}

Podglądając plik test.ser można się przekonać, że nazwa serializowanej klasy jest wypisywana do strumienia. Dzieje się tak nawet, jeżeli zastąpiliśmy domyślny mechanizm serializacji dodając prywatną metodę writeObject() lub implementując interfejs Externalizable. Dzięki temu maszyna wirtualna wie obiekt jakiej klasy utworzyć podczas deserializacji.

Wersjonowanie

Dociekliwe osoby już pewnie od jakiegoś czasu zastanawiają się, co będzie, jeżeli po serializacji obiektu definicja jego klasy ulegnie zmianie i podejmowana będzie próba deserializacji. Taka sytuacja jest dość prawdopodobna. Obiekt mógł np. być zserializowany do pliku dyskowego i przechowywany w ten sposób przez dłuższy czas. Mógł też zostać wysłany przez sieć do zdalnej maszyny wirtualnej, na której jest uruchomiona inna wersja aplikacji. Jeżeli obiekt implementuje interfejs Externalizable, programista sam odpowiada za poprawną obsługę wersjonowania. Najlepiej w metodzie writeExternal() wypisywać razem z danymi informacje o wersji klasy i na ich podstawie w readExternal() podjąć odpowiednie działania, np. ustawić domyślne wartości dla atrybutów, których nie zawiera zserializowany obiekt. Jeżeli natomiast klasa implementuje interfejs Serializable (ale nie Externalizable), to obsługa wersjonowania odbywa się automatycznie. Porównywanie klas wykonywane jest przy pomocy wartości typu long wyliczanej na podstawie:

  • nazwy klasy i użytych przy jej definicji modyfikatorów,
  • nazw wszystkich interfejsów implementowanych przez klasę,
  • opisu wszystkich metod i konstruktorów oprócz metod i konstruktorów prywatnych,
  • opisu wszystkich atrybutów poza atrybutami prywatnymi, statycznymi i oznaczonymi jako transient.

Podczas serializacji taki unikatowy identyfikator klasy jest generowany i wypisywany do strumienia, a podczas deserializacji jest porównywany z identyfikatorem klasy dostępnej dla maszyny wirtualnej. Jeżeli identyfikatory nie są równe, deserializacja zostaje przerwana i zgłaszany jest wyjątek java.io.InvalidClassException. Takie domyślne zachowanie cechuje duża ostrożność. Można je zmienić samemu, podając oznaczenie wersji klasy. Służy do tego atrybut static final long serialVersionUID potocznie określany jako suid. Poniższa klasa ma ustawiony numer wersji na 2L

class KlasaOZmieniającejSięDefinicji implements Serializable {
  static final long serialVersionUID = 2L;
  String staryAtrybut = "jakaś wartość";
}

Mimo usunięcia oraz dodania atrybutu pozostaje nadal zgodna z klasą podaną poniżej

class KlasaOZmieniającejSięDefinicji implements Serializable {
  static final long serialVersionUID = 2L;
  String nowyAtrybut = "deserializacja nie przypisze tej wartości";
}

Zapamiętana podczas serializacji wartość usuniętego atrybutu zostanie zignorowana, a nowy atrybut będzie miał wartość domyślną dla swojego typu. Oznacza to, że podczas deserializacji nie zostanie nawet wyliczone wyrażenie inicjalizujące.

Odczytywanie suid istniejącej klasy

Jeżeli mamy do czynienia z istniejącą już klasą, w której nie podano atrybutu serialVersionUID, a chcemy zachować z nią zgodność, domyślnie wygenerowany dla niej suid można odczytać przy pomocy narzędzia serialver. Wchodzi ono w skład JDK i powinno być dostępne tak samo jak java.

Serializacja do postaci XML

Serializacja obiektów do postaci binarnej przy pomocy ObjectOutputStream i ObjectInputStream doskonale nadaje się do wymiany informacji między aplikacjami napisanymi w Javie, nawet jeżeli działają na różnych platformach sprzętowych. Standardem wymiany informacji między aplikacjami napisanymi w różnych językach programowania jest język XML. Nie będziemy tu go szczegółowo omawiać. Warto jednak zaznaczyć, że:

  • dokumenty XML zazwyczaj są zrozumiałe dla człowieka nawet jeżeli nie towarzyszy im stosowna dokumentacja,
  • istnieją ogólnie przyjęte specyfikacje określających jak parsować i transformować dokumenty XML oraz wiele łatwo dostępnych narzędzi i bibliotek implementujących te specyfikacje,
  • XML jest powszechnie stosowany do wymiany danych, zarówno w aplikacjach biurkowych, np. formaty zapisu dokumentów pakietów OpenOffice, jak i rozwiązaniach serwerowych, np. usługi Web Services.

XMLDecoder i XMLEncoder

Serializacja obiektów do postaci XML jest praktycznie tak samo łatwa jak do postaci binarnej. Rolę ObjectOutputStream i ObjectInputStream pełnią klasy XMLEncoder i XMLDecoder z pakietu java.beans. W tym wypadku serializowane obiekty nie muszą implementować interfejsu Serializable, ale powinny być zgodne ze specyfikacją JavaBeans. W praktyce oznacza to, że powinny posiadać bezparametrowy publiczny konstruktor oraz dla każdego atrybutu odpowiednie metody set i get.

Poniżej jako przykład znajdują się definicje klas Osoba i Adres spełniające te wymagania oraz klasa SerializacjaDoXML, w której przeprowadzana jest serializacja i deserializacja obiektów kotek oraz kołek do postaci XML.

public class Osoba {
  String nazwisko;
  String imię;
  Adres adres;
    
  Osoba(String nazwisko, String imię, Adres adresZameldowania) {
    this.nazwisko = nazwisko;
    this.imię = imię;
    this.adres = adresZameldowania;
    System.out.println("wywołanie konstruktora klasy Osoba");
  }
    
  public String toString() {
    String adrPamięć = super.toString();
    return adrPamięć+"(" + nazwisko + ", " +
                           imię + ", " +
                           adres + ")";
  }

  public Adres getAdres() {
    return adres;
  }

  public String getImię() {
    return imię;
  }

  public String getNazwisko() {
    return nazwisko;
  }

  public void setAdres(Adres adres) {
    this.adres = adres;
  }

  public void setImię(String imię) {
    this.imię = imię;
  }

  public void setNazwisko(String nazwisko) {
    this.nazwisko = nazwisko;
  }
 
  public Osoba() {
    System.out.println("wywołanie bezparametrowego konstruktora klasy Osoba");
  }
}
public class Adres {
  String miasto;
  String ulica;
  String nrDomu;
  String nrLokalu;
  
  Adres(String miasto, String ulica, String nrDomu, String nrLokalu) {
    this.miasto = miasto;
    this.ulica = ulica;
    this.nrDomu = nrDomu;
    this.nrLokalu = nrLokalu;
    System.out.println("wywołanie konstruktora klasy Adres");   
  }
 
  public String toString() {
    String adrPamięć = super.toString();
    return adrPamięć + "(" + miasto + ", " +
                             ulica + ", " +
                             nrDomu + ", " +
                             nrLokalu + ")";
  }

  public String getMiasto() {
    return miasto;
  }

  public String getNrDomu() {
    return nrDomu;
  }

  public String getNrLokalu() {
    return nrLokalu;
  }

  public String getUlica() {
    return ulica;
  }

  public void setMiasto(String miasto) {
    this.miasto = miasto;
  }

  public void setNrDomu(String nrDomu) {
    this.nrDomu = nrDomu;
  }

  public void setNrLokalu(String nrLokalu) {
    this.nrLokalu = nrLokalu;
  }

  public void setUlica(String ulica) {
    this.ulica = ulica;
  }
  
  public Adres() {
    System.out.println("wywołanie bezparametrowego konstruktora klasy Adres");
  }
}
import java.beans.*;
import java.io.*;

public class SerializacjaDoXML {
  public static void main(String[] args) throws IOException {
    Adres alternatywy4 = new Adres("Warszawa",  "Alternatywy", "4", "9");
    Osoba kotek = new Osoba("Kotek", "Zygmunt", alternatywy4);
    Osoba kołek = new Osoba("Kołek", "Zdzisław", alternatywy4);
    // wersja dla Linuxa
    String nazwaPliku = "/tmp/lista.xml";
    // wersja dla Windows
    //String nazwaPliku = "c:\\lista.xml";    
    XMLEncoder e = new XMLEncoder(
                     new BufferedOutputStream(
                       new FileOutputStream(nazwaPliku)));
    System.out.println(kotek);
    System.out.println(kołek);
    e.writeObject(kotek);
    e.writeObject(kołek);
    e.close();
    XMLDecoder d = new XMLDecoder(
                     new BufferedInputStream(
                       new FileInputStream(nazwaPliku)));
    kotek = (Osoba) d.readObject();
    kołek = (Osoba) d.readObject();
    System.out.println(kotek);
    System.out.println(kołek);
  }
}

Otrzymany w wyniku XML ma następującą postać:

<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_06" class="java.beans.XMLDecoder"> 
  <object class="Osoba"> 
    <void property="adres"> 
      <object id="Adres0" class="Adres"> 
        <void property="miasto"> 
          <string>Warszawa</string> 
        </void> 
        <void property="nrDomu"> 
          <string>4</string> 
        </void> 
        <void property="nrLokalu"> 
          <string>9</string> 
        </void> 
        <void property="ulica"> 
          <string>Alternatywy</string> 
        </void> 
      </object> 
    </void> 
    <void property="imię"> 
      <string>Zygmunt</string> 
    </void> 
    <void property="nazwisko"> 
      <string>Kotek</string> 
    </void> 
  </object> 
  <object class="Osoba"> 
    <void property="adres"> 
      <object idref="Adres0"/> 
    </void> 
    <void property="imię"> 
      <string>Zdzisław</string> 
    </void> 
    <void property="nazwisko"> 
      <string>Kołek</string> 
    </void> 
  </object> 
</java>

XStream

Jak już wspomnieliśmy istnieje bardzo dużo narzędzi do obsługi XML. Dość popularną alternatywą dla klas XMLDecoder i XMLEncoder jest pakiet XStream. Jest on zbliżony w działaniu do ObjectOutputStream. Jego cechy charakterystyczne to:

  • wysoka wydajność,
  • przejrzysty wynikowy XML, który nie zawiera żadnych zbędnych informacji dających się uzyskać przy pomocy mechanizmu refleksji,
  • nie trzeba wprowadzać żadnych zmian do serializowanych klas (nie trzeba dodawać metod get/set, ani domyślnego konstruktora),
  • serializacji podlega cały graf obiektów, a wielokrotne referencje do tego samego obiektu zostaną zachowane[3].

W poniższym przykładzie XStream jest użyty do serializacji i deserializacji obiektów kotek oraz kołek. Do jego uruchomienia konieczne jest pobranie ze strony projektu archiwum XStream.jar i udostępnienie go dla maszyny wirtualnej poprzez zmienną CLASSPATH.

import java.io.*;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class XStreamTest {
  public static void main(String[] args) throws Exception {
    Adres alternatywy4 = new Adres("Warszawa",  "Alternatywy", "4", "9");
    Osoba kotek = new Osoba("Kotek", "Zygmunt", alternatywy4);
    Osoba kołek = new Osoba("Kołek", "Zdzisław", alternatywy4);
    System.out.println(kotek);
    System.out.println(kołek);
    // wersja dla Linuxa
    //String nazwaPliku = "/tmp/lista.xml";
    // wersja dla Windows
    String nazwaPliku = "c:\\lista.xml";
    PrintWriter pw = new PrintWriter(nazwaPliku, "UTF-8");//automatycznie dodaje buforowanie
    XStream xstream = new XStream(new DomDriver("Unicode"));
    //drugi argument konstruktora wskazuje nazwę elementu głównego
    ObjectOutputStream out = xstream.createObjectOutputStream(pw, "ListaLokatorów");
    out.writeObject(kotek);
    out.writeObject(kołek);
    out.close();
   
    Reader rdr = new BufferedReader(new InputStreamReader(new FileInputStream(nazwaPliku), "UTF-8"));
    ObjectInputStream in = xstream.createObjectInputStream(rdr);
    
    kotek = (Osoba) in.readObject();
    kołek = (Osoba) in.readObject();
    in.close();
    System.out.println(kotek);
    System.out.println(kołek);
  }
}

Otrzymany XML będzie miał następującą postać. Niestety, jak można zauważyć, nie zawiera zalecanego przez specyfikację prologu, a wspólny adres został zserializowany dwa razy[3].

<ListaLokatorów>
  <Osoba>
    <nazwisko>Kotek</nazwisko>
    <imię>Zygmunt</imię>
    <adres>
      <miasto>Warszawa</miasto>
      <ulica>Alternatywy</ulica>
      <nrDomu>4</nrDomu>
      <nrLokalu>9</nrLokalu>
    </adres>
  <Osoba>
  <Osoba>
    <nazwisko>Kołek</nazwisko>
    <imię>Zdzisław</imię>
    <adres>
      <miasto>Warszawa</miasto>
      <ulica>Alternatywy</ulica>
      <nrDomu>4</nrDomu>
      <nrLokalu>9</nrLokalu>
    </adres>
  <Osoba>
</ListaLokatorów>

Przypisy

  1. Równocześnie funkcjonują dwa alternatywne terminy angielskie marshalling/unmarshalling oraz deflating/inflating.
  2. 2,0 2,1 Dostęp do składowych prywatnych obiektu jest możliwy dzięki mechanizmowi odzwierciedleń (ang. reflection).
  3. 3,0 3,1 Przy serializacji wielu grafów do jednego strumienia wielokrotne referencje do tego samego obiektu nie są zachowywane.