引自《Java 程式設計思想》
當建立物件時,在程式執行期間可以獲取,但是程式終止時,所有的物件都會被清除,我們是無法再獲取的。當然,你可以透過將資訊寫入檔案或者資料庫來達到目的。但是為了更方便,Java為我們提供了序列化機制,並且遮蔽了大部分細節。 ——Bruce Eckel
Java提供序列化作用:
- Java的遠端方法呼叫-RMI。
- Java Beans的使用。
使用Java物件序列化,在儲存物件時,會把其狀態儲存為一組位元組,在未來,再將這些位元組組裝成物件。必須注意地是,物件序列化儲存的是物件的”狀態”,即它的成員變數。由此可知,物件序列化不會關注類中的靜態變數。
2.1 序列化機制一: Serializable 介面
Serializable介面是最簡單的實現序列化的方式,如果類實現了Serializable介面,那麼就可以表明該類的物件可被序列化!否則就會報錯。常見的預設實現該介面的有所有基本型別的封裝型別,String,容器類甚至Class 物件!
//可以看到原始碼中Serializable介面內部沒有任何東西,只是一個標識!
public interface Serializable {
//nothing!
}
序列化與反序列化方法:呼叫 ObjectOutputStream 和 ObjectInputStream
package ddx.序列化;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person(18,"ddx");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out") );
out.writeObject("person storage\n");
out.writeObject(person);
out.close();
System.out.print("序列化"+"person storage\n"+ person);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.out"));
String title = (String)in.readObject(); //注意反序列化順序與序列化順序保持一致
Person person1 = (Person) in.readObject();
in.close();
System.out.print("反序列化"+title+ person1);
}
}
class Person implements Serializable{
public int age;
public String name;
Person(int age , String s){
this.age = age;
this.name = s;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
執行結果
序列化person storage
Person{age=18, name='ddx'}
反序列化person storage
Person{age=18, name='ddx'}
輸出檔案
注意點:再強調一下,物件序列化的主體是成員變數,而不包括靜態變數 一個類序列化,另一個類反序列化,分別執行!
ackage ddx.序列化;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person(18,"ddx");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out") );
out.writeObject("person storage\n");
out.writeObject(person);
out.close();
System.out.println("序列化"+"person storage\n"+ person);
}
}
class Person implements Serializable{
public int age;
public String name;
public static int count = 0;
Person(int age , String s){
this.age = age;
this.name = s;
count++;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
",count='"+count+'\'' +
'}';
}
}
package ddx.序列化;
import java.io.*;
public class test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.out"));
String title = (String) in.readObject();
Person person1 = (Person) in.readObject();
in.close();
System.out.println("反序列化" + title + person1);
}
}
序列化person storage
Person{age=18, name='ddx',count='1'}
反序列化person storage
Person{age=18, name='ddx',count='0'}
//透過以上輸出可以發現序列化不對靜態變數做任何操作,只會保留初始值!!!!
2.2 Serializable 的序列化規則
使用預設機制,在序列化物件時,不僅會序列化當前物件本身,還會對該物件引用的其它物件也進行序列化,同樣地,這些其它物件引用的另外物件也將被序列化,以此類推。這種情況被稱為物件網
所以,如果一個物件包含的成員變數是容器類物件,而這些容器所含有的元素也是容器類物件,那麼這個序列化的過程就會較複雜,開銷也較大。
2.3 Externalizable介面
Externalizable繼承於Serializable,當使用該介面時,序列化的細節需要由程式設計師去完成。
兩者的區別除了上面需要手動完成還包括下面這點:
- 使用Serializable介面的物件反序列化恢復物件時,是完全以儲存它的二進位制位構造的
- 而使用Externalizable 介面的物件反序列化恢復物件時,必須至少要有一個預設的公共的建構函式!!!!然後呼叫readExternal方法!
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
}
演示程式碼:
package ddx.序列化;
import java.io.*;
public class Main1 {
public static void main(String[] args ) throws IOException, ClassNotFoundException {
info info1 = new info(10,"ddx");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("info.out"));
out.writeObject(info1);
System.out.println("序列化之前\n"+info1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("info.out"));
info info2 = (info) in.readObject();
System.out.println("序列化之後\n"+info2);
}
}
class info implements Externalizable{
public int id;
public String name;
public info(int id, String name){
this.id = id;
this.name = name;
System.out.println("info正在執行帶參建構函式");
}
public info(){
System.out.println("info正在執行無參建構函式");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(this.id);
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//1
//in.readInt();
//in.readObject();
id = in.readInt();
name = (String)in.readObject();
}
@Override
public String toString() {
return "info{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
2.4 transient 關鍵字
使用transient關鍵字選擇不需要序列化的欄位。比如一些敏感的私有的資訊,可以選擇使用改關鍵字,虛擬機器會忽略該欄位!!!
package ddx.序列化;
import java.io.*;
public class Main2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Message message = new Message(201792237,533534);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Self_Message.out"));
out.writeObject(message);
out.close();
System.out.println("序列化之前" + message);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Self_Message.out"));
Message message1 = (Message) in.readObject();
System.out.println("序列化之後" + message1);
}
}
class Message implements Serializable {
private int id;
private transient int password; //transient 關鍵字修飾!!!
Message(int id, int password){
this.id = id;
this.password = password;
}
@Override
public String toString() {
return "message{" +
"id=" + id +
", password=" + password +
'}';
}
}
序列化之前message{id=201792237, password=533534}
序列化之後message{id=201792237, password=0}
從輸出我們看到,使用transient修飾的屬性,java序列化時,會忽略掉此欄位,所以反序列化出的物件,被transient修飾的屬性是預設值。對於引用型別,值是null;基本型別,值是0;boolean型別,值是false。
2.5 序列化永續性
同一物件序列化多次,會將這個物件序列化多次嗎?答案是否定的。
package ddx.序列化;
import java.io.*;
public class Main3 {
public static void main(String[] args) throws Exception {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
Person person = new Person(20,"路飛");
Teacher t1 = new Teacher("雷利", person);
Teacher t2 = new Teacher("紅髮香克斯", person);
//依次將4個物件寫入輸入流
oos.writeObject(person);
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(t2);//重複序列化同一個物件!!!!!!
oos.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("teacher.txt"));
Person p = (Person) in.readObject();
Teacher tt1 = (Teacher)in.readObject();
Teacher tt2 = (Teacher)in.readObject();
Teacher tt3 = (Teacher)in.readObject();
System.out.println(p);
System.out.println(tt1);
System.out.println(tt2);
System.out.println(tt3);
}
}
}
class Teacher implements Serializable{
public String name;
public Person person;
Teacher(String name, Person person){
this.name = name;
this.person = person;
}
@Override
public String toString() {
return "Teacher{" +super.toString()+
"name='" + name + '\'' +
", person=" + person +
'}';
}
}
正在初始化
ddx.序列化.Person@6f496d9f
Teacher{ddx.序列化.Teacher@723279cf name='雷利', person=ddx.序列化.Person@6f496d9f}
Teacher{ddx.序列化.Teacher@10f87f48 name='紅髮香克斯', person=ddx.序列化.Person@6f496d9f}
Teacher{ddx.序列化.Teacher@10f87f48 name='紅髮香克斯', person=ddx.序列化.Person@6f496d9f}
//可以發現person並沒有被多次序列化,而t2也只被序列化了一次!!!地址都相同!
從輸出結果可以看出,Java序列化同一物件,並不會將此物件序列化多次得到多個物件。
Java序列化演算法
- 所有儲存到磁碟的物件都有一個序列化編碼號
- 當程式試圖序列化一個物件時,會先檢查此物件是否已經序列化過,只有此物件從未(在此虛擬機器)被序列化過,才會將此物件序列化為位元組序列輸出。
- 如果此物件已經序列化過,則直接輸出編號即可。
圖示上述序列化過程(順序不同!原理一致)
我們知道,反序列化必須擁有class檔案,但隨著專案的升級,class檔案也會升級,序列化怎麼保證升級前後的相容性呢?
java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,物件也可以正確被反序列化回來。
public class Person implements Serializable {
//序列化版本號
private static final long serialVersionUID = 1111013L;
private String name;
private int age;
//省略構造方法及get,set
}
如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。
序列化版本號可自由指定,如果不指定,JVM會根據類資訊自己計算一個版本號,這樣隨著class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class檔案沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。
什麼情況下需要修改serialVersionUID呢?分三種情況
- 如果只是修改了方法,反序列化不容影響,則無需修改版本號;
- 如果只是修改了靜態變數,瞬態變數(transient修飾的變數),反序列化不受影響,無需修改版本號;
- 如果修改了非瞬態變數,則可能導致反序列化失敗。如果新類中例項變數的型別與序列化時類的型別不一致,則會反序列化失敗,這時候需要更改serialVersionUID。如果只是新增了例項變數,則反序列化回來新增的是預設值;如果減少了例項變數,反序列化時會忽略掉減少的例項變數。
- 所有需要網路傳輸的物件都需要實現序列化介面,透過建議所有的javaBean都實現Serializable介面。
- 物件的類名、例項變數(包括基本型別,陣列,對其他物件的引用)都會被序列化;方法、類變數、transient例項變數都不會被序列化。
- 如果想讓某個變數不被序列化,使用transient修飾。
- 序列化物件的引用型別成員變數,也必須是可序列化的,否則,會報錯。
- 反序列化時必須有序列化物件的class檔案。
- 當透過檔案、網路來讀取序列化後的物件時,必須按照實際寫入的順序讀取。
- 單例類序列化,需要重寫readResolve()方法;否則會破壞單例原則。
- 同一物件序列化多次,只有第一次序列化為二進位制流,以後都只是儲存序列化編號,不會重複序列化。
- 建議所有可序列化的類加上serialVersionUID 版本號,方便專案升級。
本作品採用《CC 協議》,轉載必須註明作者和本文連結