Java对象序列化详解

          所有分布式应用常常需要跨平台,跨网络,因此要求所有传的参数、返回值都必须实现序列化。

1. Java序列化与反序列化

         Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。

 2. 为什么需要序列化与反序列化

        我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

        当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

3. 序列化机制算法

        1)所有保存到磁盘中的对象都有一个序列化编号 
   2.) 当程序试图序列化一个对象时,程序先检查该对象是否已经被序列化过。如果从未被序列化过,系统就会将该对象转换成字节序列并输出;如果已经序列化过,将直接输出一个序列化编号。

4. 如何实现Java序列化与反序列化

1)JDK类库中序列化API
        java.io.ObjectOutputStream:表示对象输出流,它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
        java.io.ObjectInputStream:表示对象输入流,它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。
2)实现序列化的要求
        只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常。
3)实现Java对象序列化与反序列化的方法
        假定一个Student类,它的对象需要序列化,可以有如下三种方法:
方法一

         若Student类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化

         ObjectOutputStream采用默认的序列化方式,对Student对象的非transient的实例变量进行序列化。

         ObjcetInputStream采用默认的反序列化方式,对对Student对象的非transient的实例变量进行反序列化。

方法二

         若Student类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。
         ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。
         ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。

方法三

         若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
         ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。
         ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。

4)JDK类库中序列化的步骤
          步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:

          ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));

          步骤二:通过对象输出流的writeObject()方法写对象:

          out.writeObject(“Hello”); 

          out.writeObject(new Date());

5)JDK类库中反序列化的步骤
          步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:

          ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\\objectfile.obj”));

          步骤二:通过对象输出流的readObject()方法读取对象:

          String obj1 = (String)in.readObject();

           Date obj2 = (Date)in.readObject();

          说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。

           为了更好地理解Java序列化与反序列化,选择方法一编码实现。

6)示例
要被序列化的对象对应的类的代码:
public class Person implements Serializable {  

    private String name = null;  

    private Integer age = null;   

    public Person(){
        System.out.println("无参构造");
    }
    public Person(String name, Integer age) {  
        this.name = name;  
        this.age = age;  
    }  
    //getter setter方法省略...
    @Override 
    public String toString() {  
        return "[" + name + ", " + age+"]";  
    }  
} 
MySerilizable 是一个简单的序列化程序,它先将一个Person对象保存到文件person.txt中,然后再从该文件中读出被存储的Person对象,并打印该对象。
public class MySerilizable {

    public static void main(String[] args) throws Exception {  
        File file = new File("person.txt");  

        //序列化持久化对象
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));  
        Person person = new Person("Peter", 27);  
        out.writeObject(person);  
        out.close();  

        //反序列化,并得到对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));  
        Object newPerson = in.readObject(); // 没有强制转换到Person类型  
        in.close();  
        System.out.println(newPerson);  
    }  
}

输出结果:
    [Peter, 27]
结果没有打印“无参构造”,说明反序列化机制无需通过构造器来初始Java对象。 
注:  
  1.) 反序列化读取的仅仅是Java对象的数据,而不是Java类,所以在反序列化时必须提供该Java对象所属类的class文件(这里是Person.class),否则会引发ClassNotFoundException异常。  
        2).当重新读取被保存的Person对象时,并没有调用Person的任何构造器,说明反序列化机制无须通过构造器来初始化对象。
7)选择序列化
       当对某个对象进行序列化时,系统会自动将该对象的所有属性依次进行序列化,如果某个属性引用到别一个对象,则被引用的对象也会被序列化。如果被引用的对象的属性也引用了其他对象,则被引用的对象也会被序列化。 这就是递归序列化。
       有时候,我们并不希望出现递归序列化,或是某个存敏感信息(如银行密码)的属性不被序列化,我们就可通过transient关键字修饰该属性来阻止被序列化。

将上面的Person类的age属性用transient修饰:
transient private Integer age = null;  
再去执行MySerilizable的结果为:
[Peter, null] //返序列化时没有值,说明age字段未被序列化
8)writeObject()方法与readObject()方法
        使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性(此时就transient一样)。 
        如果我们想要上面的Person类里的name属性在序列化后存在文件里不让别人知道具体是什么(加密),我们就可在Person类里加如下代码: 
//自定义序列化
 private void writeObject(ObjectOutputStream out) throws IOException {          
    // out.defaultWriteObject();  // 将当前类的非静态和非瞬态字段写入此流。
    //如果不写,如果还有其他字段,则不会被序列化

    out.writeObject(new StringBuffer(name).reverse());
     //将name简单加密(反转),这样别人就知道是怎么回事,当然实际应用不可能这样加密。

     out.writeInt(age);  
 }  

//反序列化
 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
    //in.defaultReadObject();// 从此流读取当前类的非静态和非瞬态字段。
    //如果不写,其他字段就不能被反序列化

     name = ((StringBuffer)in.readObject()).reverse().toString();  //解密

     age = in.readInt();  
 }

5. 单例模式的序列化

        当我们使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。此时对使用的Person类进行修改,使其实现Singleton模式,如下所示:
public class Person implements Serializable {  

    private static class InstanceHolder {  
        private static final Person instatnce = new Person("John", 31, "男");  
    }  

    public static Person getInstance() {  
        return InstanceHolder.instatnce;  
    }  

    private String name = null;  

    private Integer age = null;  

    private Gender gender = null;  

    private Person() {  
        System.out.println("无参构造");  
    }  

    private Person(String name, Integer age, String gender) {  
        System.out.println("有参构造");  
        this.name = name;  
        this.age = age;  
        this.gender = gender;  
    }  
    ...  
}
同时要修改MySerilizable 应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:
public class MySerilizable {  

    public static void main(String[] args) throws Exception {  
        File file = new File("person.txt");  
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));  
        out.writeObject(Person.getInstance()); // 保存单例对象  
        out.close();  

        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));  
        Object newPerson = in.readObject();  
        in.close();  
        System.out.println(newPerson);  

        System.out.println(Person.getInstance() == newPerson); // 将获取的对象与Person类中的单例对象进行相等性比较  
    }  
}

打印结果:
    有参构造  
    [John, 31, 男]  
    true  //说明是同一个对象

6. 序列化的意义

        客户端访问了某个能开启会话功能的资源, web服务器就会创建一个与该客户端对应的HttpSession对象,每个HttpSession对象都要站用一定的内存空间。如果在某一时间段内访问站点的用户很多,web服务器内存中就会积累大量的HttpSession对象,消耗大量的服务器内存,即使用户已经离开或者关闭了浏览器,web服务器仍要保留与之对应的HttpSession对象,在他们超时之前,一直占用web服务器内存资源。

        web服务器通常将那些暂时不活动但未超时的HttpSession对象转移到文件系统或数据库中保存,服务器要使用他们时再将他们从文件系统或数据库中装载入内存,这种技术称为Session的持久化。
        
        将HttpSession对象保存到文件系统或数据库中,需要采用序列化的方式将HttpSession对象中的每个属性对象保存到文件系统或数据库中;将HttpSession对象从文件系统或数据库中装载如内存时,需要采用反序列化的方式,恢复HttpSession对象中的每个属性对象。所以存储在HttpSession对象中的每个属性对象必须实现Serializable接口

7. serialVersionUID

        如果serialVersionUID没有显式生成,系统就会自动生成一个。此时,如果在序列化后我们将该类作添加或减少一个字段等的操作,系统在反序列化时会重新生成一个serialVersionUID然后去和已经序列化的对象进行比较,就会报序列号版本不一致的错误。为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的生明一个serialVersionUID。 
     显式定义serialVersionUID的两种用途: 
   1) 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID; 
   2) 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。

8. 序列化对象注意事项

        对象的类名、属性都会被序列化;

        而方法、static属性(静态属性)、transient属性(即瞬态属性)都不会被序列化(这也就是第4条注意事项的原因)

        虽然加static也能让某个属性不被序列化,但static不是这么用的

        要序列化的对象的引用属性也必须是可序列化的,否则该对象不可序列化,除非以transient关键字修饰该属性使其不用序列化。

        反序列化地象时必须有序列化对象生成的class文件(很多没有被序列化的数据需要从class文件获取)

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值