Java Serializable interfejs: pa to je samo prazan interfejs
Za Java serijalizaciju, dugo sam ostajao na najpovršnijem nivou razumevanja — samo neka klasa koju treba serijalizovati implementira Serializbale interfejs i to je to.
Kao da nisam želeo da ulazim u dublju analizu, jer — znam da koristim, pa šta više treba.
Ali kako je vreme odmicalo, sve češće sam sretao Serializbale, pa mi je postao zanimljiv. Došlo je vreme da utrošim malo vremena i proučim ga.
01. Prvo malo teorije
Java serijalizacija jedna je od pionirskih mehanika uvedenih još u JDK 1.1, koja služi da Java objekte pretvori u niz bajtova, radi lakšeg skladištenja ili prenosa. Nakon toga, taj niz bajtova i dalje može da se pretvori nazad u prvobitno stanje Java objekta.
Ideja serijalizacije jeste da se stanje objekta „zamrzne“, pa zatim upiše na disk ili prosledi mrežom; ideja deserijalizacije jeste da se stanje objekta „odmrzne“ i ponovo dobije upotrebljiv Java objekat.
Serijalizacija ima jedno pravilo: objekat koji se serijalizuje mora implementirati Serializbale interfejs, inače će biti bačen NotSerializableException.
Hajde da vidimo definiciju Serializbale interfejsa:
public interface Serializable {
}Ništa više!
Pa to je samo prazan interfejs, a opet uspeva da garantuje da „objekti klase“ koja ga implementira budu serijalizovani i deserijalizovani?
02. Sada malo prakse
Pre nego što odgovorimo na to pitanje, hajde da napravimo jednu klasu (samo dva polja i odgovarajući getter/setter), koju ćemo serijalizovati i deserijalizovati.
class Wanger {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}Napravićemo i test klasu: preko ObjectOutputStream upisujemo „osamnaestogodišnjeg Wang Era“ u fajl — to je zapravo proces serijalizacije; zatim preko ObjectInputStream čitamo „osamnaestogodišnjeg Wang Era“ iz fajla — to je proces deserijalizacije. (O ovome smo već govorili kad smo učili serijalizacioni tok.)
// Inicijalizacija
Wanger wanger = new Wanger();
wanger.setName("Wang Er");
wanger.setAge(18);
System.out.println(wanger);
// Upisuje objekat u fajl
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}
// Čita objekat iz fajla
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){
Wanger wanger1 = (Wanger) ois.readObject();
System.out.println(wanger1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}Međutim, pošto Wanger ne implementira Serializbale interfejs, pri pokretanju test klase biće bačen izuzetak; trag steka izgleda ovako:
java.io.NotSerializableException: com.cmower.java_demo.xuliehua.Wanger
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.cmower.java_demo.xuliehua.Test.main(Test.java:21)Prateći trag steka, hajde da pogledamo metod writeObject0() iz ObjectOutputStream. Deo izvornog koda izgleda ovako:
// Proverava da li je objekat tip String; ako jeste, poziva writeString za serijalizaciju
if (obj instanceof String) {
writeString((String) obj, unshared);
}
// Proverava da li je objekat niz; ako jeste, poziva writeArray za serijalizaciju
else if (cl.isArray()) {
writeArray(obj, desc, unshared);
}
// Proverava da li je objekat enum; ako jeste, poziva writeEnum za serijalizaciju
else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
// Proverava da li je objekat Serializable; ako jeste, poziva writeOrdinaryObject za serijalizaciju
else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
}
// Ako objekat ne može da se serijalizuje, baca NotSerializableException
else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}Drugim rečima, prilikom serijalizacije ObjectOutputStream proverava kom tipu pripada objekat koji se serijalizuje — String? niz? enum? ili Serializable? Ako nije ništa od navedenog, baca NotSerializableException.
Ako Wanger implementira Serializable interfejs, može se serijalizovati i deserijalizovati.
class Wanger implements Serializable{
private static final long serialVersionUID = -2095916884810199532L;
private String name;
private int age;
}Kako tačno izgleda serijalizacija?
Uzećemo ObjectOutputStream za primer: pri serijalizaciji on redom poziva writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields().
private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
// Uzima klasu objekta i proverava da li je dozvoljena podrazumevana serijalizacija
Class<?> cl = desc.forClass();
desc.checkDefaultSerialize();
// Uzima broj polja osnovnog (primitivnog) tipa i njihove vrednosti
int primDataSize = desc.getPrimDataSize();
desc.getPrimFieldValues(obj, primVals);
// Upisuje vrednosti polja osnovnog tipa u izlazni tok
bout.write(primVals, 0, primDataSize, false);
// Uzima vrednosti polja ne-osnovnog (referentnog) tipa
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals);
// U petlji upisuje vrednosti ne-osnovnih polja objekta
for (int i = 0; i < objVals.length; i++) {
// Poziva writeObject0 da serijalizuje ne-osnovno polje objekta u izlazni tok
try {
writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
}
// Ako tokom upisa dođe do izuzetka, umotava ga u IOException
catch (IOException ex) {
if (abortIOException == null) {
abortIOException = ex;
}
}
}
}A kako ide deserijalizacija?
Uzećemo ObjectInputStream za primer: pri deserijalizaciji on redom poziva readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields().
private void defaultReadFields(Object obj, ObjectStreamClass desc) throws IOException {
// Uzima klasu objekta i proverava da li objekat pripada toj klasi
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
// Uzima broj i vrednosti polja osnovnog tipa
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
// Iz ulaznog toka čita vrednosti polja osnovnog tipa i smješta ih u niz primVals
bin.readFully(primVals, 0, primDataSize, false);
if (obj != null) {
// Vrednosti iz niza primVals upisuje u odgovarajuća polja objekta
desc.setPrimFieldValues(obj, primVals);
}
// Uzima broj i vrednosti polja ne-osnovnog tipa
int objHandle = passHandle;
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
// U petlji čita vrednosti ne-osnovnih polja objekta
for (int i = 0; i < objVals.length; i++) {
// Poziva readObject0 da pročita vrednost ne-osnovnog polja objekta
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(Object.class, f.isUnshared());
// Ako je polje referentnog tipa, označava zavisnost od tog objekta
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
// Vrednosti iz niza objVals upisuje u odgovarajuća polja objekta
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}Mislim da ti je sad, kad si ovo pročitao, sve jasno i da si zadovoljno uzdahnuo „aha“. Razlog zašto je Serializable interfejs prazan jeste taj što on služi samo kao oznaka — govori programu da objekti koji ga implementiraju mogu da se serijalizuju, ali same operacije serijalizacije i deserijalizacije ne obavlja on.
03. Još neke napomene
Da odmah kažemo jasno: polja označena sa static i transient ne serijalizuju se.
Zašto? Hajde prvo da dokažemo, pa onda da objasnimo.
Prvo, dodajmo dva polja u klasu Wanger.
class Wanger implements Serializable {
private static final long serialVersionUID = -2095916884810199532L;
private String name;
private int age;
public static String pre = "Chenmo";
transient String meizi = "Wang San";
@Override
public String toString() {
return "Wanger{" + "name=" + name + ",age=" + age + ",pre=" + pre + ",meizi=" + meizi + "}";
}
}Zatim, u test klasi ispišimo objekat pre serijalizacije i posle deserijalizacije, i izmenimo vrednost static polja između serijalizacije i deserijalizacije. Konkretno:
// Inicijalizacija
Wanger wanger = new Wanger();
wanger.setName("Wang Er");
wanger.setAge(18);
System.out.println(wanger);
// Upisuje objekat u fajl
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}
// Menja vrednost static polja
Wanger.pre ="Bu Chenmo";
// Čita objekat iz fajla
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){
Wanger wanger1 = (Wanger) ois.readObject();
System.out.println(wanger1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}Izlaz:
Wanger{name=Wang Er,age=18,pre=Chenmo,meizi=Wang San}
Wanger{name=Wang Er,age=18,pre=Bu Chenmo,meizi=null}Upoređivanjem rezultata možemo primetiti:
- Pre serijalizacije
preima vrednost „Chenmo”; posle serijalizacije menjamopreu „Bu Chenmo”; posle deserijalizacijepreje „Bu Chenmo”, a ne „Chenmo” kakvo je bilo pre serijalizacije.
Zašto? Zato što serijalizacija čuva stanje objekta, dok polja označena sa static pripadaju stanju klase — dakle dokazano je da serijalizacija ne čuva static polja.
- Pre serijalizacije
meiziima vrednost „Wang San”; posle deserijalizacijemeizijenull, a ne „Wang San” kakvo je bilo pre.
Zašto? transient u prevodu znači „privremeno“ (evo zašto je važno znati engleski) — ono sprečava da polje bude serijalizovano u fajl, a nakon deserijalizacije vrednost transient polja se postavlja na podrazumevanu: za int to je 0, za objekte null.
Ako želiš da zaviriš u izvorni kod, u klasi ObjectStreamClass možeš naći ovakav kod:
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
// Uzima sva polja deklarisana u klasi
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
// Prolazi kroz sva polja i u listu dodaje ona koja nisu static ni transient
for (int i = 0; i < clFields.length; i++) {
Field field = clFields[i];
int mods = field.getModifiers();
if ((mods & mask) == 0) {
// Pravi ObjectStreamField na osnovu imena, tipa polja i toga da li klasa može da se serijalizuje
ObjectStreamField osf = new ObjectStreamField(field.getName(), field.getType(), !Serializable.class.isAssignableFrom(cl));
list.add(osf);
}
}
int size = list.size();
// Ako je lista prazna vraća prazan niz ObjectStreamField; inače listu pretvara u niz i vraća
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}Vidiš Modifier.STATIC | Modifier.TRANSIENT? Polja označena ovim dvama modifikatorima ne ulaze u listu polja za serijalizaciju — jasno?
04. Još malo suštine
Pored Serializable, Java nudi i interfejs za serijalizaciju Externalizable (malo teže se izgovara).
Koja je razlika između ova dva interfejsa? Probaj pa ćeš videti.
Najpre zamenimo interfejs Serializable koji Wanger implementira sa Externalizable.
class Wanger implements Externalizable {
private String name;
private int age;
public Wanger() {
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Wanger{" + "name=" + name + ",age=" + age + "}";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}Wanger klasa koja implementira Externalizable razlikuje se od onog koja implementira Serializable u nekoliko stvari:
- Dodat je konstruktor bez argumenata.
Pri deserijalizaciji preko Externalizable poziva se konstruktor bez argumenata serijalizovane klase da bi se napravio novi objekat, a zatim se prekopiraju sačuvane vrednosti polja. U suprotnom baca se izuzetak:
java.io.InvalidClassException: com.cmower.java_demo.xuliehua1.Wanger; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:790)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1782)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)- Dodate su dve metode
writeExternal()ireadExternal(), koje su obavezne pri implementacijiExternalizableinterfejsa.
Zatim u test klazi ispišimo objekat pre serijalizacije i posle deserijalizacije.
// Inicijalizacija
Wanger wanger = new Wanger();
wanger.setName("Wang Er");
wanger.setAge(18);
System.out.println(wanger);
// Upisuje objekat u fajl
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}
// Čita objekat iz fajla
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {
Wanger wanger1 = (Wanger) ois.readObject();
System.out.println(wanger1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
// Wanger{name=Wang Er,age=18}
// Wanger{name=null,age=0}Prema izlazu, sva polja objekta posle deserijalizacije dobila su podrazumevane vrednosti — dakle stanje objekta pre serijalizacije nije „zamrznuto“.
Zašto? Zato što za klasu Wanger nismo pregazili konkretne writeExternal() i readExternal() metode. Kako bi trebalo da ih pregazimo?
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}Pozivom
writeObject()izObjectOutputupisuje se stringovnonameu izlazni tok;Pozivom
writeInt()izObjectOutputupisuje se celobrojnoageu izlazni tok;Pozivom
readObject()izObjectInputčita se stringovnoname(iz ulaznog toka);Pozivom
readInt()izObjectInputčita se celobrojnoage(iz ulaznog toka);
Ponovo pokreni test klasu i videćeš da objekat sada normalno serijalizuje i deserijalizuje.
Pre serijalizacije: Wanger{name=Wang Er,age=18} Posle serijalizacije: Wanger
Da sumiramo:
Externalizable i Serializable služe za serijalizaciju i deserijalizaciju Java objekata, ali se razlikuju u sledećem:
①. Serializable je interfejs iz Java standardne biblioteke, dok je Externalizable podinterfejs interfejsa Serializable;

②. Interfejs Serializable ne zahteva implementaciju nijedne metode — dovoljno je označiti klasu koja se serijalizuje; interfejs Externalizable zahteva implementaciju dve metode: writeExternal i readExternal;
③. Interfejs Externalizable nudi veću kontrolu nad serijalizacijom — omogućava prilagođenu obradu objekta tokom serijalizacije i deserijalizacije, npr. šifrovanje i dešifrovanje osetljivih podataka.
05. Još malo deserta
Neka te prvo pitam: znaš li čemu služi linija private static final long serialVersionUID = -2095916884810199532L;?
Hmm...
serialVersionUID se zove serijalizacioni ID i ključni je faktor koji odlučuje da li će Java objekat biti uspešno deserijalizovan. Pri deserijalizaciji, Java virtuelna mašina poredi serialVersionUID iz toka bajtova sa serialVersionUID u klasi koja se serijalizuje; ako su jednaki, deserijalizacija je moguća, inače se baca izuzetak o nekompatibilnosti verzije serijalizacije.
Kada klasa implementira Serializable interfejs, IDE će predložiti da joj se doda serijalizacioni ID, ovako:

- Dodavanje serijalizacionog ID-a podrazumevane verzije:
private static final long serialVersionUID = 1L.- Dodavanje slučajno generisanog, jedinstvenog serijalizacionog ID-a.
private static final long serialVersionUID = -2095916884810199532L;- Dodavanje anotacije
@SuppressWarnings.
@SuppressWarnings("serial")Kako odabrati?
Prvo, uzećemo drugi pristup: u klasu koja se serijalizuje dodaćemo slučajno generisan serijalizacioni ID.
class Wanger implements Serializable {
private static final long serialVersionUID = -2095916884810199532L;
private String name;
private int age;
// ostatak koda izostavljen
}Zatim ćemo serijalizovati jedan Wanger objekat u fajl.
// Inicijalizacija
Wanger wanger = new Wanger();
wanger.setName("Wang Er");
wanger.setAge(18);
System.out.println(wanger);
// Upisuje objekat u fajl
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}Sada potajno zamenimo serijalizacioni ID klase Wanger — hej, hej.
// private static final long serialVersionUID = -2095916884810199532L;
private static final long serialVersionUID = -2095916884810199533L;Hajde, spremi se za deserijalizaciju.
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {
Wanger wanger = (Wanger) ois.readObject();
System.out.println(wanger);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}Ups, greška.
java.io.InvalidClassException: local class incompatible: stream classdesc
serialVersionUID = -2095916884810199532,
local class serialVersionUID = -2095916884810199533
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)Trag steka nam govori da serijalizacioni ID pročitan iz fajla za trajno čuvanje nije isti kao lokalni serijalizacioni ID, pa deserijalizacija nije moguća.
Šta ako odaberemo treći pristup i klasi Wanger dodamo anotaciju @SuppressWarnings("serial")?
@SuppressWarnings("serial")
class Wanger implements Serializable {
// ostatak koda izostavljen
}Hajde, još jednom deserijalizujmo. Nažalost, opet greška.
java.io.InvalidClassException: local class incompatible: stream classdesc
serialVersionUID = -2095916884810199532,
local class serialVersionUID = -3818877437117647968
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)Trag steka nam govori da je lokalni serijalizacioni ID -3818877437117647968, koji i dalje nije isti kao onaj pročitan iz fajla za trajno čuvanje, pa deserijalizacija nije moguća. Šta nam to govori? Pri korišćenju anotacije @SuppressWarnings("serial") ona automatski generiše slučajni serijalizacioni ID za klasu koja se serijalizuje.
Time se dokazuje da Java virtuelna mašina pri dozvoli deserijalizacije ne zavisi samo od classpath-a i istog funkcionalnog koda, već i od jednog jako važnog faktora — da li se serijalizacioni ID poklapa.
Drugim rečima, ako nema posebnih zahteva, dovoljan je podrazumevani serijalizacioni ID (1L) — na taj način se, uz dosledan kod, obezbeđuje uspešna deserijalizacija.
class Wanger implements Serializable {
private static final long serialVersionUID = 1L;
// ostatak koda izostavljen
}06. Još malo zaključka
Pre nego što sam napisao ovaj članak, stvarno nisam očekivao da će „prazni“ Serializable imati ovoliko toga za proučavanje!
Kada sam završio ovaj članak, setio sam se jedne izreke Cao Linjing, najboljeg učenika iz prirodnih nauka: „U učenju nijedan problem nije premali da se zanemari; svaku tačku znanja treba sumirati“ — zaista je u pravu!
Hvala na čitanju! Ako ti se svideo članak, lajkuj i prati
