1. 程式人生 > >java的序列化 和 反序列化總結---學習筆記

java的序列化 和 反序列化總結---學習筆記

  java的序列化 和 反序列化

1、我們先看一下《java程式設計思想》第四版中對序列化定義

物件序列化
Java 1.1 增添了一種有趣的特性,名為“物件序列化”( Object Serialization)。它面向那些實現了
Serializable 介面的物件,可將它們轉換成一系列位元組,並可在以後完全恢復回原來的樣子。這一過程亦可
通過網路進行。這意味著序列化機制能自動補償作業系統間的差異。換句話說,可以先在Windows 機器上創
建一個物件,對其序列化,然後通過網路發給一臺 Unix 機器,然後在那裡準確無誤地重新“裝配”。不必關
心資料在不同機器上如何表示,也不必關心位元組的順序或者其他任何細節。
就其本身來說,物件的序列化是非常有趣的,因為利用它可以實現“有限持久化”。請記住“持久化”意味
著物件的“生存時間”並不取決於程式是否正在執行—— 它存在或“生存”於程式的每一次呼叫之間。通過
序列化一個物件,將其寫入磁碟,以後在程式重新呼叫時重新恢復那個物件,就能圓滿實現一種“持久”效
果。之所以稱其為“有限”,是因為不能用某種“ persistent”(持久)關鍵字簡單地地定義一個物件,並
讓系統自動照看其他所有細節問題(儘管將來可能成為現實)。相反,必須在自己的程式中明確地序列化和
組裝物件。
語言裡增加了物件序列化的概念後,可提供對兩種主要特性的支援。 Java 1.1 的“遠端方法呼叫”( RMI)
使本來存在於其他機器的物件可以表現出好象就在本地機器上的行為。將訊息發給遠端物件時,需要通過對
象序列化來傳輸引數和返回值。 RMI 將在第 15 章作具體討論。
物件的序列化也是 Java Beans 必需的,後者由 Java 1.1 引入。使用一個 Bean 時,它的狀態資訊通常在設計
期間配置好。程式啟動以後,這種狀態資訊必須儲存下來,以便程式啟動以後恢復;具體工作由物件序列化
完成。
物件的序列化處理非常簡單,只需物件實現了 Serializable 介面即可(該介面僅是一個標記,沒有方法)。
在 Java 1.1 中,許多標準庫類都發生了改變,以便能夠序列化—— 其中包括用於基本資料型別的全部封裝
器、所有集合類以及其他許多東西。甚至 Class 物件也可以序列化(第 11 章講述了具體實現過程)。
為序列化一個物件,首先要建立某些 OutputStream 物件,然後將其封裝到 ObjectOutputStream 物件內。此
時,只需呼叫 writeObject()即可完成物件的序列化,並將其傳送給 OutputStream。相反的過程是將一個
InputStream 封裝到 ObjectInputStream 內,然後呼叫 readObject()。和往常一樣,我們最後獲得的是指向
一個上溯造型 Object 的控制代碼,所以必須下溯造型,以便能夠直接設定。
物件序列化特別“聰明”的一個地方是它不僅儲存了物件的“全景圖”,而且能追蹤物件內包含的所有控制代碼
並儲存那些物件;接著又能對每個物件內包含的控制代碼進行追蹤;以此類推。我們有時將這種情況稱為“物件
網”,單個物件可與之建立連線。而且它還包含了物件的控制代碼陣列以及成員物件。若必須自行操縱一套物件
序列化機制,那麼在程式碼裡追蹤所有這些連結時可能會顯得非常麻煩。在另一方面,由於Java 物件的序列化
似乎找不出什麼缺點,所以請儘量不要自己動手,讓它用優化的演算法自動維護整個物件網。

下面是序列化的小例子:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;


public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
	private String name;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	
	@Override
	public String toString() {
		return "Person [name=" + name + "]";
	}
	public static void main(String[] args)  {
		Person p=new Person();
		p.setName("張三");
		try {
			
		File f=new File("d:/3.txt");
		if(!f.exists()){
			f.createNewFile();
		}
		
		ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(f));
		
		
		oos.writeObject(p);
		oos.flush();
		oos.close();
		
		} catch (Exception e) {
			e.printStackTrace();
			// TODO: handle exception
		}
		
		try {
			ObjectInputStream ois=new ObjectInputStream(new FileInputStream("d:/3.txt"));
			
			Person pp=(Person)ois.readObject();
			System.out.println(pp);
			
		} catch (Exception e) {
			e.printStackTrace();
			// TODO: handle exception
		}
	
		
	}
	
	
}
結果為:

Person [name=張三]

從上面的程式碼中可以知道序列化物件讀取序列化物件過程中最要的兩個函式是OutStream的writeObject和InputStream的readObject ,這個序列化過程是預設的。當然如果有特殊要求可以自己定製序列化。下面是書中例子

2、序列化的控制
正如大家看到的那樣,預設的序列化機制並不難操縱。然而,假若有特殊要求又該怎麼辦呢?我們可能有特殊的安全問題,不希望物件的某一部分序列化;或者某一個子物件完全不必序列化,因為物件恢復以後,那一部分需要重新建立。此時,通過實現 Externalizable 介面,用它代替 Serializable 介面,便可控制序列化的具體過程。這個
Externalizable 介面擴充套件了 Serializable,並增添了兩個方法: writeExternal()和 readExternal()。在序
列化和重新裝配的過程中,會自動呼叫這兩個方法,以便我們執行一些特殊操作。
下面這個例子展示了 Externalizable 介面方法的簡單應用。注意 Blip1 和 Blip2 幾乎完全一致,除了極微小

的差別(自己研究一下程式碼,看看是否能發現):

//: Blips.java
// Simple use of Externalizable & a pitfall
import java.io.*;
import java.util.*;

class Blip1 implements Externalizable {
    public Blip1() {
        System.out.println("Blip1 Constructor");
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Blip1.writeExternal");
    }

    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        System.out.println("Blip1.readExternal");
    }
}

class Blip2 implements Externalizable {
    Blip2() {
        System.out.println("Blip2 Constructor");
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Blip2.writeExternal");
    }

    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        System.out.println("Blip2.readExternal");
    }
}

public class Blips {
    public static void main(String[] args) {
        System.out.println("Constructing objects:");
        Blips b1 = new Blips();
        Blip2 b2 = new Blip2();
        try {
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
                    "Blips.out"));
            System.out.println("Saving objects:");
            o.writeObject(b1);
            o.writeObject(b2);
            o.close();
            // Now get them back:
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(
                    "Blips.out"));
            System.out.println("Recovering b1:");
            b1 = (Blips) in.readObject();
            // OOPS! Throws an exception:
            // ! System.out.println("Recovering b2:");
            // ! b2 = (Blip2)in.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} // /:~

該程式輸出如下:
Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal
未恢復 Blip2 物件的原因是那樣做會導致一個違例。你找出了 Blip1 和 Blip2 之間的區別嗎? Blip1 的構建
器是“公共的”( public), Blip2 的構建器則不然,這樣便會在恢復時造成違例。試試將 Blip2 的構建器
屬性變成“ public”,然後刪除//!註釋標記,看看是否能得到正確的結果。
恢復 b1 後,會呼叫 Blip1 預設構建器。這與恢復一個 Serializable(可序列化)物件不同。在後者的情況
下,物件完全以它儲存下來的二進位制位為基礎恢復,不存在構建器呼叫。而對一個Externalizable 物件,所
有普通的預設構建行為都會發生(包括在欄位定義時的初始化),而且會呼叫readExternal()。必須注意這
一事實—— 特別注意所有預設的構建行為都會進行— — 否則很難在自己的 Externalizable 物件中產生正確的
行為。
下面這個例子揭示了儲存和恢復一個 Externalizable 物件必須做的全部事情:

//: Blip3.java
// Reconstructing an externalizable object
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

class Blip3 implements Externalizable {
	int i;
	String s; // No initialization

	public Blip3() {
		System.out.println("Blip3 Constructor");
		// s, i not initialized
	}

	public Blip3(String x, int a) {
		System.out.println("Blip3(String x, int a)");
		s = x;
		i = a;
		// s & i initialized only in non-default
		// constructor.
	}

	public String toString() {
		return s + i;
	}

	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("Blip3.writeExternal");
		// You must do this:
		out.writeObject(s);
		out.writeInt(i);
	}

	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		System.out.println("Blip3.readExternal");
		// You must do this:
		s = (String) in.readObject();
		i = in.readInt();
	}

	public static void main(String[] args) {
		System.out.println("Constructing objects:");
		Blip3 b3 = new Blip3("A String ", 47);
		System.out.println(b3.toString());
		try {
			ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
					"Blip3.out"));
			System.out.println("Saving object:");
			o.writeObject(b3);
			o.close();
			// Now get it back:
			ObjectInputStream in = new ObjectInputStream(new FileInputStream(
					"Blip3.out"));
			System.out.println("Recovering b3:");
			b3 = (Blip3) in.readObject();
			System.out.println(b3.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
} // /:~

其中,欄位 s 和 i 只在第二個構建器中初始化,不關預設構建器的事。這意味著假如不在readExternal 中初
始化 s 和 i,它們就會成為 null(因為在物件建立的第一步中已將物件的儲存空間清除為 1)。若註釋掉跟
隨於“ You must do this”後面的兩行程式碼,並執行程式,就會發現當物件恢復以後, s 是 null,而 i 是
零。
若從一個 Externalizable 物件繼承,通常需要呼叫 writeExternal()和 readExternal()的基礎類版本,以便
正確地儲存和恢復基礎類元件。
所以為了讓一切正常運作起來,千萬不可僅在 writeExternal()方法執行期間寫入物件的重要資料(沒有默
認的行為可用來為一個 Externalizable 物件寫入所有成員物件)的,而是必須在 readExternal()方法中也
恢復那些資料。初次操作時可能會有些不習慣,因為Externalizable 物件的預設構建行為使其看起來似乎正
在進行某種儲存與恢復操作。但實情並非如此。

3、transient(臨時)關鍵字


控制序列化過程時,可能有一個特定的子物件不願讓Java 的序列化機制自動儲存與恢復。一般地,若那個子
物件包含了不想序列化的敏感資訊(如密碼),就會面臨這種情況。即使那種資訊在物件中具有“ private”
(私有)屬性,但一旦經序列化處理,人們就可以通過讀取一個檔案,或者攔截網路傳輸得到它。
為防止物件的敏感部分被序列化,一個辦法是將自己的類實現為Externalizable,就象前面展示的那樣。這
樣一來,沒有任何東西可以自動序列化,只能在writeExternal()明確序列化那些需要的部分。
然而,若操作的是一個 Serializable 物件,所有序列化操作都會自動進行。為解決這個問題,可以用
transient(臨時) 逐個欄位地關閉序列化,它的意思是“不要麻煩你(指自動機制)儲存或恢復它了—— 我
會自己處理的”。
例如,假設一個 Login 物件包含了與一個特定的登入會話有關的資訊。校驗登入的合法性時,一般都想將數
據儲存下來,但不包括密碼。為做到這一點,最簡單的辦法是實現Serializable,並將 password 欄位設為
transient。有時候需要序列化的物件中的屬性包含有不能序列化的物件。例如下面的例子:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;


public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
	private String name;
	private Student student;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Student getStudent() {
		return student;
	}
	public void setStudent(Student student) {
		this.student = student;
	}
	
	@Override
	public String toString() {
		return "Person [name=" + name + ", student=" + student + "]";
	}
	public static void main(String[] args)  {
		Person p=new Person();
		p.setStudent(new Student("張三","4歲"));
		p.setName("學生");
		try {
			
		File f=new File("d:/3.txt");
		if(!f.exists()){
			f.createNewFile();
		}
		
		ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(f));
		
		
		oos.writeObject(p);
		oos.flush();
		oos.close();
		
		} catch (Exception e) {
			e.printStackTrace();
			// TODO: handle exception
		}
		
		try {
			ObjectInputStream ois=new ObjectInputStream(new FileInputStream("d:/3.txt"));
			
			Person pp=(Person)ois.readObject();
			System.out.println(pp);
			
		} catch (Exception e) {
			e.printStackTrace();
			// TODO: handle exception
		}
	
		
	}
	
	
}

class Student {

	private String name;
	private String age;
	
	
	
	public Student() {
		super();
		// TODO Auto-generated constructor stub
	}
	
	public Student(String name, String age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAge() {
		return age;
	}
	public void setAge(String age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
	
	
	
}

執行結果報錯:

java.io.NotSerializableException: Student

這是由於Student 類不可以序列化, 在這種情況下,可以給Student 新增 transient 關鍵字,

    private transient Student student;

執行結果:

Person [name=學生, student=null]

如果Student是可以序列化的,Student類實現Serializable,去掉transient 關鍵字:

執行結果:

Person [name=學生, student=Student [name=張三, age=4歲]]

4 總結
序列化:將java物件轉換為位元組序列的過程叫做序列化

反序列化:將位元組物件轉換為java物件的過程叫做反序列化

通過實現 Externalizable 介面,用它代替 Serializable 介面,便可控制序列化的具體過程,這個
Externalizable 介面擴充套件了 Serializable,並增添了兩個方法: writeExternal()和 readExternal()。

transient 可以使某些屬性不被序列化,但是如果是實現了Externalizable介面的類中屬性新增 transient也是會被序列化的.

本文參考了<java程式設計思想>第四版 第10章第九節