Java面向對象系列[v1.0.0][對象序列化]

對象序列化機制和作用

對象序列化的目標是將對象保存到磁盤中,或允許在網絡中直接傳輸對象,對象序列化機制允許把內存中的Java對象轉換成平臺無關的二進制流,從而允許吧這種二進制流持久的保存在磁盤上,通過網絡將這種二進制流傳輸到另一個網絡節點,其他程序一旦獲得了這種二進制流,無論是從磁盤中獲取還是從網絡中獲取,都可以將這種二進制流回複稱原來的Java對象


  • 序列化機制允許將實現序列化的Java對象轉換成字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以備以後重新恢復成原來的對象,序列化機制使得對象可以脫離程序的運行而獨立存在
  • 對象的序列化(Serialize)是指將一個Java對象寫入IO流中,與此對應的是,對象的反序列化(Deserialize)則指從IO流中恢復該Java對象
  • Java9之後增強了對象序列化機制,它允許對讀入的序列化數據進行過濾,這種過濾可在反序列化之前對數據執行校驗,從而提高安全性和健壯性
  • 如果要讓某個對象支持序列化機制,則必須讓它的類是可序列化的(serializable)的,要讓這個類是可序列化的,該類必須實現如下兩個接口之一
    • Serializable
    • Externalizable
  • Java的很多類已經實現了Serializable,該接口是一個標記接口,實現該接口無需實現任何方法,它只是表明該類的實例是可序列化的
  • 所有可能在網絡上傳輸的對象的類都應該是可序列化的,否則程序將會出現異常,例如RMI(Remote Method Invoke,遠程方法調用) 過程中的參數和返回值,所有需要保存到磁盤裏的對象的類都必須可序列化,例如Web應用中需要保存到HttpSession或ServletContext屬性的Java對象
  • 序列化是RMI過程的參數和返回值都必須實現的機制,而所有分佈式應用常常需要跨平臺、跨網絡,所以要求所有傳遞的參數、返回值必須實現序列化,通常情況下程序創建的每個JavaBean類都應該實現Serializable

使用對象流實現序列化

如果需要將某個對象保存到磁盤上或者通過網絡傳輸,則這個類應該實現Serializable接口或者Externalizable接口,而使用Serializable來實現序列化非常簡單,只要讓目標淚實現Serializable接口即可,無需實現任何方法。
當一個類實現了Serializable接口後,其對象就是可序列化的,程序可以通過如下兩個步驟來序列化該對象

  • 創建一個ObjectOutputStream,這個輸出流是一個處理流,所以必須建立在其他節點流的基礎之上
// 創建個ObjectOutputStream輸出流
ObjectOutputStream oos = ObjectOutputStream(new FileOutputStream("object.txt"));
  • 調用ObjectOutputStream對象的writeObject()方法輸出可序列化對象
//將一個Person對象輸出到輸出流中
oos.writeObject(per);
public class Person
	implements java.io.Serializable
{
	private String name;
	private int age;
	// 注意此處沒有提供無參數的構造器!
	public Person(String name, int age)
	{
		System.out.println("有參數的構造器");
		this.name = name;
		this.age = age;
	}
	// 省略name與age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}
}

定義了一個Person類,且實現了Serializable接口,於是Person類可序列化,然後使用ObjectOutputStream將一個Person對象寫入磁盤文件

import java.io.*;

public class WriteObject
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectOutputStream輸出流
			var oos = new ObjectOutputStream(new FileOutputStream("object.txt")))
		{
			var per = new Person("孫悟空", 500);
			// 將per對象寫入輸出流
			oos.writeObject(per);
		}
		catch (IOException ex)
		{
			ex.printStackTrace();
		}
	}
}

如果要在二進制流中恢復Java對象,則需要使用反序列化,反序列化的實現分爲兩步:

  • 創建一個ObjectInputStream輸入流,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎上
// 創建一個ObjectInputStream輸入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
  • 調用ObjectInputStream對象的readObject()方法讀取流中的對象,該方法返回一個Object類型的Java對象,如果程序知道該Java對象的類型,則可以將該對象強制類型轉換成其真實的類型
// 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
Person p = (Person)ois.readObject();
import java.io.*;

public class ReadObject
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectInputStream輸入流
			var ois = new ObjectInputStream(new FileInputStream("object.txt")))
		{
			// 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
			var p = (Person) ois.readObject();
			System.out.println("名字爲:" + p.getName()
				+ "\n年齡爲:" + p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

反序列化讀取的僅僅是Java對象的數據,而不是Java類,因此採用反序列化恢復Java對象時,必須提供該Java對象所屬類的class文件,否則將會拋ClassNotFoundException異常

  • 如果使用序列化機制向文件中寫入了多個Java對象,使用反序列化機制恢復對象時必須按實際寫入的順序讀取
  • 當一個可序列化類有多個父類時(包括直接父類和間接父類),這些父類要麼有無參數的構造器,要麼也是可序列化的,否則反序列化時將拋出InvalidClassException,並且如果父類是不可序列化的,只是帶有無參數的構造器,則該父類中定義的成員變量值不會序列化到二進制流中

對象引用的序列化

前面的Person類的兩個成員變量分別是String類型和int類型,如果某個類的成員變量的類型不是基本類型或者String類型,而是另一個引用類型,那麼這個引用類型必須是可序列化的,否則擁有該類型成員變量的類也不不可序列化的
如下代碼所示,Teacher類持有一個Person類的引用,只有Person類是可序列化的,Teacher類纔是可序列化的,如果Person類不可序列化,則無論Teacher類是否實現Serilizable、Externalizable接口,Teacher類都是不可序列化的

public class Teacher
	implements java.io.Serializable
{
	private String name;
	private Person student;
	public Teacher(String name, Person student)
	{
		this.name = name;
		this.student = student;
	}
	// 此處省略了name和student的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// student的setter和getter方法
	public void setStudent(Person student)
	{
		this.student = student;
	}
	public Person getStudent()
	{
		return this.student;
	}
}
Person per = new Person("孫子", 500);
Teacher t1 = new Teacher("孫賊", per);
Teacher t2 = new Teacher("孫砸", per);

創建了兩個Teacher對象和一個Person對象,這3個對象在內存中如圖所示:
在這裏插入圖片描述
那麼問題就來了:

  • 如果先序列化t1對象,則系統將該t1對象所引用的Person對象一起序列化
  • 如果先序列化t2對象,則系統將該t2對象所引用的Person對象一起序列化
  • 如果先序列化per對象,則系統將再次序列化該Person對象

如果系統向輸出流中寫入了三個Person對象,那麼當程序從輸入流中反序列化這些對象時,將會得到3個Person對象,這顯然違背了Java序列化機制的初衷


解決問題(Java序列化機制採用了一種特殊的序列化算法):

  • 所有保存到磁盤中的對象都有一個序列化編號
  • 當程序視圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在當前JVM中)被序列化過,系統纔會將該對象轉換成字節序列並輸出
  • 如果某個對象已經序列化過,程序將只是直接輸出一個序列化編號,而不是再次重新序列化該對象

假設用如下順序的序列化代碼:

oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);

則序列化後磁盤文件的存儲示意圖如圖所示
在這裏插入圖片描述

import java.io.*;

public class WriteTeacher
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectOutputStream輸出流
			var oos = new ObjectOutputStream(new FileOutputStream("teacher.txt")))
		{
			var per = new Person("孫悟空", 500);
			var t1 = new Teacher("唐僧", per);
			var t2 = new Teacher("菩提祖師", per);
			// 依次將四個對象寫入輸出流
			oos.writeObject(t1);
			oos.writeObject(t2);
			oos.writeObject(per);
			oos.writeObject(t2);
		}
		catch (IOException ex)
		{
			ex.printStackTrace();
		}
	}
}


import java.io.*;

public class ReadTeacher
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectInputStream輸出流
			var ois = new ObjectInputStream(new FileInputStream("teacher.txt")))
		{
			// 依次讀取ObjectInputStream輸入流中的四個對象
			var t1 = (Teacher) ois.readObject();
			var t2 = (Teacher) ois.readObject();
			var p = (Person) ois.readObject();
			var t3 = (Teacher) ois.readObject();
			// 輸出true
			System.out.println("t1的student引用和p是否相同:"
				+ (t1.getStudent() == p));
			// 輸出true
			System.out.println("t2的student引用和p是否相同:"
				+ (t2.getStudent() == p));
			// 輸出true
			System.out.println("t2和t3是否是同一個對象:"
				+ (t2 == t3));
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

那麼問題又來了:只有第一次使用writeObject()方法輸出時纔會將該對象轉換成字節序列,之後再調用只是返回序列化編號,那麼如果這個對象是可變對象,當他在被序列化後發生了變化的情況下,就不會再次序列化輸出

import java.io.*;

public class SerializeMutable
{
	public static void main(String[] args)
	{

		try (
			// 創建一個ObjectOutputStream輸入流
			var oos = new ObjectOutputStream(new FileOutputStream("mutable.txt"));
			// 創建一個ObjectInputStream輸入流
			var ois = new ObjectInputStream(new FileInputStream("mutable.txt")))
		{
			var per = new Person("孫悟空", 500);
			// 系統會per對象轉換字節序列並輸出
			oos.writeObject(per);
			// 改變per對象的name實例變量
			per.setName("豬八戒");
			// 系統只是輸出序列化編號,所以改變後的name不會被序列化
			oos.writeObject(per);
			var p1 = (Person) ois.readObject();    // ①
			var p2 = (Person) ois.readObject();    // ②
			// 下面輸出true,即反序列化後p1等於p2
			System.out.println(p1 == p2);
			// 下面依然看到輸出"孫悟空",即改變後的實例變量沒有被序列化
			System.out.println(p2.getName());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章