`

【总结】你所不知道的Java序列化

阅读更多

我们都知道,Java序列化可以让我们记录下运行时的对象状态(对象实例域的值),也就是我们经常说的对象持久化 。这个过程其实是非常复杂的,这里我们就好好理解一下Java的对象序列化。

 

1、 首先我们要搞清楚,Java对象序列化是将 对象的实例域数据( 包括private私有域) 进行持久化存储。而并非是将整个对象所属的类信息进行存储。 其实了解JVM的话,我们就能明白这一点了。实际上堆中所存储的对象包含了实例域数据值以及指向类信息的地址,而对象所属的类信息却存放在方法区中。当我们要对持久层数据反序列化成对象的时候,也就只需要将实例域数据值存放在新创建的对象中即可。

 

2、 我们都知道凡要序列化的类都必须实现Serializable接口。 但是不是所有类都可以序列化呢?当然不是这样,想想看序列化可以让我们轻而易举的接触到对象的私有数据域,这是多么危险的漏洞呀!总结一下,JDK中有四种类型的类对象是绝对不能序列化的

     (1) 太依赖于底层实现的类(too closely tied to native code)。比如java.util.zip.Deflater。 

     (2) 对象的状态依赖于虚拟机内部和不停变化的运行时环境。比如java.lang.Thread, java.io.InputStream
     (3) 涉及到潜在的安全性问题。比如:java.lang.SecurityManager, java.security.MessageDigest
     (4) 全是静态域的类,没有对象实例数据。要知道静态域本身也是存储在方法区中的。

 

3、 自定义的类只要实现了Serializable接口,是不是都可以序列化呢? 当然也不是这样,看看下面的例子:

class Employee implements Serializable{
         private ZipFile zf=null;
         Employee(ZipFile zf){
                this.zf=zf;
         } 
}

ObjectOutputStream oout=
new ObjectOutputStream(new FileInputStream(new File("aaa.txt")));
oout.writeObject(new Employee(new ZipFile("c://.."));

     我们会发现运行之后抛出java.io.NotSerializableException : java.util.zip.ZipFile 。很明显,如果要对Employee对象序列化,就必须对其数据域ZipFile对象也进行序列化,而这个类在JDK中是不可序列化的。因此,包含了不可序列化的对象域的对象也是不能序列化的。 实际上,这也并非不可能,我们在下面第6点会谈到。

 

4、 可序列化的类成功序列化之后,是不是一定可以反序列化呢? (这里默认在同一环境下,而且类定义永远不会改变,即满足兼容性。在下面我们会讨论序列化的不兼容性)。答案是不一定哦!我们还是看一个列子:

//父类对象不能序列化
class Employee{ 
	private String name;
	Employee(String n){
		this.name=n;
	}
	public String getName(){
		return this.name;
	}
}
//子类对象可以序列化
class Manager extends Employee implements Serializable{
	private int id;
	Manager(String name, int id){
		super(name);
		this.id=id;
	}
}
//序列化与反序列化测试
public static void main(String[] args) throws IOException, ClassNotFoundException{
         File file=new File("E:/aaa.txt");
	ObjectOutputStream oout=new ObjectOutputStream(new FileOutputStream(file));
	oout.writeObject(new Manager("amao",123));
	oout.close();
	System.out.println("序列化成功");
		
	ObjectInputStream oin=new ObjectInputStream(new FileInputStream(file));
	Object o=oin.readObject();
	oin.close();
	System.out.println("反序列化成功:"+((Manager) o).getName());
}

        程序的运行结果是:打印出“序列化成功”之后抛出java.io.InvalidClassException: Manager; Manager; no valid constructor。 为什么会出现这种情况呢?很显然,序列化的时候只是将Manager类对象的数据域id写入了文件,但在反序列化的过程中,需要在堆中建立一个Manager新对象。我们都知道任何一个类对象的建立都首先需要调用父类的构造器对父类进行初始化,很可惜序列化文件中并没有父类Employee的name数据,那么此时调用Employee(String)构造器会因为没有数据而出现异常。既然没有数据,那么可不可以调用无参构造器呢? 事实却是如此,如果有Employee()无参构造器的存在,将不会抛出异常,只是在执行打印的时候出现--- “反序列化成功:null”。

       总结一下:如果当前类的所有超类中有一个类即不能序列化,也没有无参构造器。那么当前类将不能反序列化。如果有无参构造器,那么此超类反序列化的数据域将会是null或者0,false等等。

 

5、 序列化的兼容性问题!

     类定义很有可能在不停的人为更新(比如JDK1.1到JDK1.2中HashTable的改变)。那么以前序列化的旧类对象很可能不能再反序列化成为新类对象。这就是序列化的兼容性问题,严格意义上来说改变类中除statictransient以外的所有部分都会造成兼容性问题。而JDK采用了一种stream unique identifier (SUID) 来识别兼容性。SUID是通过复杂的函数来计算的类名,接口名,方法和数据域的 一个64位 hash值。而这个值存储在类中的静态域内:

                               private static final long serialVersionUID = 3487495895819393L

只要稍微改动类的定义,这个类的SUID就会发生变化,我们通过下面的程序来看看:

//修改前的Employee
class Employee implements Serializable{
	private String name;
	Employee(String n){
		this.name=n;
	}
	public String getName(){
		return this.name;
	}
}
//测试,打印SUID=5135178525467874279L
long serialVersionUID=ObjectStreamClass.lookup(Class.forName("Employee")).getSerialVersionUID();
System.out.println(serialVersionUID);

//修改后的Employee
class Employee implements Serializable{
	private String name1; //注意,这里略微改动一下数据域的名字
	Employee(String n){
		this.name1=n;
	}
	public String getName(){
		return this.name1;
	}
}
//测试,打印SUID=-2226350316230217613L
long serialVersionUID=ObjectStreamClass.lookup(Class.forName("Employee")).getSerialVersionUID();
System.out.println(serialVersionUID);

      两次测试的SUID都不一样,不过你可以试试如果name域是static或transient声明的,那么改变这个域名是不会影响SUID的。

     很显然,JVM正是通过检测新旧类SUID的不同,来检测出序列化对象与反序列化对象的不兼容。抛出 java.io.InvalidClassException: Employee; local class incompatible:

     很多时候,类定义的改变势在必行,但又不希望出现序列化的不兼容性。我们就可以通过在类中显示的定义serialVersionUID,并赋予一个明确的long值即可。这样会逃过JVM的默认兼容性检查。但是如果数据域名的改变会导致反序列化后,改变的数据域只能得到默认的null或者0或者false值。

 

6、 在上面第3点中谈到了一个不能成功序列化的Employee的列子,原因就是包含了一个不能序列化的ZipFile对象引用的数据域。但有时我们非常想将ZipFile所对应的本地文件路径进行序列化,是不是真的没有办法了呢? 这里我们就将一个非常有用的应用。

      当我们需要用writeObject(Object)方法对某个类对象序列化的时候,会首先对这个类对象的所有超类按照继承层次从高到低来写出每个超类的数据域。谁能保证每个超类都实现了Serializable接口呢? 其实,对于这些不能序列化的类,JVM会检查这些类是否有这样一个方法:

                  private void writeObject(ObjectOutputStream out)throws IOException
      如果有,JVM会调用这个方法仍然对该类的数据域进行序列化。我们来看看JDK的ObjectOutputStream类中对这一部分的实现(我这里只列出了源码中的执行过程):

//下面的方法从上到下进行调用
writeObject(Object); 

//ObjectOutputStream的writeObject方法
public final void writeObject(Object obj) throws IOException { 
        writeObject0(obj, false);
}

//ObjectOutputStream, 底层写入Object的实现
private void writeObject0(Object obj, boolean unshared) {
       if (obj instanceof Serializable) {
		writeOrdinaryObject(obj, desc, unshared);
}

//ObjectOutputStream
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc,  boolean unshared) {
       writeSerialData(obj, desc);
}

//ObjectOutputStream, 对超类到子类的每个可序列化的类,写出数据域
 private void writeSerialData(Object obj, ObjectStreamClass desc)  throws IOException{
         //如果类中有writeObject(ObjectOutputStream)方法,则通过底层进行调用
         if (slotDesc.hasWriteObjectMethod()) { 
                slotDesc.invokeWriteObject(obj, this);
         }//如果没有此方法,则采用默认的写类数据域的方法。
         else {//这个方法会对可序列化的对象中的数据域进行写出,但是如果这个数据域是不可序列化而且没有writeObject(ObjectOutputStream)方法的类对象,那么将抛出异常。
		defaultWriteFields(obj, slotDesc);
	 }
}

         ObjectOutputStream中的writeSerialData()方法说明了JVM检查writeObject(ObjectOutputStream out) 这个私有方法的潜在执行机制。这就是说,我们可以通过构造这个方法,使得原本不能序列化的类的部分数据域可以序列化。下面我们就开始对ZipFile进行可序列化的改造吧!

//自定义的一个可序列化的ZipFile,当然这个类不能继承JDK中的ZipFile,否则序列化将不可能完成。
class SerializableZipFile implements Serializable{
	public ZipFile zf;
	//包含一个ZipFile对象
	SerializableZipFile(String filename) throws IOException{
		zf=new ZipFile(filename);
	}
	//对ZipFile中的文件名进行序列化,因为它是String类型的
	private void writeObject(ObjectOutputStream out)throws IOException{
		out.writeObject(zf.getName());
	}
	//对应的,反序列化过程中JVM也会检查类似的一个私有方法。
	private void readObject(ObjectInputStream in)throws IOException,ClassNotFoundException{
		String filename=(String)in.readObject();
		zf=new ZipFile(filename);
	}
}
//测试
public static void main(String[] args) throws IOException, ClassNotFoundException{
	//序列化
        File file=new File("E:/aaa.txt");
	ObjectOutputStream oout=new ObjectOutputStream(new FileOutputStream(file));
	oout.writeObject(new SerializableZipFile("e:/aaa.zip"));
	oout.close();
	System.out.println("序列化成功");
	//反序列化
	ObjectInputStream oin=new ObjectInputStream(new FileInputStream(file));
	Object o=oin.readObject();
	oin.close();
	System.out.println("反序列化成功:"+((SerializableZipFile) o).zf.getName());
}
//序列化成功
//反序列化成功:e:\aaa.zip

      太棒了,我们构造了一个可序列化的ZipFile类。这真是一件伟大的事情。

 

 

 

4
1
分享到:
评论
1 楼 express_wind 2012-08-23  
写得相当深入。

相关推荐

    java反序列化工具

    Java反序列化是一种将已序列化的对象状态转换回对象的过程,它是Java平台中持久化数据的一种常见方式。在Java应用程序中,序列化用于保存对象的状态以便稍后恢复,或者在网络间传输对象。然而,这个过程也可能引入...

    java序列化(Serializable)的作用和反序列化.doc

    ### Java序列化(Serializable)的作用与反序列化详解 #### 一、序列化的概念 序列化是指将程序中的对象转换为一系列字节流的过程,主要用于保存对象的状态或在网络之间传输对象。序列化的主要目的是为了能够持久化...

    java序列化全解

    Java序列化是Java平台中的一种核心机制,它允许对象的状态被转换成字节流,以便存储到磁盘、数据库,或者在网络中进行传输。这对于实现持久化、远程方法调用(RMI)以及Enterprise JavaBeans(EJB)等高级功能至关...

    java 序列化时排除指定属性

    Java序列化是Java平台提供的一种持久化机制,它允许我们将一个Java对象转换为字节流,以便存储到磁盘上,或者通过网络进行传输。这使得我们可以保存和恢复对象的状态。实现序列化的类需要实现`Serializable`接口,...

    java 对象的序列化与反序列化

    1. **序列化标识符(SerialVersionUID)**:Java允许你为每个可序列化的类定义一个唯一的`serialVersionUID`,默认是由JVM根据类的结构计算出来的。如果类的版本更新导致结构变化,而此值未做更新,反序列化时会抛出`...

    java 序列化代码示例

    总结,Java序列化是一个强大的工具,但使用时需注意其潜在的风险和性能影响。理解序列化的工作原理,并合理运用,可以在许多场景下提高软件的灵活性和可维护性。在实际项目中,结合淘宝等大型系统的具体需求,序列化...

    C#和Java的序列化反序列化

    总结来说,C#和Java的序列化和反序列化机制都是各自语言中不可或缺的一部分,它们使得数据能够在不同环境之间自由流动。理解和掌握这些技术对于任何软件开发者来说都是非常重要的,特别是涉及到数据持久化、网络通信...

    java序列化(Serializable)的作用和反序列化

    ### Java序列化(Serializable)的作用与反序列化详解 #### 一、序列化是什么? 序列化是指将程序中的对象转换为字节流的过程,从而方便存储或传输这些对象。通常,序列化用于将对象的状态(即其实例变量的值,而非...

    java序列化原理与算法

    ### Java序列化原理与算法详解 #### 序言 在现代软件开发中,尤其是在网络通信和数据持久化领域,对象的序列化与反序列化扮演着至关重要的角色。Java作为一种广泛应用的编程语言,提供了强大的内置支持来实现序列化...

    java serializable 序列化与反序列化

    **一、Java序列化** 1. **什么是序列化**:序列化是将对象的状态(属性和成员变量)转换为可以存储或传输的数据格式的过程。在Java中,通常是将对象转换为字节数组,以便写入磁盘或通过网络发送。 2. **为什么需要...

    java自动序列化

    Java序列化是将对象转换为字节流的过程,目的是为了保存对象的状态以便稍后恢复或传输到其他地方。通过实现`Serializable`接口,一个Java对象就可以被序列化。这个接口是一个标记接口,没有定义任何方法,仅表示对象...

    java对象序列化.ppt

    总结来说,Java对象序列化是将对象状态转换为字节流,便于存储和网络传输的关键技术。通过实现`Serializable`或`Externalizable`接口,我们可以控制对象如何被序列化和反序列化,同时`transient`关键字提供了保护...

    Java对象序列化的秘密

    总结,Java对象序列化是一种强大但需要谨慎使用的工具。了解其原理和注意事项,可以帮助开发者充分利用这一特性,同时避免潜在的问题。在实际应用中,要权衡序列化的优点和可能带来的安全、性能挑战,选择最适合项目...

    Java Json序列化与反序列化

    总结,Java中的JSON序列化与反序列化是数据交互的重要环节,Jackson和Gson是两个常用的库,它们提供了丰富的功能和良好的API设计,使得处理JSON数据变得简单高效。通过理解和掌握这些知识,开发者可以更好地在Java...

    通过实例深入了解java序列化

    通过实例深入了解 Java 序列化 Java 序列化是 Java 系列技术中一个较为重要的技术点,用于将 Java 对象序列化为二进制文件。开发人员只需要了解被序列化的类需要实现 Serializable 接口,使用 ObjectInputStream 和...

    java序列化与反序列化

    Java序列化是将Java对象转换为字节流的过程,以便可以在磁盘、数据库或网络上存储或传输这些对象。这使得我们能够保存对象的状态,并在稍后的时间点恢复它,或者在网络之间传递对象。反序列化是相反的过程,即从字节...

    克隆和序列化(Java )

    在Java编程语言中,克隆和序列化是两个...总结来说,Java中的克隆和序列化是两个强大的工具,它们在数据持久化、对象复制以及网络通信等方面有着广泛的应用。理解和熟练掌握这两个概念,对于Java开发者来说至关重要。

    Java序列化的机制和原理

    ### Java序列化的机制和原理 #### 一、序列化与反序列...总结来说,Java序列化机制提供了对象序列化和反序列化的能力,使得对象可以跨网络传输或存储于磁盘上。理解序列化的原理对于开发基于网络的应用程序尤其重要。

    Netty中的java序列化

    总结起来,Netty中的Java序列化是通过实现`Serializable`接口的对象转换为字节流,然后使用`ByteBuf`在网络中传输的一种方式。在实际开发中,我们可以根据性能需求选择合适的序列化策略,并结合Netty的强大功能,...

    Java中Tree的序列化

    3. **非序列化成员**:有时我们不希望某些成员被序列化,可以使用`transient`关键字标记这些字段。 4. **序列化代理**:对于复杂的数据结构,可能需要创建一个序列化代理类来处理序列化和反序列化的细节。 ### 源码...

Global site tag (gtag.js) - Google Analytics