На перший погляд серіалізація видається тривіальним процесом. Справді, що може бути простішим? Оголосив клас, що реалізує інтерфейс
java.io.Serializable
– і всі справи. Можна серіалізувати клас без проблем. Теоретично це справді так. Практично ж є дуже багато тонкощів. Вони пов'язані з продуктивністю, десеріалізацією, з безпекою класу. І ще з багатьма аспектами. Про такі тонкощі й йтиметься розмова. Цю статтю можна розділити на такі частини:
- Тонкощі механізмів
- Навіщо потрібен
Externalizable
- Продуктивність
- Зворотній бік медалі
- Безпека даних
- Серіалізація об'єктів
Singleton
Тонкощі механізмів
Насамперед, питання на засипку. А скільки існує способів зробити об'єкт, що серіалізується? Практика показує, що понад 90% розробників відповідають це питання приблизно однаково (з точністю до формулювання) – такий спосіб один. Тим часом їх два. Про другий згадують далеко не всі, не кажучи вже про те, щоб сказати щось виразне про його особливості. Отже, які ж ці методи? Про перший пам'ятають усі. Це вже згадана реалізаціяjava.io.Serializable
, яка не потребує жодних зусиль. Другий метод - це також реалізація інтерфейсу, але вже іншого: java.io.Externalizable
. На відміну від java.io.Serializable
, він містить два методи, які необхідно реалізувати - writeExternal(ObjectOutput)
і readExternal(ObjectInput)
. У цих методах якраз і є логіка серіалізації/десеріалізації. Зауваження.Надалі серіалізацію з реалізацією Serializable
я іноді називатиму стандартною, а реалізацію Externalizable
– розширеною. Ще однезауваження. Я навмисно не торкаюся нині такі повноваження управління стандартної серіалізацією, як визначення readObject
і writeObject
, т.к. вважаю ці методи до певної міри некоректними. Ці методи не визначені в інтерфейсі Serializable
і є фактично підпірками для обходу обмежень і надання стандартної серіалізації гнучкості. Натомість Externalizable
методи, що забезпечують гнучкість, закладені спочатку. Задамося ще одним питанням. А як, власне, працює стандартна серіалізація з використаннямjava.io.Serializable
? А працює вона через Reflection API. Тобто. клас розбирається як набір полів, кожне з яких пишеться у вихідний потік. Думаю, зрозуміло, що ця операція неоптимальна за продуктивністю. Наскільки саме – з'ясуємо пізніше. Між цими двома способами серіалізації існує ще одна серйозна відмінність. А саме – у механізмі десеріалізації. При використанні Serializable
десеріалізації відбувається так: під об'єкт виділяється пам'ять, після чого його поля заповнюються значеннями потоку. Конструктор об'єкта у своїй не викликається. Тут треба ще окремо розглянути таку ситуацію. Добре, наш клас серіалізується. А його батько? Абсолютно необов'язково! Більше того, якщо успадкувати клас від Object
- батько вже точно несеріалізується. І хай про поляObject
ми нічого не знаємо, але у наших власних батьківських класах вони можуть бути. Що буде із ними? У потік серіалізації вони не потраплять. Які значення вони набудуть при десеріалізації? Подивимося цей приклад:
package ru.skipy.tests.io;
import java.io.*;
/**
* ParentDeserializationTest
*
* @author Eugene Matyushkin aka Skipy
* @since 05.08.2010
*/
public class ParentDeserializationTest {
public static void main(String[] args){
try {
System.out.println("Creating...");
Child c = new Child(1);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
c.field = 10;
System.out.println("Serializing...");
oos.writeObject(c);
oos.flush();
baos.flush();
oos.close();
baos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
System.out.println("Deserializing...");
Child c1 = (Child)ois.readObject();
System.out.println("c1.i="+c1.getI());
System.out.println("c1.field="+c1.getField());
} catch (IOException ex){
ex.printStackTrace();
} catch (ClassNotFoundException ex){
ex.printStackTrace();
}
}
public static class Parent {
protected int field;
protected Parent(){
field = 5;
System.out.println("Parent::Constructor");
}
public int getField() {
return field;
}
}
public static class Child extends Parent implements Serializable{
protected int i;
public Child(int i){
this.i = i;
System.out.println("Child::Constructor");
}
public int getI() {
return i;
}
}
}
Він прозорий – у нас є несеріалізований батьківський клас та серіалізований дочірній. І ось що виходить:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Тобто при десеріалізації викликається конструктор без параметрів батьківського класу, що не серіалізується . І якщо такого конструктора не буде – при десеріалізації виникне помилка. Конструктор же дочірнього об'єкта, того, який ми десеріалізуємо, не викликається, як було сказано вище. Так поводяться стандартні механізми при використанні Serializable
. При використанні Externalizable
ситуація інша. Спочатку викликається конструктор без параметрів, а потім вже на створеному об'єкті викликається метод readExternal, який і вичитує, власне, всі свої дані. Тому - кожен реалізуючий інтерфейс Externalizable клас повинен мати громадський архітектор без властивостей! Більше того, оскільки всі спадкоємці такого класу теж будуть вважатися такими, що реалізують інтерфейсExternalizable
, у них теж має бути конструктор без параметрів! Ходімо далі. Існує такий модифікатор поля, як transient
. Він означає, що це поле не повинно бути серіалізованим. Однак, як ви самі розумієте, це вказує лише на стандартний механізм серіалізації. При використанні Externalizable
ніхто не заважає серіалізувати це поле, так само як і віднімати його. Якщо поле оголошено transient, то при десеріалізації об'єкта воно набуває значення за замовчуванням. Ще один досить тонкий момент. При стандартній серіалізації поля, що мають модифікатор static
, не серіалізуються. Відповідно, після десеріалізації це поле значення не змінює. Зрозуміло, при реалізаціїExternalizable
серіалізувати та десеріалізувати це поле ніхто не заважає, проте я вкрай не рекомендую цього робити, тому що це не так. це може призвести до важких помилок. Поля з модифікатором final
серіалізуються, як і звичайні. За одним винятком їх неможливо десеріалізувати при використанні Externalizable. Бо final-поля
повинні бути ініціалізовані в конструкторі, а після цього в readExternal змінити значення цього поля буде неможливо. Відповідно – якщо вам необхідно серіалізувати об'єкт, що маєfinal
-поле, вам доведеться використовувати лише стандартну серіалізацію. Ще один момент, який багато хто не знає. При стандартній серіалізації враховується порядок оголошення полів у класі. У всякому разі, так було в ранніх версіях, в JVM версії 1.6 реалізації Oracle вже порядок неважливий, важливий тип та ім'я поля. Склад методів з дуже великою ймовірністю вплине на стандартний механізм, при тому, що поля можуть взагалі залишитися тими ж. Щоб уникнути цього, є наступний механізм. У кожен клас, що реалізує інтерфейс Serializable
, на стадії компіляції додається ще одне поле -private static final long serialVersionUID
. Це поле містить унікальну ідентифікатор версії серіалізованого класу. Воно обчислюється за вмістом класу – полям, їх порядку оголошення, методам, порядку оголошення. Відповідно, за будь-якої зміни в класі це поле змінить своє значення. Це поле записується в потік серіалізації класу. До речі, це, мабуть, єдиний відомий мені випадок, коли static
поле серіалізується. При десеріалізації значення цього поля порівнюється з наявним у класу у віртуальній машині. Якщо значення не збігаються – ініціюється виняток на кшталт цього:
java.io.InvalidClassException: test.ser2.ChildExt;
local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
local class serialVersionUID = 1465687698753363969
Є, однак, спосіб цю перевірку якщо не охвабони, то обдурити. Це може бути корисним, якщо набір полів класу та його порядок вже визначено, а методи класу можуть змінюватися. У цьому випадку серіалізації нічого не загрожує, проте стандартний механізм не дасть десеріалізувати дані з використанням байткоду зміненого класу. Але, як я вже сказав, його можна обдурити. А саме – вручну у класі визначити полеprivate static final long serialVersionUID
. У принципі значення цього поля може бути абсолютно будь-яким. Деякі вважають за краще ставити його рівним даті модифікації коду. Деякі взагалі використовують 1L. Для отримання стандартного значення (того, яке обчислюється внутрішнім механізмом) можна використовувати утиліту serialver, що входить до постачання SDK. Після такого визначення значення поля буде фіксовано, отже десеріалізація завжди буде дозволена. Більше того, у версії 5.0 у документації з'явилося приблизно таке: вкрай рекомендується всім серіалізованим класам декларувати це поле в явному вигляді, бо обчислення за умовчанням дуже чутливе до деталей структури класу, які можуть відрізнятися в залежності від реалізації компілятора, і викликати таким чином несподівані InvalidClassException
при десеріалізації. Оголошувати це поле краще якprivate
, т.к. воно відноситься виключно до класу, в якому оголошується. Хоча у специфікації модифікатора не обумовлено. Розглянемо тепер ось який аспект. Нехай у нас є така структура класів:
public class A{
public int iPublic;
protected int iProtected;
int iPackage;
private int iPrivate;
}
public class B extends A implements Serializable{}
Інакше кажучи, ми маємо клас, успадкований від несеріалізованого батька. Чи можна серіалізувати цей клас і що для цього треба? Що буде зі змінними батьківського класу? Відповідь така. Так, серіалізувати екземпляр класу B
можна. Що для цього потрібно? А потрібно, щоб клас A
був конструктор без параметрів, public
або protected
. Тоді при десеріалізації всі змінні класи A
будуть ініціалізовані за допомогою конструктора. Змінні класи B
будуть ініціалізовані значеннями потоку серіалізованих даних. Теоретично можна в класі B
визначити методи, про які я говорив на початку – readObject
і writeObject
, – на початку яких проводити (де-)серіалізацію змінних класу B
черезin.defaultReadObject/out.defaultWriteObject
, а потім – (де-)серіалізацію доступних змінних з класу A
(у нашому випадку це iPublic
, iProtected
і iPackage
якщо B
знаходиться з тим же пакеті, що і A
). Однак, на мій погляд, для цього краще використати розширену серіалізацію. Наступний момент, який я хотів би торкнутися – серіалізація кількох об'єктів. Нехай у нас є така структура класів:
public class A implements Serializable{
private C c;
private B b;
public void setC(C c) {this.c = c;}
public void setB(B b) {this.b = b;}
public C getC() {return c;}
public B getB() {return b;}
}
public class B implements Serializable{
private C c;
public void setC(C c) {this.c = c;}
public C getC() {return c;}
}
public class C implements Serializable{
private A a;
private B b;
public void setA(A a) {this.a = a;}
public void setB(B b) {this.b = b;}
public B getB() {return b;}
public A getA() {return a;}
}
Що станеться, якщо серіалізувати екземпляр класу A
? Він потягне за собою екземпляр класу B
, який, у свою чергу, потягне екземпляр C
, який має посилання на екземпляр A
, той самий, з якого все починалося. Замкнене коло та нескінченна рекурсія? На щастя ні. Подивимося на наступний тестовий код:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
Що ми робимо? Ми створюємо за екземпляром класів A
, B
і C
, ставимо їм посилання один на одного, після чого серіалізуємо кожен з них. Потім ми десеріалізуємо їх і проводимо серію перевірок. Що вийде в результаті:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Отже, що можна отримати з цього тесту. Перше. Посилання на об'єкти після десеріалізації відрізняються від посилань до неї. Інакше висловлюючись, при серіалізації/десеріалізації об'єкт було скопійовано. Цей метод іноді використовується для клонування об'єктів. Другий висновок, більш істотний. При серіалізації/десеріалізації кількох об'єктів, що мають перехресні посилання, ці посилання залишаються дійсними після десеріалізації. Інакше кажучи, якщо до серіалізації вони вказували на один об'єкт, то після десеріалізації вони також вказуватимуть на один об'єкт. Ще один невеликий тест на підтвердження цього:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Об'єкт класу B
має посилання об'єкт класу C
. При серіалізації b
серіалізується разом з екземпляром класу С
, після чого той самий екземпляр з серіалізується тричі. Що виходить після десеріалізації?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Як бачимо, всі чотири десеріалізовані об'єкти насправді є одним об'єктом – посилання на нього рівні. Як і було до серіалізації. Ще один цікавий момент – що буде, якщо одночасно реалізувати у класу Externalizable
та Serializable
? Як у тому питанні – слон проти кита – хто кого поборе? Поборе Externalizable
. Механізм серіалізації спочатку перевіряє його наявність, а потім – наявність Serializable
Так що якщо клас B, що реалізує Serializable
, успадковується від класу A, що реалізує Externalizable
, поля класу B серіалізовані не будуть. Останній момент – успадкування. При успадкування від класу, що реалізує Serializable
, ніяких додаткових дій робити не треба. Серіалізація буде поширюватись і на дочірній клас. При наслідуванні від класу, що реалізуєExternalizable
Необхідно перевизначити методи батьківського класу readExternal і writeExternal. Інакше поля дочірнього класу не будуть серіалізовані. В цьому випадку треба б не забути викликати батьківські методи, інакше не будуть серіалізовані вже батьківські поля. * * * З деталями, мабуть, закінчабо. Однак є одне питання, яке ми не торкнулися, глобального характеру. А саме -
Навіщо потрібний Externalizable
Навіщо взагалі потрібна розширена серіалізація? Відповідь проста. По-перше, вона дає набагато більшу гнучкість. По-друге, найчастіше вона може дати чималий виграш за обсягом серіалізованих даних. По-третє, існує такий аспект як продуктивність, про який ми поговоримо нижче . З гнучкістю начебто зрозуміло все. Дійсно, ми можемо керувати процесами серіалізації та десеріалізації як хочемо, що робить нас незалежними від будь-яких змін у класі (як я говорив трохи вище, зміни у класі здатні сильно вплинути на десеріалізацію). Тому хочу сказати кілька слів про виграш за обсягом. Допустимо, у нас є наступний клас:public class DateAndTime{
private short year;
private byte month;
private byte day;
private byte hours;
private byte minutes;
private byte seconds;
}
Решта несуттєва. Поля можна було б зробити типу int, але це лише посаболо ефект прикладу. Хоча насправді поля можуть бути типуint
з міркувань продуктивності. У будь-якому випадку суть зрозуміла. Клас являє собою дату та час. Нам він цікавий насамперед із погляду серіалізації. Можливо, найпростіше було б зберігати найпростіший часповітря. Він має тип long, тобто. при серіалізації він би зайняв 8 байт. З іншого боку, цей підхід вимагає методів перетворення компонент одне значення і назад, тобто. - Втрата у продуктивності. Плюс такого підходу – абсолютно божевільна дата, яка може поміститися у 64 біти. Це величезний запас міцності, найчастіше насправді не потрібний. Клас, наведений вище, займе 2 + 5 * 1 = 7 байт. Плюс службові витрати на клас та 6 полів. Чи можна якось утиснути ці дані? Напевно. Секунди та хвабони лежать в інтервалі 0-59, тобто. для їх подання достатньо 6 біт замість 8. Годинник – 0-23 (5 біт), дні – 0-30 (5 біт), місяці – 0-11 (4 біти). Усього, без урахування року – 26 біт. До розміру int залишається 6 біт. Теоретично, у деяких випадках цього може вистачити протягом року. Якщо ні – додавання ще одного байта збільшує обсяг поля даних до 14 біт, що дає проміжок 0-16383. Цього цілком достатньо в реальних додатках. Разом – ми зменшабо обсяг даних, необхідні зберігання необхідної інформації, до 5 байт. Якщо не до 4. Нестача та сама, що й у попередньому випадку – якщо зберігати дату упакованої, то потрібні методи перетворення. А хочеться так зберігати в окремих полях, а серіалізувати в упакованому вигляді. Ось тут доцільно використовувати Якщо ні – додавання ще одного байта збільшує обсяг поля даних до 14 біт, що дає проміжок 0-16383. Цього цілком достатньо в реальних додатках. Разом – ми зменшабо обсяг даних, необхідні зберігання необхідної інформації, до 5 байт. Якщо не до 4. Нестача та сама, що й у попередньому випадку – якщо зберігати дату упакованої, то потрібні методи перетворення. А хочеться так зберігати в окремих полях, а серіалізувати в упакованому вигляді. Ось тут доцільно використовувати Якщо ні – додавання ще одного байта збільшує обсяг поля даних до 14 біт, що дає проміжок 0-16383. Цього цілком достатньо в реальних додатках. Разом – ми зменшабо обсяг даних, необхідні зберігання необхідної інформації, до 5 байт. Якщо не до 4. Нестача та сама, що й у попередньому випадку – якщо зберігати дату упакованої, то потрібні методи перетворення. А хочеться так зберігати в окремих полях, а серіалізувати в упакованому вигляді. Ось тут доцільно використовувати а серіалізувати в упакованому вигляді. Ось тут доцільно використовувати а серіалізувати в упакованому вигляді. Ось тут доцільно використовуватиExternalizable
:
// data is packed into 5 bytes:
// 3 2 1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
int packed = 0;
packed += ((int)hours) << 27;
packed += ((int)minutes) << 21;
packed += ((int)seconds) << 15;
packed += ((int)day) << 10;
packed += ((int)month) << 6;
packed += (((int)year) >> 8) & 0x3F;
out.writeInt(packed);
out.writeByte((byte)year);
}
public void readExternal(ObjectInput in){
int packed = in.readInt();
year = in.readByte() & 0xFF;
year += (packed & 0x3F) << 8;
month = (packed >> 6) & 0x0F;
day = (packed >> 10) & 0x1F;
seconds = (packed >> 15) & 0x3F;
minutes = (packed >> 21) & 0x3F;
hours = (packed >> 27);
}
Власне, це все. Після серіалізації ми отримуємо службові витрати на клас, два поля (замість 6) та 5 байт даних. Що вже значно краще. Подальшу упаковку можна залишити спеціалізованим бібліотекам. Наведений приклад дуже простий. Його основне призначення – показати як можна застосовувати розширену серіалізацію. Хоча можливий виграш в обсязі серіалізованих даних – далеко не основна перевага, як на мене. Основна ж перевага, крім гнучкості... (плавно переходимо до наступного розділу...) Посилання на першоджерело: Серіалізація як вона є
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ