Serializarea obiectelor
În afară de fluxurile de I/O despre care s-a discutat în detaliu în capitolul de fluxuri, limbajul Java pune la dispoziția programatorilor o noțiune puternică de serializare a obiectelor. Știm deja că o clasă este o structură ce încapsulează date și funcționalitate. Serializarea este procedeul prin care datele încapsulate în instanța unei clase sunt trimise și primite cu ajutorul unui flux de I/O. Clasele care realizează acest lucru sunt java.io.ObjectOutputStream și java.io.ObjectInputStream iar metodele cele mai utilizate sunt writeObject(Object) și readObject().
public class Question implements Serializable{
public String questionBody;
public String[] answers;
public int correctAnswerIndex;
}
Scrierea obiectelor
Un ObjectOutputStream este, ca și clasa FilterOutputStream, un "wrapper", adică o înfășurătoare care are nevoie de un alt OutputStream pentru a funcționa. Rolul clasei ObjectOutputStream este de a transforma un obiect într-un șir de octeți care se scriu mai departe pe stream-ul la nivel de octet:
public class ObjectWriter{
public static void main(String[] _args){
try{
OutputStream _byteStream = getSomeOutputStream();
ObjectOutputStream _objectStream = new ObjectOutputStream(_byteStream);
Question _q1 = new Question();
_objectStream.writeObject(_q1);
_objectStream.close();
}catch(IOException _ioe){
System.out.println("Unable to write object: " + _ioe.getMessage());
}
}
public static OutputStream getSomeOutputStream(){
//...
}
}
Câmpurile statice nu sunt serializate (serializarea se referă la obiect și nu la clasă). Dacă obiectul serializat conține referințe la alte obiecte, acestea sunt și ele serializate automat urmând aceleaşi reguli.
În plus, pentru a proteja un câmp la serializare, se poate folosi modificatorul transient. Câmpurile transient nu se serializează:
public class Question implements Serializable{
public String questionBody;
public String[] answers;
public transient int correctAnswerIndex;
}
Citirea obiectelor
Citirea obiectelor serializate se face utilizând un obiect de tip ObjectInputStream care se folosește de un InputStream generic. După cum spuneam mai sus, un obiect serializat este de fapt un șir de octeți ce reprezintă valorile fiecărui câmp (ne-static și ne-transient) al clasei. Acesta nu conține și informații despre clasă, prin urmare, ca un obiect să poată fi deserializat, aplicația care face deserializarea trebuie să aiba la dispoziție clasa a cărei instanță este obiectul deserializat. Dacă această clasă nu este disponibilă, atunci metoda readObject() va arunca o excepție de tip java.lang.ClassNotFoundException.
Verificarea clasei este făcută la runtime (în timpul execuției programului, la momentul apelării metodei readObject()) și implică compararea unui câmp static final long serialVersionUID declarat în clasă, cu valoarea conținută de obiectul serializat. Prin urmare, nu este suficient ca două clase sa fie identice din punct de vedere al sursei Java, ca să fie compatibile la serializare, trebuie să aibă aceeași valoare a câmpului serialVersionUID. Dacă nu este specificat de către programator, valoarea acestui câmp este generată aleator de compilator la compilarea clasei și va diferi pentru același cod compilat pe două mașini diferite:
public class Question implements Serializable{
static final long serialVersionUID = 0x0000CAFEBABE0000;
public String questionBody;
public String[] answers;
public int correctAnswerIndex;
}
Totuși, dacă două clase au aceeași valoare pentru câmpul serialVersionUID, dar ele diferă ca implementare, atunci metoda readObject() va arunca o excepție de tip java.io.InvalidClassException semnalând că există o problemă de compatibilitate între clasa disponibilă și obiectul serializat. Toate aceste excepții sunt extinse din clasa java.io.ObjectStreamException.
Pentru a putea fi compatibilă cu orice tip de obiect, metoda readObject() din clasa ObjectInputStream întoarce o referință la un obiect de tip Object. Astfel, pentru a-l putea folosi în aplicații reale, programatorul trebuie să facă un up-cast explicit:
import java.io.*;
public class ObjectReader{
public static void main(String[] _args){
try{
InputStream _byteStream = getSomeInputStream();
ObjectInputStream _objectStream = new ObjectInputStream(_byteStream);
Question _q1;
_q1 = (Question)_objectStream.readObject();
_objectStream.close();
}catch (ClassNotFoundException _cnfe) {
System.out.println("Class not available: " + _cnfe.getMessage());
}catch(ObjectStreamException _ose){
// ObjectStreamException first since it inherits IOException
System.out.println("Unable to deserialize (Object stream problem): " + _ose.getMessage());
}catch(IOException _ioe){
System.out.println("Unable to deserialize (Byte stream problem): " + _ioe.getMessage());
}
}
public static InputStream getSomeInputStream(){
//...
}
}
Probleme cunoscute la serializare
- Dacă un obiect este scris pe un stream și apoi citit, acest obiect citit și obiectul original NU vor avea aceeași referință, adică vor fi, de fapt, două obiecte distince, dar cu același conținut.
- Mașina virtuală creează un cache de obiecte atunci când acestea sunt scrise. Astfel, dacă un obiect este scris pe un stream, unul din câmpuri modificat, iar apoi obiectul scris din nou, modificarea nu va apărea la scriere, pentru că, de fapt, scriere se face folosind obiectul vechi, din zona cache. Pentru a evita această problemă, există două varinate:
- puteți să copiați obiectul original, să-l modificați, apoi să-l trimiteți ca un obiect nou;
- puteți apela metoda ObjectOutputStream.reset() care șterge acea memorie cache.
- Dacă o resursă conține un InputStream și un OutputStream și se dorește definirea unor ObjectInputStream și ObjectOutputStream asociate, întotdeauna aveți grijă să creați întâi stream-ul de scriere de obiecte (ObjectOutputStream).