1. 程式人生 > >Java IO操作——物件序列化(Serializable介面、ObjectOutputStream、以及與Externalizable介面的用法和區別)

Java IO操作——物件序列化(Serializable介面、ObjectOutputStream、以及與Externalizable介面的用法和區別)

學習目標

掌握物件序列化的作用。 掌握Serializable介面的作用。 可以使用ObjectOutputStream進行物件的序列化操作。 可以使用ObjectInputStream進行物件的反序列化操作。 掌握Externalizable介面的作用及與Serializable介面的實現區別。 掌握transient關鍵字的作用。 可以序列化一組物件。

物件序列化

物件序列化,就是把一個物件變為二進位制的資料流的一種方法,通過物件序列化可以方便的實現物件的傳輸或儲存。
如果一個類的物件想被序列化,則物件所在的類必須實現java.io.Serializable介面。此介面的定義如下: public interface Serializable{ }
一個類不能被無緣無故的被序列化。 但是,在此介面中沒有任何一個方法,此介面屬於一個標示介面,表示具備了某種能力。例如:現在定義一個類,此類可以被序列化。 定義一個可被序列化的類:
import java.io.Serializable ;
public class Person implements Serializable{
	private String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
};
以後此類的物件就可以被序列化了。變為二進位制byte流。

物件的序列化和反序列化

要想完成物件的輸入或輸出,還必須依靠物件輸出流(ObjectOutputStream)和物件輸入流(ObjectInputStream), 使用物件輸出流輸出序列化物件的步驟,有時也稱為序列化,而使用物件輸入流讀入的過程,有時也稱為反序列化。

serialVersionUID

在物件進行序列化或反序列化操作的時候,要考慮JDK版本的問題,如果序列化的JDK版本和反序列化的JDK版本不統一則就有可能造成異常。所以在序列化操作中引入了一個serialVersionUID的常量,可以通過此常量來驗證版本的一致性,在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現版本不一致的異常。
但是在進行序列化或反序列化操作的時候,對於不同的JDK版本,實際上會出現版本的相容問題。
import java.io.Serializable ;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
};
如果使用開發工具開發,沒有編寫此程式碼,則會出現一些安全警告的資訊。

物件的序列化及反序列化操作

  物件序列化依靠ObjectOutputStream,物件反序列化依靠ObjectInputStream

 序列化:ObjectOutputStream

物件輸出流:ObjectOutputStream 一個物件如果想要進行輸出,則必須使用ObjectOutputStream類,此類定義如下: public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants  此類的常用方法如下: 1、public ObjectOutputStream(OutputStream out) throws IOException構造方法  傳入輸出的物件 2、public final void writeObject(Object obj) throws IOException  普通方法 輸出物件 此類的使用形式與PrintStream非常的相似,在例項化時也需要傳入一個OutputStream的子類物件,之後根據傳入的OutputStream子類的物件的不同,輸出的位置也不同。
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
public class SerDemo01{
	public static void main(String args[]) throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectOutputStream oos = null ;	// 宣告物件輸出流
		OutputStream out = new FileOutputStream(f) ;	// 檔案輸出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("張三",30)) ;	// 儲存物件
		oos.close() ;	// 關閉
	}
};
序列化的真正內容:所有的物件擁有各自的屬性,但是所有的方法都是公共的,所以序列化物件的時候實際上序列化的就是屬性。

反序列化:ObjectInputStream

物件輸入流:ObjectInputStream 使用ObjectInputStream可以直接把被序列化好的物件反序列化回來。 ObjectInputStream的定義如下: public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants 此類的常用方法如下: 1、public ObjectInputStream(InputStream in) throws IOException 構造方法 構造輸入物件。 2、public final Object readObject() throws IOException, ClassNotFoundException普通方法 從指定位置讀取物件。 此類也是InputStream的子類,與PrintStream類的使用類似,此類同樣需要接收InputStream類的例項才可以被例項化。
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
public class SerDemo01{
	public static void main(String args[]) throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectOutputStream oos = null ;	// 宣告物件輸出流
		OutputStream out = new FileOutputStream(f) ;	// 檔案輸出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("張三",30)) ;	// 儲存物件
		oos.close() ;	// 關閉
	}
};

問題:   如果一個類實現了Serializable介面,則肯定此類可以被序列化下來,那麼也就意味著此類多了一項功能,那麼讓所有的類都實現此介面是不是更好?   其實並不是這樣,因為JDK是不斷升級的,現在的Serialzable介面中沒有任何定義,一旦JDK升級後在裡面定義了類必須實現的方法,那麼對所有類的維護就會變得更加困難,額外增加了很多代價。

Externalizable介面

被Serializable介面宣告的類其物件的內容都將被序列化,如果現在使用者希望可以自己指定序列化的內容,則可以讓一個類實現Externalizable介面,此介面的定義如下: public interface Externalizable extends Serializable{     public void writeExternal(ObjectOutput out) throws IOException;// 寫入     public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;// 讀取 } 利用此介面修改之前的程式:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		// TODO Auto-generated method stub
		  this.name = (String)in.readObject();
		  this.age = in.readInt();
	}
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeObject(this.name); // 儲存姓名屬性
		out.writeInt(this.age);
	}

};
為了方便測試,現在將序列化及反序列化操作弄成方法呼叫的形式。
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo03{
	public static void main(String args[]) throws Exception{
//		ser() ;
		dser() ;
	}
	public static void ser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectOutputStream oos = null ;	// 宣告物件輸出流
		OutputStream out = new FileOutputStream(f) ;	// 檔案輸出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("張三",30)) ;	// 儲存物件
		oos.close() ;	// 關閉
	}
	public static void dser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectInputStream ois = null ;	// 宣告物件輸入流
		InputStream input = new FileInputStream(f) ;	// 檔案輸入流
		ois = new ObjectInputStream(input) ;	// 例項化物件輸入流
		Object obj = ois.readObject() ;	// 讀取物件
		ois.close() ;	// 關閉
		System.out.println(obj) ;
	}
};
以上程式執行的時候出現了一個錯誤:
在使用Externalizable介面的時候需要在被序列化的類中定義一個無參構造,因為此介面在進行反序列化的時候,會先使用類中的無參構造方法為其進行例項化,之後再將內容分別設定到屬性之中,再次修改Person類如下:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(){}; 
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		// TODO Auto-generated method stub
		  this.name = (String)in.readObject();
		  this.age = in.readInt();
	}
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeObject(this.name); // 儲存姓名屬性
		out.writeInt(this.age);
	}

};

Externalizable 介面與 Serializable介面實現序列化的區別

在開發中使用Serializable介面是最多的。而Externalizable介面基本上是不會出現的。

transient關鍵字

當使用Serializable介面實現序列化操作時,如果一個物件中的某個屬性不希望被序列化的話,則可以使用transient關鍵字進行宣告。
import java.io.Serializable ;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private transient String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
};
操作程式碼:
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo04{
	public static void main(String args[]) throws Exception{
		ser() ;
		dser() ;
	}
	public static void ser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectOutputStream oos = null ;	// 宣告物件輸出流
		OutputStream out = new FileOutputStream(f) ;	// 檔案輸出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("張三",30)) ;	// 儲存物件
		oos.close() ;	// 關閉
	}
	public static void dser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectInputStream ois = null ;	// 宣告物件輸入流
		InputStream input = new FileInputStream(f) ;	// 檔案輸入流
		ois = new ObjectInputStream(input) ;	// 例項化物件輸入流
		Object obj = ois.readObject() ;	// 讀取物件
		ois.close() ;	// 關閉
		System.out.println(obj) ;
	}
};

transient+Serializable介面完全可以取代Externalizable介面的功能。

序列化一組物件

物件輸出時只提供了一個物件的輸出操作(writeObject(Object obj)),並沒有提供多個物件的輸出,所以如果現在要同時序列化多個物件的時候就可以使用物件陣列進行操作,因為陣列屬於引用資料型別,所以可以直接使用Object型別進行接收。
如果要儲存多個物件,則最好使用物件陣列的形式完成。
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo05{
	public static void main(String args[]) throws Exception{
		Person per[] = {new Person("張三",30),new Person("李四",31),
			new Person("王五",32)} ;
		ser(per) ;
		Object o[] = (Object[])dser() ;
		for(int i=0;i<o.length;i++){
			Person p = (Person)o[i] ;
			System.out.println(p) ;
		}
	}
	public static void ser(Object obj[]) throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectOutputStream oos = null ;	// 宣告物件輸出流
		OutputStream out = new FileOutputStream(f) ;	// 檔案輸出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(obj) ;	// 儲存物件
		oos.close() ;	// 關閉
	}
	public static Object[] dser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定義儲存路徑
		ObjectInputStream ois = null ;	// 宣告物件輸入流
		InputStream input = new FileInputStream(f) ;	// 檔案輸入流
		ois = new ObjectInputStream(input) ;	// 例項化物件輸入流
		Object obj[] = (Object[])ois.readObject() ;	// 讀取物件
		ois.close() ;	// 關閉
		return obj ;
	}
};

問題:儲存的資料有限,所以為了解決這樣的問題,JAVA中引入了類集框架解決陣列的儲存限制問題。

關於複雜型別的類操作如下:

import java.io.Serializable ;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private  String name ;	// 宣告name屬性,但是此屬性不被序列化
	private int age ;		// 宣告age屬性
	public Person(String name,int age){	// 通過構造設定內容
		this.name = name ;
		this.age = 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;
	}

	public String toString(){	// 覆寫toString()方法
		return "姓名:" + this.name + ";年齡:" + this.age ;
	}
};
class Book implements Serializable{
	private static final long serialVersionUID = 1L;
	private String bookName;
	private float price;
	public Book(String bookName,float price){
		this.bookName = bookName;
		this.price = price;
	}
	public String toString() {
		return "書名:"+this.bookName+"; 價格:"+this.price;
	}
}
class Student extends Person{
	private static final long serialVersionUID = 1L;
	private int score ;		// 宣告學分屬性
	private List<Book> books = new ArrayList<Book>();
	public Student(String name,int age,int score,Book ...books){	// 通過構造設定內容
	    super(name, age); //呼叫父類構造方法
		this.score = score ;
		Collections.addAll(this.books, books);
	}
	public String toString(){	// 覆寫toString()方法
		return super.toString()+"; 學分"+this.score +"\n"+this.books;
	}
};


操作:
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo04{
	public static void main(String args[]) throws Exception{
		ser() ;
		dser() ;
	}
	public static void ser() throws Exception {
		File f = new File("/Users/liuxun/Downloads/test.txt") ;	// 定義儲存路徑
		ObjectOutputStream oos = null ;	// 宣告物件輸出流
		OutputStream out = new FileOutputStream(f) ;	// 檔案輸出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Student("張三",30,80,new Book("一萬個為什麼",23.5f))) ;	// 儲存物件
		oos.close() ;	// 關閉
	}
	public static void dser() throws Exception {
		File f = new File("/Users/liuxun/Downloads/test.txt") ;	// 定義儲存路徑
		ObjectInputStream ois = null ;	// 宣告物件輸入流
		InputStream input = new FileInputStream(f) ;	// 檔案輸入流
		ois = new ObjectInputStream(input) ;	// 例項化物件輸入流
		Object obj = ois.readObject() ;	// 讀取物件
		ois.close() ;	// 關閉
		System.out.println(obj) ;
	}
};


總結: 1、物件序列化的作用:物件序列化不一定都向檔案中儲存,也有可能面向於其他的輸入或輸出。 2、被序列化的物件的類必須實現Serializable介面,如果某個屬性不希望被儲存下來,則可以使用transient關鍵字宣告。 3、關於巢狀或繼承的複雜類,則所有涉及的類都直接實現Serializable介面即可。