Java基础学习IO流之序列化的总结与归纳

 一、基本概念

           Java中序列化(serialize)是指把把一个Java对象写入IO流并保存在磁盘中(转换为字节序列),相应的反序列化是指从磁盘中恢复被序列化的Java对象(由字节序列恢复Java对象)。

二、对象序列化的作用

        通过Java序列化的基本概念我们不难得出,序列化就是为了保存JVM中的各种对象的状态,当我们需要再次使用这些对象数据时,只需从字节序列中重新读出对象即可。你或许会问为什么要用Java对象的序列化呢?我自己粘贴,复制保存对象的状态不也可以吗?原理上是肯定可行的,但既然Java已经为我们提供了这么强大的序列化机制,我们为什么不直接使用呢?并且我们可以很轻松的自定义序列化(下面会介绍)方式来序列化和反序列化我们的对象状态,何乐而不为呢?

        一般来说,以下各种情况需要我们使用Java序列化对象。

(1)当我们需要把对象保存在一个文件或数据库中的时候

        这种情况最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,   内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。再比如如果我们需要记录当前用户的登录状态数据,我们就可以把用户对象的状态序列化保存在磁盘上,当下次用户再进行登录时我们可以恢复上次登录时系统运行时的状态,极大地方便了用户。

(2)当我们使用Socket在网络之间传输对象的时候

       这一点也是很好理解的,当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送,接收方则需要把字节序列再恢复为Java对象。

(3)当我们想通过RMI传输对象的时候

       网上很多序列化介绍中都“提到“这一点,但是却很少有人来解释什么是RMI以及序列化对RMI的意义。

       RMI即远程方法调用,这种框架是通过实现Remote接口来实现的,Java帮助文档给出Remote接口的解释如下:

       The Remote interface serves to identify interfaces whose methods may be invoked from a non-local virtual machine. Any object that is a remote object must directly or indirectly implement this interface. Only those methods specified in a "remote interface", an interface that extends java.rmi.Remote are available remotely.

Implementation classes can implement any number of remote interfaces and can extend other remote implementation classes. RMI provides some convenience classes that remote object implementations can extend which facilitate remote object creation.

       当一个远程对象直接或间接的实现Remote接口后,这个远程对象才可以被非本地JVM调用。在RMI分布式应用系统中,服务器与客户机之间传递的Java对象必须是可序列化的对象。不可序列化的对象不能在对象流中进行传递。为了通过Java的序列化机制来进行传输,远程接口中的方法的参数和返回值,要么是Java的基本类型,要么是远程对象,要么是实现了 Serializable接口的Java类。当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。(关于RMI的详细信息科参见http://haolloyin.blog.51cto.com/1177454/332426)

三、序列化的实现

                    
Java中对象的序列化是通过实现Serializable接口或Externalizable接口两者之中的一个来实现的,下面将一一进行介绍。

     (1)实现Serializable接口

                    当一个类实现Serializable接口后,通过处理流ObjectOutputStream对象的writeObject()方法可以输出序列化对象。

             先定义一个待序列化的Person类:

<span style="font-size:14px;">   
      class Person implements Serializable
      {
	   private String name;
	   private short age;
	   public Person(){}
	   public Person(String name,short age)
           {
		   this.name = name;
		   this.age = age;
	   }
           public void setName(String name)
           {
               this.name = name;
           }
           public String getName()
           {
               return name;
           }
           public void setAge()
           {
               this.age = age;
           }
           public short getAge()
           {
               return age;
           }
      }
</span>
 

             下面写出一个WriteObjectTest测试类:

<span style="font-size:14px;">  
      import java.io.*;
      public class WriteObjectTest
      {
	
	      public static void main(String[] args)
              {
		    try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")))
                     {
		  	    Person per = new Person("刘德华",(short)52);
		  	    oos.writeObject(per);
		     }
                    catch(IOException ioe)
                     {
		  	ioe.printStackTrace();
		     }
	      }
       }
</span>
              下面写出一个ReadObjectTest测试类:

<span style="font-size:14px;">     
      import java.io.*;
      public class ReadObjectTest
      {
	
	    public static void main(String[] args) throws Exception
            {
	  	 try(ObjectInputStream ois = new ObjectInputStream(newFileInputStream("person.txt")))
                 {
	  	      Person per = (Person)ois.readObject();
	  	      System.out.println("姓名:"+per.getName()+"\r\n"+"年龄:"+pergetAge());
	  	 }
                 catch(Exception ioe)
                 {
	  		ioe.printStackTrace();
	  	 }
	    }
	  
       }
</span>

         通过上述程序我们可以很容易的通过ObjectOutputStream对象的writeObject()和readObject()方法实现对象的序列化和反序列化。

        上面的person类的属性只是String和short,如果某个类的属性不是基本类型或String类型而是另外一个类的引用类型,那么首先该引用类必须是可序列化的,如果该引用类不可序列化,则引用了该引用类的类也是不可序列化。如:

<span style="font-family:SimSun;font-size:14px;">    
          class Teacher  implements Serializable{ 
             private String name;
            private Person per;
             //此处略去构造方法以及属性的getter、setter方法
       }
</span>

 

         如果Person类不可以序列化,无论Teacher是否实现Serializable接口,Teacher类都是不可序列化的。当有多个类引用相同的一个被引用类对象时,Java的序列化机制究竟会怎样做呢?看下面的例子:

      Person per = new Person("学生",20);
      Teacher t1 = new Teacher("老师1",per);
      Teacher t2 = new Teacher("老师2",per);
      oos.writeObject(t1);
      oos.writeObject(t2);
      oos.writeObject(per);     

 

         假设序列化t1时,per序列化一次,则序列化t2时,per也要序列化一次,而per又单独自己序列化一次,所以per对象序列化了三次,那么反序列化时就会残生三个堆内存中per对象,这显然是不合理的。在《疯狂JAVA讲义》一书中给出了Java序列化的算法,其内容是:

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

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

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

        根据上面的序列化算法我们得知,当第二次、第三次序列化per对象时,程序不会再次将per对象转换成字节序列并输出,而是仅仅输出一个序列化编号。当多次调用writeObject()方法输出同一个对象时,只有第一次调用writeObject()方法时才会把对象转换成字节序列输出。那么这样有会产生一个问题,假如我们在第一次调用writeObject()方法之后修改了对象的状态,然后我们再次调用writeObject()方法来再次序列化对象,那到底保存的是修改后的对象还是没有修改后的对象呢?答案显而易见是保存的依旧是未修改的对象,严格来说保存的应该是未修改对象的编号,也就是说,当修改一个对象状态之后再次序列化是不会保存修改的内容的。

     (2)实现Externalizable接口

        当一个类实现Externalizable接口时,还必须实现readExternal()和writeExternal()这两个方法来自定义对象序列和反序列化的方式,也就是说该接口强制用户进行自定义序列化的方式。

        Java帮助文档对readExternal()方法的解释如下:

        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. The readExternal method must read the values in the same sequence and with the same types as were written by writeExternal.

        对writeExternal()方法的解释如下:

       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.

下面person类为实现了Externalizable接口:

import java.io.*;
public class Person
{
	private String name;
	private int age;
	public Person(String name , int age)
	{
		this.name = name;
		this.age = age;
	}
	// 省略name与age的setter和getter方法
	public void writeExternal(java.io.ObjectOutput out)throws IOException
	{
		// 将name的值写入二进制流
		out.writeObject(new StringBuffer(name)));
		out.writeInt(age);
	}
	public void readExternal(java.io.ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		// 将读取的字符串赋给name
		this.name = ((StringBuffer)in.readObject()).toString();
		this.age = in.readInt();
	}
}
实现Externalizable接口的类的序列化和反序列化的方式与实现Serializable接口的类相同,故此处不再反复论述。

四、注意事项

(1) 如果在序列化一个对象时不希望某个属性值被序列化,则定义类时需要在该属性前面使用transient关键字修   饰。使用该关键字修饰的属性在反序列化时会被赋予默认的初始值,比如int型默认为0,String型默认为null

(2)反序列化对象时必须有序列化对象的class文件

(3)当一个文件中序列化了多个对象时,则反序列化时读取的顺序必须与序列化时存入的顺序一致

(4)自定义序列化和反序列化时,序列化的方式与反序列化的方式应该”保持一致“,否则反序列化会产生错误      

五、版本

       当Java的class文件升级之后,出于安全性的考虑,程序会抛出异常拒绝反序列化。因此我们还需要为序列化的类指定一个版本信息。Java序列化机制允许为序列化类提供一个private static final 的serialVersionUID 属性用于标识序列化的版本。如果一个类升级后,但只要它的serialVersionUID属性值保持不变,依然可以进行反序列化。只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法或者属性可以用。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值