如何正确的使用Java序列化技术(转)

如何正确的使用Java序列化技术(转)
摘要:本文比较全面的介绍了Java 序列化技术方方面面的知识,从序列化技术的基础谈起,
介绍了Java 序列化技术的机制和序列化技术的原理。并在随后的部分详细探讨了序列化的
高级主题-如何精确的控制序列化机制。通过阅读该文章,你可以了解如何使用Java 序列
化机制的方式和正确使用的方法,避免实际编程中对该技术的误用。并能掌握如何高效使用
该技术来完成特殊的功能。

关键字:序列化(Serialize)、反序列化(DeSerialize)、类加载(ClassLoad)、指纹技术
(fingerprint)


1 Java 序列化技术概述
Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方
把该Byte 流里的数据读出来。重新构造一个相同的对象。这种机制允许你将对象通过网络
进行传播,并可以随时把对象持久化到数据库、文件等系统里。Java的序列化机制是RMI、
EJB、JNNI等技术的技术基础。
1.1 序列化技术基础
并非所有的Java 类都可以序列化,为了使你指定的类可以实现序列化,你必须使该类
实现如下接口:
java.io.Serializable
需要注意的是,该接口什么方法也没有。实现该类只是简单的标记你的类准备支持序列
化功能。我们来看如下的代码:
/**
* 抽象基本类,完成一些基本的定义
*/
public abstract class Humanoid
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
}
public Humanoid(int noOfHeads)
{
如何正确的使用Java序列化技术 技术研究系列
if (noOfHeads > 10)
throw new Error("Be serious. More than 10 heads?!");
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
该类的一个子类如下:
/**
* Humanoid的实现类,实现了序列化接口
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
private String firstName;
private transient Thread workerThread;
private static int population;
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
synchronized (Person.class)
{
population++;
}
}
public String toString()
{
return "Person " + firstName + " " + lastName;
}
static synchronized public int getPopulation()
{
return population;
}
}
1.2 对象的序列化及反序列化
上面的类Person 类实现了Serializable 接口,因此是可以序列化的。我们如果要把一个
可以序列化的对象序列化到文件里或者数据库里,需要下面的类的支持:
java.io.ObjectOutputStream
如何正确的使用Java序列化技术 技术研究系列
下面的代码负责完成Person类的序列化操作:
/**
* Person的序列化类,通过该类把Person写入文件系统里。
*/
import java.io.*;
public class WriteInstance
{
public static void main(String [] args) throws Exception
{
if (args.length != 1)
{
System.out.println("usage: java WriteInstance file");
System.exit(-1);
}
FileOutputStream fos = new FileOutputStream(args[0]);
ObjectOutputStream oos = new ObjectOutputStream(fos);
Person p = new Person("gaoyanbing", "haiger");
oos.writeObject(p);
}
}
如果我们要序列化的类其实是不能序列化的,则对其进行序列化时会抛出下面的异常:
java.io.NotSerializableException
当我们把Person 序列化到一个文件里以后,如果需要从文件中恢复Person 这个对象,
我们需要借助如下的类:
java.io.ObjectInputStream
从文件里把Person类反序列化的代码实现如下:
/**
* Person的反序列化类,通过该类从文件系统中读出序列化的数据,并构造一个
* Person对象。
*/
import java.io.*;
public class ReadInstance
{
public static void main(String [] args) throws Exception
{
if (args.length != 1)
{
System.out.println("usage: java ReadInstance filename");
System.exit(-1);
}
FileInputStream fis = new FileInputStream(args[0]);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
如何正确的使用Java序列化技术 技术研究系列
System.out.println("read object " + o);
}
}
1.3 序列化对类的处理原则
并不是一个实现了序列化接口的类的所有字段及属性都是可以序列化的。我们分为以下
几个部分来说明:
u 如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可序列化接口。则
其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接
口,则该类的父类所有的字段属性将不会序列化。
u 如果该类的某个属性标识为static类型的,则该属性不能序列化;
u 如果该类的某个属性采用transient关键字标识,则该属性不能序列化;
需要注意的是,在我们标注一个类可以序列化的时候,其以下属性应该设置为transient
来避免序列化:
u 线程相关的属性;
u 需要访问IO、本地资源、网络资源等的属性;
u 没有实现可序列化接口的属性;(注:如果一个属性没有实现可序列化,而我们又
没有将其用transient 标识, 则在对象序列化的时候, 会抛出
java.io.NotSerializableException 异常)。
1.4 构造函数和序列化
对于父类的处理,如果父类没有实现序列化接口,则其必须有默认的构造函数(即没有
参数的构造函数)。为什么要这样规定呢?我们来看实际的例子。仍然采用上面的Humanoid
和Person 类。我们在其构造函数里分别加上输出语句:
/**
* 抽象基本类,完成一些基本的定义
*/
public abstract class Humanoid
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
System.out.println("Human's default constructor is invoked");
}
public Humanoid(int noOfHeads)
{
if (noOfHeads > 10)
throw new Error("Be serious. More than 10 heads?!");
如何正确的使用Java序列化技术 技术研究系列
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
/**
* Humanoid的实现类,实现了序列化接口
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
private String firstName;
private transient Thread workerThread;
private static int population;
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
synchronized (Person.class)
{
population++;
}
System.out.println("Person's constructor is invoked");
}
public String toString()
{
return "Person " + firstName + " " + lastName;
}
static synchronized public int getPopulation()
{
return population;
}
}
在命令行运行其序列化程序和反序列化程序的结果为:
如何正确的使用Java序列化技术 技术研究系列
可以看到,在从流中读出数据构造Person对象的时候,Person 的父类Humanoid的默认
构造函数被调用了。当然,这点完全不用担心,如果你没有给父类一个默认构造函数,则编
译的时候就会报错。
这里,我们把父类Humanoid做如下的修改:
/**
* 抽象基本类,完成一些基本的定义
*/
public class Humanoid implements java.io.Serializable
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
System.out.println("Human's default constructor is invoked");
}
public Humanoid(int noOfHeads)
{
if (noOfHeads > 10)
throw new Error("Be serious. More than 10 heads?!");
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
我们把父类标记为可以序列化, 再来看运行的结果:
如何正确的使用Java序列化技术 技术研究系列
可以看到,在反序列化的时候,如果父类也是可序列化的话,则其默认构造函数也不会
调用。这是为什么呢?
这是因为Java 对序列化的对象进行反序列化的时候,直接从流里获取其对象数据来生
成一个对象实例,而不是通过其构造函数来完成,毕竟我们的可序列化的类可能有多个构造
函数,如果我们的可序列化的类没有默认的构造函数,反序列化机制并不知道要调用哪个构
造函数才是正确的。
1.5 序列化带来的问题
我们可以看到上面的例子,在Person 类里,其字段population 很明显是想跟踪在一个
JVM里Person类有多少实例,这个字段在其构造函数里完成赋值,当我们在同一个JVM 里
序列化Person 并反序列化时,因为反序列化的时候Person 的构造函数并没有被调用,所以
这种机制并不能保证正确获取Person在一个JVM的实例个数,在后面的部分我们将要详细
探讨这个问题及给出比较好的解决方案。
2 控制序列化技术
2.1 使用readObject 和writeObject方法
由于我们对于对象的序列化是采用如下的类来实现具体的序列化过程:
java.io.ObjectOutputStream
而该类主要是通过其writeObject 方法来实现对象的序列化过程,改类同时也提供了一
种机制来实现用户自定义writeObject 的功能。方法就是在我们的需要序列化的类里实现一
如何正确的使用Java序列化技术 技术研究系列
个writeObject方法,这个方法在ObjectOutputStream序列化该对象的时候就会自动的回调它。
从而完成我们自定义的序列化功能。
同样的,反序列化的类也实现了同样的回调机制,我们通过扩展其readObject来实现自
定义的反序列化机制。
通过这种灵活的回调机制就解决了上面提出的序列化带来的问题,针对上面的Person
的问题,我们编写如下的readObject方法就可以彻底避免population计数不准确的问题:
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException
{
ois.defaultReadObject();
synchronized (Person.class)
{
population++;
}
System.out.println("Adjusting population in readObject");
}
2.2 序列化过程的类版本控制
本节讨论以下问题:
u 在对象反序列化过程中如何寻找对象的类;
u 如果序列化和反序列化两边的类不是同一个版本,如何控制;
2.2.1 序列化类的寻找机制
在对象的反序列化过程中,是一定需要被反序列化的类能被ClassLoader 找到的,否则
在反序列化过程中就会抛出java.lang.ClassNotFoundException 异常。关于ClassLoader 如何
寻找类,这里就不多说了,可以参考我的另一篇讨论ClassLoader 的文章《在非管理环境下
如何实现热部署》。我们这里只是关心该序列化对象对应的类是被哪个ClassLoader 给Load
的。为此,我们修改上面的
/**
* 修改后的反序列化类
*/
import java.io.*;
public class ReadInstance
{
public void readPerson(String filename)
{
如何正确的使用Java序列化技术 技术研究系列
try{
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
System.out.println("read object " + o);
System.out.println(this.getClass().getClassLoader());
Person person = (Person)o;
System.out.println(person.getClass().getClassLoader());
}catch(java.io.IOException ie)
{
ie 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值