什麼是序列化和反序列化?
序列化:將對象轉化爲字節的過程稱爲序列化過程。
反序列化:將字節轉化爲對象的過程稱爲反序列化。
序列化主要應用於網絡傳輸和數據存儲的場景。在java中,只有類實現了java.io.serializable接口,該類才能被序列化。
示例Demo1.java:
package com.example.demo;
import java.io.*;
public class Demo1 {
public static class Person implements Serializable {
private static final long serialVersionUID = 233858934995755239L;
private String firstName;
private String lastName;
public Person(String firstName,String lastName){
System.out.println("Init Person...");
this.firstName = firstName;
this.lastName = lastName;
}
public String toString() {
return String.format("Pearson.toString(): firstName=%s,lastName=%s", firstName, lastName);
}
}
public static void main(String[] args) {
String firstName="Li",LastName="Kelly";
Person person = new Person(firstName, LastName);
System.out.println("序列化前:"+person.toString());
ObjectOutputStream outStream;
ObjectInputStream inStream;
String filePath = "D:/demo/person.obj";
try {
//創建一個ObjectOutputStream輸出流
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
//將對象序列化到文件filePath
outStream.writeObject(person);
inStream = new ObjectInputStream(new FileInputStream(filePath));
Person readObject = (Person)inStream.readObject();
System.out.println("反序列化後:"+readObject.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
輸出:
Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly
反序列化後:Pearson.toString(): firstName=Li,lastName=Kelly
可見對象反序列化,並不沒有調用類的構造方法(反序列化沒有輸出Init Person...)。
注:
如果一個可序列化的類的成員不是基本類型,也不是String類型,那這個引用類型也必須是可序列化的,也即繼承serializable,否則,會導致此類不能序列化。
比如上面的代碼中,我們給Person類加一個Dog對象,這個Dog沒有繼承serializable。
package com.example.demo;
import java.io.*;
public class Demo1 {
public static class Dog{
}
public static class Person implements Serializable {
private static final long serialVersionUID = 233858934995755239L;
private String firstName;
private String lastName;
private Dog dog;
public Person(String firstName,String lastName, Dog dog){
System.out.println("Init Person...");
this.firstName = firstName;
this.lastName = lastName;
this.dog = dog;
}
public String toString() {
return String.format("Pearson.toString(): firstName=%s,lastName=%s", firstName, lastName);
}
}
public static void main(String[] args) {
String firstName="Li",LastName="Kelly";
Dog dog= new Dog();
Person person = new Person(firstName, LastName,dog);
System.out.println("序列化前:"+person.toString());
ObjectOutputStream outStream;
ObjectInputStream inStream;
String filePath = "D:/demo/person.obj";
try {
//創建一個ObjectOutputStream輸出流
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
//將對象序列化到文件filePath
outStream.writeObject(person);
inStream = new ObjectInputStream(new FileInputStream(filePath));
Person readObject = (Person)inStream.readObject();
System.out.println("反序列化後:"+readObject.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
此時執行代碼會報java.io.NotSerializableException: com.example.demo.Demo1$Dog的錯誤。讓Dog繼承serializable則不會報錯。
問:
①同一個對象序列化多次,會將這個對象序列化多次嗎?否。java序列化同一對象,並不會將此對象序列化多次得到多個對象。
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
//將對象序列化到文件filePath
outStream.writeObject(person);
outStream.writeObject(person);
inStream = new ObjectInputStream(new FileInputStream(filePath));
Person readObject1 = (Person)inStream.readObject();
Person readObject2 = (Person)inStream.readObject();
System.out.println(readObject1==readObject2);
System.out.println("反序列化後:"+readObject1.toString());
output:
Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly
true
序列化後:Pearson.toString(): firstName=Li,lastName=Kelly
ReadObject1和ReadObject2是同一對象。
②有些時候,我們有這樣的需求,某些屬性不需要序列化。怎麼做?使用transient關鍵字選擇不需要序列化的字段。
示例:
package com.example.demo;
import java.io.*;
public class Demo1 {
public static class Dog implements Serializable{
}
public static class Person implements Serializable {
private static final long serialVersionUID = 233858934995755239L;
private String firstName;
private transient String lastName;
private transient int age;
private Dog dog;
public Person(String firstName,String lastName, Dog dog, int age){
System.out.println("Init Person...");
this.firstName = firstName;
this.lastName = lastName;
this.dog = dog;
this.age = age;
}
public String toString() {
return String.format("Pearson.toString(): firstName=%s,lastName=%s,age=%s", firstName, lastName,age);
}
}
public static void main(String[] args) {
String firstName="Li",LastName="Kelly";
Dog dog= new Dog();
Person person = new Person(firstName, LastName,dog,23);
System.out.println("序列化前:"+person.toString());
ObjectOutputStream outStream;
ObjectInputStream inStream;
String filePath = "D:/demo/person.obj";
try {
//創建一個ObjectOutputStream輸出流
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
//將對象序列化到文件filePath
outStream.writeObject(person);
inStream = new ObjectInputStream(new FileInputStream(filePath));
Person readObject = (Person)inStream.readObject();
System.out.println("反序列化後:"+readObject.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
輸出:
Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly,age=23
反序列化後:Pearson.toString(): firstName=Li,lastName=null,age=0
從輸出我們看到,使用transient修飾的屬性(此處是lastName、age),java序列化時,會忽略掉此類字段。
反序列化出的對象,被transient修飾的屬性是默認值。對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。
③使用transient雖然簡單,但是將此屬性完全隔離在了序列化之外。有沒有一種機制,可以定製對象序列化的方式?
答:有。通過重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化, 哪些屬性不需要。如果writeObject使用某種規則序列化,則相應的readObject需要相反的規則反序列化,以便能正確反序列化出對象。
示例:
package com.example.demo;
import java.io.*;
public class Demo1 {
public static class Dog implements Serializable{
}
public static class Person implements Serializable {
private static final long serialVersionUID = 233858934995755239L;
private String firstName;
private transient String lastName;
private transient int age;
private Dog dog;
public Person(String firstName,String lastName, Dog dog, int age){
System.out.println("Init Person...");
this.firstName = firstName;
this.lastName = lastName;
this.dog = dog;
this.age = age;
}
public String toString() {
return String.format("Pearson.toString(): firstName=%s,lastName=%s,age=%s", firstName, lastName,age);
}
private void writeObject(ObjectOutputStream out) throws IOException {
//將名字反轉寫入二進制流
out.writeObject(new StringBuffer(this.lastName).reverse());
out.writeInt(age);
}
private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
//將讀出的字符串反轉恢復回來
this.lastName = ((StringBuffer)ins.readObject()).reverse().toString();
this.age = ins.readInt();
}
}
public static void main(String[] args) {
String firstName="Li",lastName="Kelly";
Dog dog= new Dog();
Person person = new Person(firstName, lastName, dog,23);
System.out.println("序列化前:"+person.toString());
ObjectOutputStream outStream;
ObjectInputStream inStream;
String filePath = "D:/demo/person.obj";
try {
//創建一個ObjectOutputStream輸出流
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
//將對象序列化到文件filePath
outStream.writeObject(person);
inStream = new ObjectInputStream(new FileInputStream(filePath));
Person readObject = (Person)inStream.readObject();
System.out.println("反序列化後:"+readObject.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
在上面的例子中,我們給Person類增加了writeObject和readObject方法。
private void writeObject(ObjectOutputStream out) throws IOException {
//將名字反轉寫入二進制流
out.writeObject(new StringBuffer(this.lastName).reverse());
out.writeInt(age);
}
private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
//將讀出的字符串反轉恢復回來
this.lastName = ((StringBuffer)ins.readObject()).reverse().toString();
this.age = ins.readInt();
}
輸出:
Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly,age=23
反序列化後:Pearson.toString(): firstName=null,lastName=Kelly,age=23
在writeObject裏,首先out.writeObject(new StringBuffer(this.lastName).reverse());這句的作用是將名字反轉寫入二進制流。我們打開D:/demo/person.obj文件,看下lastName的形式是不是反轉的。
person.obj文件是亂碼的,但是我們仍然可以看到我們想要的,Kelly變成了yllek。
writeObject第二句是out.writeInt(age);age屬性我們加了transient關鍵字,理論上這個屬性應該不會被序列化。然而我們在這裏加上了這行代碼,所以age又可以被序列化。
除了這個,我們還可以看到writeObject和readObject都是private方法,但是它卻被外部類(ObjectOutputStream和ObjectInputStream)調用,並且它們既不存在與Java.lang.Object,也沒有在Serializeable中聲明,那它們是如何被ObjectOutputStream和ObjectInputStream調用的呢?
答:利用反射機制。ObjectOutputStream和ObjectInputStream使用了反射來尋找是否聲明瞭這兩個方法。因爲它們使用getPrivateMethod,所以這些方法不得不被聲明爲priate以至於供ObjectOutputStream來使用。
注:Write的順序和read的順序需要對應。譬如writeObject有多個字段都用writeInt寫入流中,那麼readint需要按照順序將其賦值。
例:
private void writeObject(ObjectOutputStream out) throws IOException {
//將名字反轉寫入二進制流
out.writeObject(new StringBuffer(this.lastName).reverse());
out.writeInt(age);
out.writeObject(this.firstName);
}
private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
//將讀出的字符串反轉恢復回來
this.lastName = ((StringBuffer)ins.readObject()).reverse().toString();
this.firstName =(String) ins.readObject();
this.age = ins.readInt();
}
此時代碼報錯:
java.io.OptionalDataException
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)