PO Serializacja: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Jsroka (dyskusja | edycje)
Jsroka (dyskusja | edycje)
Linia 11: Linia 11:


== Klasy ''ObjectOutputStream'', ''ObjectInputStream'' oraz interfejs ''Serializable'' ==
== 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 ''readObject()''. Aby obiekt był poddany automatycznej serializacji musi implementować interfejs ''java.io.Serializable''. Interfejs ten nie posiada ż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''.
Do serializacji obiektów na postać binarną służą klasy ''ObjectOutputStream'' i ''ObjectInputStream'', które posiadają odpowiednio metody ''writeObject(Object)'' oraz ''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.*;
  '''import''' java.io.*;
   
   

Wersja z 01:11, 9 wrz 2006

Wprowadzenie

Omówione na poprzednim wykładzie rodzaje strumieni udostępniają jedynie metody do zapisywania/odczytywania wartości typów podstawowych 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) <ref name="PO_Nazwy_Serializacji">Równocześnie funkcjonują dwa alternatywne terminy angielskie marshalling/unmarshalling oraz deflating/inflating.</ref>.

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 Javowego mechanizmu zdalnego wywoływania metod Remote Method Invocetion (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 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 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 + ")";
  }
}

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 dla wszystkich grafów serializowanych do jednego strumienia w sumie. 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ą posiadały 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))

Kontrolowanie serializacji

Domyślnie serializowane są wszystkie składowe każdego obiektu, nawet jeżeli są oznaczone jako prywatne<ref name="PO_ser_dostęp_do_pry_składowych">Dostęp do składowych prywatnych obiektu jest możliwy dzięki mechanizmowi odzwierciedleń (ang. reflection).</ref>. 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. Podczas deserializacji zostaną na nie przypisane wartość domyślne dla danego typu. W poniższym przykładzie modyfikatorem transient oznaczono atrybut adres klasy Osoba.

class Osoba implements Serializable {
  String nazwisko;
  String imię;
  transient 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 + ")";
  }
}

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<ref name="PO_ser_dostęp_do_pry_składowych">Dostęp do składowych prywatnych obiektu jest możliwy dzięki mechanizmowi odzwierciedleń (ang. reflection).</ref>. 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ć na przekazanym jako parametr strumieniu odpowiednio defaultWriteObject() oraz defaultWriteObject(). 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 adres;
  
  private void writeObject(ObjectOutputStream oos) throws IOException, ClassNotFoundException {
    System.out.println("writeObject()");
    oos.defaultWriteObject();
    oos.writeObject(adres);
  }

  private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    System.out.println("readObject()");
    ois.defaultReadObject();
    adres = (Adres) ois.readObject();
  }
  
  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 + ")";
  }
}

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 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. Oba przypadkach wiążą się z tym pewne wady i zalety.

Dostępność klasy przy deserializacji

Wersjonowanie

Serializacja do postaci XML

Przypisy

<references/>