Kategória: Java standard könyvtárak.
A szerializáció napjainkban annyira alapvető fontosságú, hogy ami ebben a fejezetben le van írva, azon már túl is haladott a világ, ugyanis ennél jobb megoldások kellenek. Arról van szó, hogy az adatokat, konkrétabban egy osztály mezőit olyan módon kell kódolni, hogy abból aztán esetleg máshol vissza legyen állítható az eredeti osztály. Talán nem is kell részletezni ennek a leggyakoribb használatát: az adatok a memóriában keletkeznek, és ha át szeretnénk adni hálózaton keresztül egy másik rendszernek, akkor egy olyan formába kell kódolni, amit át lehet küldeni a másik számítógépnek. Ha egy osztály csak primitív típusokat tartalmaz, akkor a probléma egyszerűnek tűnik: bitről bitre le kell másolni, majd a túloldalon bemásolni a memóriába. A probléma ott kezdődik, ha az osztály referenciákat is tartalmaz; ez esetben ugyanis az adat a memóriának egy egészen más szegletében van.
Itt a Java rendszer által már szinte a kezdetektől nyújtott megoldásról olvashatunk. A valóságban többnyire a JSON szerializációt alkalmazzuk, de azért érdemes ismerni ezt a módszert is.
Egy egyszerű példa
Tegyük fel, hogy a szerializálni kívánt osztályunk a következőképpen néz ki:
class MyClass implements Serializable { private static final long serialVersionUID = 7775212379896052775L; private int myInt; private String myString; public MyClass(int myInt, String myString) { this.myInt = myInt; this.myString = myString; } public int getMyInt() { return myInt; } public String getMyString() { return myString; } @Override public String toString() { return "MyClass [myInt=" + myInt + ", myString=" + myString + "]"; } }
Teljesen szokványos: tartalmaz egy int és egy String mezőt, konstruktort, gettereket, valamint egy toString-et a könnyebb ellenőrzéshez. A példa szempontjából lényegtelen dolgokat (equals(), hashCode, setterek stb.) kihagytam. Két fontos dolgot viszont vegyünk észre:
- Az osztály a Serializable interfészt implementálja. Valójában ez egy ún. marker interfész, azaz nem deklarál semmilyen függvényt; ezzel jelezzük a fordítónak azt, hogy ez az osztály szerializálható.
- A szerializálhatóság öröklődik, tehát ha az ős szerializálható, akkor a leszármazott automatikusan az lesz.
- serialVersionUID: a szerializálható osztályok nem kötelező, de erősen javasolt tartozéka. Elősegítheti a deszerializációt: ezt használja fel a Java rendszer annak eldöntésére, hogy a küldőnek és a fogadónak ugyanaz az osztály definíció verzió áll-e rendelkezésére. Ha eltér a verzió, akkor InvalidClassException kivétel váltódik. Hiányában a Java automatikusan generál egy ilyen számot. A probléma ezzel az, hogy ez már két Java implementáció között potenciálisan eltérhet, ami hamis InvalidClassException-ökhöz vezethet. (Egy személyes megjegyzés ezzel kapcsolatban: ma már a legritkább esetben használjuk a Java szerializációt, viszont ha olyan osztályból öröklődünk, ami szegről-végről szerializálható (ld. előző pontot), akkor a fordító figyelmeztet a serialVersionUID fontosságára. Véleményem szerint ez az esetek közel 100%-ában felesleges programsort eredményez.)
A szerializációt és deszerializációt tartalmazó kód az alábbi:
import java.io.*; public class SerializationExample { public static void main(String args[]) throws IOException, ClassNotFoundException { MyClass myClass = new MyClass(7, "peach"); System.out.println("Before serialization: " + myClass); FileOutputStream fileOutputStream = new FileOutputStream("myClass.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(myClass); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("myClass.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); MyClass myClassDeserialized = (MyClass) objectInputStream.readObject(); objectInputStream.close(); System.out.println("Deserialized object: " + myClassDeserialized); } }
Az egyszerűség érdekében került ugyanoda a két művelet; ez akár kerülhetett volna két teljesen eltérő programba is. Láthatjuk a Java világ kezdeni nehézkességét benne: a '90-es évek stream osztályaival kell dolgoznunk. (Nem összetévesztendő a Java 8-ban bevezetett stream típussal.) Ha lefuttatjuk, az alábbi kapjuk eredményül:
Before serialization: MyClass [myInt=7, myString=peach]
Deserialized object: MyClass [myInt=7, myString=peach]
A kódot megnézve a két objektum fizikailag különböző, és sikerült a művelet. A futtatás könyvtárában találunk egy myClass.ser nevű fájlt. Ez azt is jelenti, hogy csak olyan környezetben tudjuk futtatni, ahol van írható fájlrendszer.
Különleges mezők
3 különleges mezőt fogunk a következő példában megnézni:
- Tranziens (transient): ezeket nem szeretnénk szerializálni.
- Statikus (static): mivel nem a példányhoz tartoznak, hanem magához az osztályhoz, ez nem szerializálódik.
- Hivatkozás másik osztály példányára: az eredeti osztályt csak akkor tudjuk szerializálni, ha a hivatkozott osztály is szerializálható.
Lássunk mindezekre egy példát!
class Ser implements Serializable { private static final long serialVersionUID = 8114564193168470756L; private int value; public Ser(int value) { this.value = value; } public int getValue() { return value; } @Override public String toString() { return "Ser [value=" + value + "]"; } } class MyClass implements Serializable { private static final long serialVersionUID = 7775212379896052775L; private static int myStatic; private int myInt; private String myString; private transient int myTransient; private Ser ser; public MyClass(int myInt, String myString, int myTransient, Ser serializable) { super(); this.myInt = myInt; this.myString = myString; this.myTransient = myTransient; this.ser = serializable; myStatic = 2; } public MyClass() { this(2, "apple", 5, null); myStatic = 1; } public static void setMyStatic(int myStaticValue) { myStatic = myStaticValue; } public static int getMyStatic() { return myStatic; } public int getMyInt() { return myInt; } public String getMyString() { return myString; } public int getMyTransient() { return myTransient; } @Override public String toString() { return "MyClass [myStatic=" + myStatic + "; myInt=" + myInt + ", myString=" + myString + ", myTransient=" + myTransient + ", ser=" + ser + "]"; } }
A kód tehát az első példában már látottakon túl tartalmaz tranziens és statikus adatmezőt, valamint referenciát is másik osztály objektumára. A főprogramot némileg módosítjuk: kicsit eljátszunk a statikus mezővel:
import java.io.*; public class SerializationExample { public static void main(String args[]) throws IOException, ClassNotFoundException { MyClass myClass = new MyClass(7, "peach", 4, new Ser(6)); MyClass.setMyStatic(3); System.out.println("Before serialization: " + myClass); FileOutputStream fileOutputStream = new FileOutputStream("myClass.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(myClass); objectOutputStream.flush(); objectOutputStream.close(); MyClass.setMyStatic(4); FileInputStream fileInputStream = new FileInputStream("myClass.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); MyClass myClassDeserialized = (MyClass) objectInputStream.readObject(); objectInputStream.close(); System.out.println("Deserialized object: " + myClassDeserialized); } }
Ha lefuttatjuk, akkor az alábbi kapjuk:
Before serialization: MyClass [myStatic=3; myInt=7, myString=peach, myTransient=4, ser=Ser [value=6]]
Deserialized object: MyClass [myStatic=4; myInt=7, myString=peach, myTransient=0, ser=Ser [value=6]]
A tranziens mező felvette az alapértelmezett értékét (ami egész szám esetén a 0). A statikus mező nem szerializálódott: a szerializáció és a deszerializciót között beállítottunk egy értéket, ami megmaradt a deszerializáció után is. A hivatkozott osztály viszont szépen szerializálódott.
Egyéni szerializáció
Előfordulhat, hogy nem szeretnénk rábízni - legalábbis nem teljes egészében - a Java rendszerre a szerializációt, mi magunk szeretnénk megvalósítani azt részben vagy egészben. Ennek számos oka lehet: pl. titkosítani szeretnénk az érzékeny adatokat, vagy pl. az, hogy van egy nem szerializálható hivatkozásunk. Ez utóbbi keletkezhet úgy is, hogy a hivatkozott osztály egy külső könyvtár osztálya, tehát nem módosíthatjuk. Ez utóbbira lássunk egy példát (persze úgy, hogy mi magunk valósítjuk meg a nem szerializálható osztályt):
class Nonser { private int value; public Nonser(int value) { this.value = value; } public int getValue() { return value; } @Override public String toString() { return "Nonser [value=" + value + "]"; } } class MyClass implements Serializable { private static final long serialVersionUID = 7775212379896052775L; private int myInt; private transient Nonser nonser; public MyClass(int myInt, Nonser nonser) { this.myInt = myInt; this.nonser = nonser; } public MyClass() { this(0, null); } public int getMyInt() { return myInt; } public Nonser getNonser() { return nonser; } private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeObject(nonser.getValue()); } private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { ois.defaultReadObject(); Integer value = (Integer) ois.readObject(); this.nonser = new Nonser(value); } @Override public String toString() { return "MyClass [myInt=" + myInt + ", nonser=" + nonser + "]"; } }
A példában látható a megoldás:
- A kérdéses hivatkozást tranzienssé kell tennünk; ennek hiányában NotSerializableException váltódik ki.
- Meg kell valósítani a writeObject() és readObject() függvényeket a szerializációhoz ill. deszerializációhoz. A példában szerializáló kód lementi a hivatkozott objektum adatmezőjét, a deszerializációt pedig létrehozza az objektumot.
A főprogram csak a példányosításban tér el a bevezetőben bemutatottól:
import java.io.*; public class SerializationExample { public static void main(String args[]) throws IOException, ClassNotFoundException { MyClass myClass = new MyClass(2, new Nonser(7)); System.out.println("Before serialization: " + myClass); FileOutputStream fileOutputStream = new FileOutputStream("myClass.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(myClass); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("myClass.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); MyClass myClassDeserialized = (MyClass) objectInputStream.readObject(); objectInputStream.close(); System.out.println("Deserialized object: " + myClassDeserialized); } }
Az eredmény az alábbi:
Before serialization: MyClass [myInt=2, nonser=Nonser [value=7]]
Deserialized object: MyClass [myInt=2, nonser=Nonser [value=7]]
Más objektum szerializálása
Nem találtam értelmes példát és nem is tudtam kitalálni arra a lehetőségre, hogy a szerializációba oy módon is beleszólhatunk, hogy nem az adott példányt szerializáljuk, hanem egy másikat. De a tehcnika bemutatásához készítettem egy példát:
class MyAlternative implements Serializable { private static final long serialVersionUID = 7054415598155004335L; private int value; private transient MyAlternative other = null; public MyAlternative(int value, MyAlternative other) { this.value = value; this.other = other; } public MyAlternative() { this(0, null); } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public MyAlternative getOther() { return other; } public void setOther(MyAlternative other) { this.other = other; } private Object writeReplace() { return other; } @Override public String toString() { return "MyAlternative [value=" + value + "]"; } }
A példában tehát az osztály hivatkozik annak egy másik példányára. A writeReplace() függvény adja meg azt, hogy melyik példányt szerializálja; most ezt az alternatívát adjuk meg neki.
A főprogramban a szerializálandó osztályban az érték 2 lesz, az alternatívájában 3:
import java.io.*; public class SerializationExample { public static void main(String args[]) throws IOException, ClassNotFoundException { MyAlternative myAlternative = new MyAlternative(2, new MyAlternative(3, null)); System.out.println("Before serialization: " + myAlternative); FileOutputStream fileOutputStream = new FileOutputStream("myAlternative.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(myAlternative); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("myAlternative.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); MyAlternative myAlternativeDeserialized = (MyAlternative) objectInputStream.readObject(); objectInputStream.close(); System.out.println("Deserialized object: " + myAlternativeDeserialized); } }
Az eredmény az alábbi:
Before serialization: MyAlternative [value=2]
Deserialized object: MyAlternative [value=3]
Visszaállítás létező objektumra
Lehetőségünk van arra is, hogy a deszerializáláskor ne hozzon létre objektumot, hanem mi magunk gondoskodunk arról. Ennek pl. az egyke (singleton) deszerializálásakor lehet szükség, enélkül ugyanis több példánya lehetne. Másik példa: ha az osztálynak csak néhány jól meghatározott értéke lehet, és ahelyett, hogy minden egyes alkalommal újat hoznánk létre, előre példányosítjuk az összes lehetséges különböző értékű objektumot, és azokat használjuk fel. Ez egyébként a pehelysúlyú (flyweight) tervezési minta. Ez esetben a tényleges deszerializáció helyett kiválasztjuk a megfelelő, már előre létrehozott példányt.
Lássunk egy példát az egykére!
class MySingleton implements Serializable { private static final long serialVersionUID = -1163185245347261070L; public static MySingleton instance = new MySingleton(); private MySingleton() {} private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } private Object readResolve() { return instance; } @Override public String toString() { return "MySingleton [value=" + value + "]"; } }
A readResolve() az, amely a megfelelő példányt, azaz jelen esetben az instance referenciát adja vissza. A főprogram:
import java.io.*; public class SerializationExample { public static void main(String args[]) throws IOException, ClassNotFoundException { MySingleton mySingleton = MySingleton.instance; mySingleton.setValue(3); System.out.println("Before serialization: " + mySingleton); FileOutputStream fileOutputStream = new FileOutputStream("mySingleton.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(mySingleton); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("mySingleton.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); MySingleton mySingletonDeserialized = (MySingleton) objectInputStream.readObject(); objectInputStream.close(); System.out.println("Deserialized object: " + mySingletonDeserialized); mySingletonDeserialized.setValue(6); System.out.println("Original object: " + mySingleton); System.out.println("Deserialized object after setting value: " + mySingletonDeserialized); } }
Érdemes először úgy lefuttatni, hogy kitöröljük a readResolve() függvényt:
Before serialization: MySingleton [value=3]
Deserialized object: MySingleton [value=3]
Original object: MySingleton [value=3]
Deserialized object after setting value: MySingleton [value=6]
A szerializáció és deszerializáció látszólag rendben lezajlott, amit az első két sor illusztrál, a problémát viszont az utolsó két sor jelzi: valójában két különböző egyke példányunk van, két különböző értékkel. Pedig a minta teljesen szabályos, privát a konstruktor. Ha a readResolve() visszaállítása után futtatjuk a programot, akkor az eredmén az alábbi:
Before serialization: MySingleton [value=3]
Deserialized object: MySingleton [value=3]
Original object: MySingleton [value=6]
Deserialized object after setting value: MySingleton [value=6]
Tehát itt már mindkét esetben ugyanaz a példány.
További információk
A fenti példák nem tartalmaznak minden részletet a témában, de nem is volt céljuk. További ajánlott olvasmány a témában:
- https://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html: a hivatalos API dokumentum meglepően jól használható tananyagként is.
- https://www.baeldung.com/java-serialization: az alapokat az oldaltól megszokott magas színvonalon olvashatjuk, bár a speciális esetekre nem tér ki.
- https://www.geeksforgeeks.org/serialization-in-java/: ez is egy igen jó alapot nyújt.
- https://dzone.com/articles/java-serialization-magic-methods-and-use-cases: példákat ad a speciális esetekre is.