Java 对象序列化机制详解

对象序列化的目标:将对象保存到磁盘中,或允许在网络中直接传输对象。

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久的保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以讲这种二进制流恢复成原来的Java对象。

 

如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的,则这个类必须实现如下两个接口之一:

· Serializable

· Externalizable

Serializable是一个标志接口,它只是表明该类的实例是可序列化的。

一、 使用对象流实现序列化

一旦某个类实现了Serializable接口,则该类的对象就是可序列化的,程序通过如下步骤创建可序列化对象:

1) 创建一个ObjectOutStream,这个输出流是一个处理流:

ObjectOutputStream oos = new ObjectOutputStream("object.txt");

2) 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象:

public class Person implements java.io.Serializable
{
   public String name;

   public int age;

   // 构造方法

   // setter和getter方法
}

使用ObjectOutputStream将一个Person对象写入磁盘文件:

public class WriteObject
{
	public static void main(String[] args) 	throws Exception
	{
		ObjectOutputStream oos = new ObjectOutputStream("object.txt");
		Person per = new Person("沉缘",25);
		oos.writeObject(per);
	}
}

通过ObjectOutputStream,我们将Person对象保存到了文件中(硬盘),我们可以看到在当前目录中已经有了object.txt文件。

 

如果希望从二进制流中恢复对象,则可以通过反序列化机制,步骤如下:

1) 创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础上。

 FileInputStream fis = new FileInputStream("object.txt");
 ObjectInputStream ois = new ObjectInputStream(fis);

2) 调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,可对该对象进行强制转换:

Person per= (Person) ois.readObject();
ois.close();

实例:

public class ReadObject
{
	public static void main(String[] args)
	{
		try(
			// 创建一个ObjectInputStream输入流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("object.txt")))
		{
			// 从输入流中读取一个Java对象,并将其强制类型转换为Person类
			Person p = (Person)ois.readObject();
			System.out.println("名字为:" + p.getName()
				+ "\n年龄为:" + p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该对象所属类的class文件,否则将会引发ClassNotFoundException异常。

反序列化机制无须通过构造器来初始化Java对象。

 

二、 对象引用的序列化

在对象的嵌套过程中,比如Teacher类中有Person对象,如果希望Teacher对象是可序列化的,则Person对象也必须是可序列化的。

class Teacher implements java.io.Serializable
{
	private String name;

	private Person student;

	//构造方法

	//setter、getter方法
}

序列化机制的算法:

· 所有保存到磁盘中的对象都有一个序列化编号。

· 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未被序列化过,系统才会将该对象转换成字节序列输出。

· 如果某个对象已经被序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。

下面程序序列化两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject()方法输出同一个Teacher对象。

public class WriteTeacher
{
	public static void main(String[] args) throws Exception{
		ObjectOutputStream oos = new ObjectOutputStream("object.txt");
		Person per = new Person("沉缘",25);
		Teacher t1 = new Teacher("无情",<span style="font-family: SimSun;">per</span>);
		Teacher t2 = new Teacher("无缘",per);

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

		oos.close();
	}
}

上述程序,我们看着是序列化了四个对象,实际上只有三个,而且序列中的两个Teacher对象的student引用实际上时用一个Person对象。

接下来,我们读取引用:

public class ReadTeacher
{
	public static void main(String[] args)
	{
		try(
			// 创建一个ObjectInputStream输出流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("teacher.txt")))
		{
			// 依次读取ObjectInputStream输入流中的四个对象
			Teacher t1 = (Teacher)ois.readObject();
			Teacher t2 = (Teacher)ois.readObject();
			Person p = (Person)ois.readObject();
			Teacher 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()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序只是输出前面的序列化编号,及时后面对象的Field值已改变,改变的Field值也不会被输出。

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

		try(
			// 创建一个ObjectOutputStream输入流
			ObjectOutputStream oos = new ObjectOutputStream(
				new FileOutputStream("mutable.txt"));
			// 创建一个ObjectInputStream输入流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("mutable.txt")))
		{
			Person per = new Person("孙悟空", 500);
			// 系统会per对象转换字节序列并输出
			oos.writeObject(per);
			// 改变per对象的name Field
			per.setName("猪八戒");
			// 系统只是输出序列化编号,所以改变后的name不会被序列化
			oos.writeObject(per);
			Person p1 = (Person)ois.readObject();    //①
			Person p2 = (Person)ois.readObject();    //②
			// 下面输出true,即反序列化后p1等于p2
			System.out.println(p1 == p2);
			// 下面依然看到输出"孙悟空",即改变后的Field没有被序列化
			System.out.println(p2.getName());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

三、 自定义序列化

通过在Field(属性)前使用transient关键字,可以指定Java序列化时无须理会该Field。

public class Person
	implements java.io.Serializable
{
	private String name;
	private transient int age;
	// 注意此处没有提供无参数的构造器!
	public Person(String name , int age)
	{
		System.out.println("有参数的构造器");
		this.name = name;
		this.age = age;
	}
	// 省略name与age的setter和getter方法

}

测试该Person对象:

public class TransientTest
{
	public static void main(String[] args)
	{
		try(
			// 创建一个ObjectOutputStream输出流
			ObjectOutputStream oos = new ObjectOutputStream(
				new FileOutputStream("transient.txt"));
			// 创建一个ObjectInputStream输入流
			ObjectInputStream ois = new ObjectInputStream(
				new FileInputStream("transient.txt")))
		{
			Person per = new Person("孙悟空", 500);
			// 系统会per对象转换字节序列并输出
			oos.writeObject(per);
			Person p = (Person)ois.readObject();
			System.out.println(p.getName());
			System.out.println(p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

输出:

有参数的构造器

孙悟空0

观察输出,获取的age值为0, 说明在序列化时,该age属性并未被序列化。

四、 Externalizable接口

该接口提供的序列化机制,完全由程序员决定存储和恢复对象数据。Externalizable接口中两个需实现的方法。

void readExternal(ObjectInput in)

The object implements the readExternal method to restore its contents by calling the methods of DataInput for primitive types and readObject for objects, strings and arrays.
void writeExternal(ObjectOutput out)

The object implements the writeExternal method to save its contents by calling the methods of DataOutput for its primitive values or calling the writeObject method of ObjectOutput for objects, strings, and arrays.

我们举个例子,看下如何使用该接口来序列化对象。

public class Person
	implements java.io.Externalizable
{
	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;
	}

	public void writeExternal(java.io.ObjectOutput out)
		throws IOException
	{
		// 将name Field的值反转后写入二进制流
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	public void readExternal(java.io.ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		// 将读取的字符串反转后赋给name Field
		this.name = ((StringBuffer)in.readObject()).reverse().toString();
		this.age = in.readInt();
	}
}

两种序列化机制对比:

对象序列化需要注意:

1.  对象的类名、Field都会被序列化; 方法、static Field、transient Field都不会被序列化。

2. 实现Serializable接口的类如果需要让某个Field不被序列化,则可以在该Field前添加transient私事符。

3. 保证序列化对象的Field类型也是可序列化的。

4. 反序列化对象时必须有序列化对象的class文件。

5. 当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取。

五、 版本

在对象进行序列化或者反序列化操作的时候,要考虑JDK版本问题。如果序列化的JDK版本和反序列化的版本不一致,则可能出现异常。

因此,可以在序列化操作中引入一个serialVersionUID的长了,通过此常量验证版本的一致性。

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 ;
	}
};
时间: 2024-10-22 17:51:05

Java 对象序列化机制详解的相关文章

Java 反射机制详解及实例代码_java

Java反射详解 本篇文章依旧采用小例子来说明,因为我始终觉的,案例驱动是最好的,要不然只看理论的话,看了也不懂,不过建议大家在看完文章之后,在回过头去看看理论,会有更好的理解. 下面开始正文. [案例1]通过一个对象获得完整的包名和类名 package Reflect; /** * 通过一个对象获得完整的包名和类名 * */ class Demo{ //other codes... } class hello{ public static void main(String[] args) {

Java反射机制详解_java

本文较为详细的分析了Java反射机制.分享给大家供大家参考,具体如下: 一.预先需要掌握的知识(java虚拟机) java虚拟机的方法区: java虚拟机有一个运行时数据区,这个数据区又被分为方法区,堆区和栈区,我们这里需要了解的主要是方法区.方法区的主要作用是存储被装载的类 的类型信息,当java虚拟机装载某个类型的时候,需要类装载器定位相应的class文件,然后将其读入到java虚拟机中,紧接着虚拟机提取class 中的类型信息,将这些信息存储到方法区中.这些信息主要包括: 1.这个类型的全

Java 配置加载机制详解及实例_java

前言 现如今几乎大多数Java应用,例如我们耳熟能详的tomcat, struts2, netty-等等数都数不过来的软件,要满足通用性,都会提供配置文件供使用者定制功能. 甚至有一些例如Netty这样的网络框架,几乎完全就是由配置驱动,这样的软件我们也通常称之为"微内核架构"的软件.你把它配置成什么,它就是什么. It is what you configure it to be. 最常见的配置文件格式是XML, Properties等等文件. 本文探讨加载配置中最通用也是最常见的场

怎样使用Java Servlet动态生成图片详解

servlet|动态|详解 在Web应用中,经常需要动态生成图片,比如实时股市行情,各种统计图等等,这种情况下,图片只能在服务器内存中动态生成并发送给用户,然后在浏览器中显示出来. 本质上,浏览器向服务器请求静态图片如JPEG时,服务器返回的仍然是标准的http响应,只不过http头的contentType不是text/html,而是image/jpeg而已,因此,我们在Servlet中只要设置好contentType,然后发送图像的数据流,浏览器就能正确解析并显示出图片. 在Java中,jav

Java对象序列化使用基础

所谓对象序列化就是将对象的状态转换成字节流,以后可以通过这些值再生成相同状态的对象.这个过程也可以通过网络实现,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新"装配".像RMI.Socket.JMS.EJB它们中的一种,彼此为什么能够传递Java对象,当然都是对象序列化机制的功劳. Java对象序列化机制一般来讲有两种用途: Java的JavaBeans: Bean的状态信息通常是在设计时配置的,Bean的状态信息必须被

iOS开发系列--通知与消息机制详解_IOS

概述 在多数移动应用中任何时候都只能有一个应用程序处于活跃状态,如果其他应用此刻发生了一些用户感兴趣的那么通过通知机制就可以告诉用户此时发生的事情.iOS中通知机制又叫消息机制,其包括两类:一类是本地通知:另一类是推送通知,也叫远程通知.两种通知在iOS中的表现一致,可以通过横幅或者弹出提醒两种形式告诉用户,并且点击通知可以会打开应用程序,但是实现原理却完全不同.今天就和大家一块去看一下如何在iOS中实现这两种机制,并且在文章后面会补充通知中心的内容避免初学者对两种概念的混淆. 本地通知 本地通

Java Serializable和Parcelable详解及实例代码_java

对 Serializable和Parcelable理解 1.首先他们两个接口都是为了实现对象的序列化,使之可以传递,所谓序列化就是将对象信息装换成可以存储的介质的过程. 2.Serializable是jdk所提供的序列化接口,该接口存在于io包下,可想用于输入输出,使用非常简单,只要让你的类实现此接口就ok了:可以使用transient关键字修饰你不想序列化的属性. 3.Parcelable是sdk所提供的序列化接口,使用较上者麻烦,实现此接口后,需要重写writeToParcel方法,将需要序

node.js中的事件处理机制详解_node.js

EventEmitter类 在Node.js的用于实现各种事件处理的event模块中,定义了一个EventEmitter类.所有可能触发事件的对象都是一个集成了EventEmitter类的子类的实例对象,在Node.js中,为EventEmitter类定义了许多方法,所有与对象的事件处理函数的绑定及解除相关的处理均依靠这些方法的调用来执行. EventEmitter类的各种方法 event:代表事件名 listener:代表事件处理函数 中括号内的参数代表该参数为可选参数 方法名与参数 描述 a

Tomcat与Java Web开发技术详解连载之二

web|详解 2.2.4 部署HTML文件 在helloapp目录下加入index.htm文件,这个文件仅仅用来显示一串带链接的字符"Welcome to HelloApp", 它链接到login.jsp文件.以下是index.htm文件的代码: <html><head><title>helloapp</title></head><body ><p><font size="7"