`
ahentent
  • 浏览: 2622 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

[转]谈谈J2SE中的序列化之接受默认序列化

    博客分类:
  • Java
阅读更多

 

首先要了解java默认的序列化行为,java将一切关于对象的信息都保存了下了,也就是说,有些时候那些不需要保存的也被保存了下来。一般情况下,我们仅仅需要保存逻辑数据就可以了。不需要保存的数据我们可以用关键字transient标出。

  
以下是一个例子:


import java.io.*;

public class Serial implements Serializable {
 int company_id;
 String company_addr;

 transient boolean company_flag;
}


  
则company_flag字段将不会参与序列化与反序列化,但同时你也增加了为他初始值的责任。这也是序列化常常导致的问题 之一。因为序列化相当于一个只接受数据流的public构造函数,这种对象构造方法是语言之外的。但他仍然是一种形式上的构造函数。如若你的类不能够通过 其他方面来保证初始化,则你需要额外的提供readObject方法,首先正常的反序列化,然后对transient标示的字段进行初始化。

   在不适合的时候,使用java默认的序列化行为可能会带来速度上的影响,最糟糕的情况是,可能导致溢出。在某些数据结构的实现中,经常会充斥着各种的循 环引用,而java的默认序列化行为,并不了解你的对象结构,其结果就是java试图通过一种昂贵的“图遍历”来保存对象状态。可想而知,不但慢而且可能 溢出。这时候你就要提供自己的readObject,来代替默认的行为。

  
兼容性问题

  兼容性历来是复杂而麻烦的问题。

  不要兼容性:

  首先来看看如果我们的目的是不要兼容性,应该注意哪些。不要兼容性的场合很多,比如war3每当版本升级就不能够读取以前的replays。

   兼容也就是版本控制,java通过一个名为UID(stream unique identifier)来控制,这个UID是隐式的,它通过类名,方法名等诸多因素经过计算而得,理论上是一一映射的关系,也就是唯一的。如果UID不一 样的话,就无法实现反序列化了,并且将会得到InvalidClassException。

  当我们要人为的产生一个新的版本(实现并没有改动),而抛弃以前的版本的话,可以通过显式的声名UID来实现:


private static final long serialVersionUID=????;


  
你可以编造一个版本号,但注意不要重复。这样在反序列化的时候老版本将得到InvalidClassException,我们可以在老版本的地方捕捉这个异常,并提示用户升级的新的版本。

  当改动不大时,保持兼容性(向下兼容性的一个特例):

  
有时候你的类增加了一些无关紧要的非私有方法,而逻辑字段并不改变的时候,你当然希望老版本和新版本保持兼容性,方法同样是通过显式的声名UID来实现。下面我们验证一下。

  
老版本:


import java.io.*;

public class Serial implements Serializable {

 int company_id;
 String company_addr;

 public Serial1(int company_id, String company_addr) {
  this.company_id = company_id;
  this.company_addr = company_addr;
 }

 public String toString() {
  return “DATA: “+company_id+” “+
  company_addr;
 }
}


  新版本


import java.io.*;

public class Serial implements Serializable {

 int company_id;
 String company_addr;
 public Serial1(int company_id, String company_addr) {
  this.company_id = company_id;
  this.company_addr = company_addr;
 }

 public String toString() {
  return “DATA: “+company_id+” “+ company_addr;
 }
 public void todo(){}//无关紧要的方法
}


  
首先将老版本序列化,然后用新版本读出,发生错误:


java.io.InvalidClassException: Serial.Serial1; local class incompatible: stream classdesc serialVersionUID = 762508508425139227, local class serialVersionUID = 1187169935661445676


  
接下来我们加入显式的声名UID:


private static final long serialVersionUID=762508508425139227l;


  
再次运行,顺利地产生新对象


  如何保持向上兼容性:

  向上兼容性是指老的版本能够读取新的版本序列化的数据流。常常出现在我们的服务器的数据更新了,仍然希望老的客户端能够支持反序列化新的数据流,直到其更新到新的版本。可以说,这是半自动的事情。

   跟一般的讲,因为在java中serialVersionUID是唯一控制着能否反序列化成功的标志,只要这个值不一样,就无法反序列化成功。但只要这 个值相同,无论如何都将反序列化,在这个过程中,对于向上兼容性,新数据流中的多余的内容将会被忽略;对于向下兼容性而言,旧的数据流中所包含的所有内容 都将会被恢复,新版本的类中没有涉及到的部分将保持默认值。利用这一特性,可以说,只要我们认为的保持serialVersionUID不变,向上兼容性 是自动实现的。

  当然,一但我们将新版本中的老的内容拿掉,情况就不同了,即使UID保持不变,会引发异常。正是因为这一点,我们要牢记一个类一旦实现了序列化又要保持向上下兼容性,就不可以随随便便的修改了!!!

  测试也证明了这一点,有兴趣的读者可以自己试一试。

  如何保持向下兼容性:

  一如上文所指出的,你会想当然的认为只要保持serialVersionUID不变,向下兼容性是自动实现的。但实际上,向下兼容要复杂一些。这是因为,我们必须要对那些没有初始化的字段负责。要保证它们能被使用。

  所以必须要利用


private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException{
 in.defaultReadObject();//先反序列化对象
 if(ver=5552){
  //以前的版本5552
  …初始化其他字段
 }else if(ver=5550){
  //以前的版本5550
  …初始化其他字段
 }else{
  //太老的版本不支持
  throw new InvalidClassException();
 }
}


  
细心的读者会注意到要保证in.defaultReadObject();能够顺利执行,就必须要求 serialVersionUID保持一致,所以这里的ver不能够利用serialVersionUID了。这里的ver是一个我们预先安插好的 final long ver=xxxx;并且它不能够被transient修饰。所以保持向下的兼容性至少有三点要求:

  1.serialVersionUID保持一致

  2.预先安插好我们自己的版本识别标志的final long ver=xxxx;

  3.保证初始化所有的域

  讨论一下兼容性策略:

   到这里我们可以看到要保持向下的兼容性很麻烦。而且随着版本数目的增加。维护会变得困难而繁琐。讨论什么样的程序应该使用怎么样的兼容性序列化策略已经 超出本文的范畴,但是对于一个游戏的存盘功能,和对于一个字处理软件的文档的兼容性的要求肯定不同。对于rpg游戏的存盘功能,一般要求能够保持向下兼 容,这里如果使用java序列化的方法,则可根据以上分析的三点进行准备。对于这样的情况使用对象序列化方法还是可以应付的。对于一个字处理软件的文档的 兼容性要求颇高,一般情况下的策略都是要求良好的向下兼容性,和尽可能的向上兼容性。则一般不会使用对象序列化技术,一个精心设计的文档结构,更能解决问 题。

  数据一致性问题、约束问题

  要知道序列化是另一种形式上的“public构造函数”,但他仅仅构造起对象,而不作任何的检查,这样人很不舒服,所以必要的检查是必须的,这利用了readObject()


private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException{
 in.defaultReadObject();//先反序列化对象
 …进行检查与初始化
}


  出于结构化的考虑,通常使用一个名为initialize的函数,负责检查与初始化,如果失败抛出异常。要保持检查与初始化是 很容易被忘记的,这常常导致问题。另一个问题在于当父类没有加入readObject()的时候,子类很容易忘记要调用对应的initialize函数。 这仿佛回到了当初为什么要引入构造函数的问题,原因就是防止子类忘记调用初始化函数引发各种问题。所以,如果要保持数据一致性,一定要加入 readObject()。

  安全问题

  安全性的话题超出了本文的范畴,但是你应该要知道,有可 能一个攻击者会对你的类准备一个恶意的数据流企图生成一个错误的类。当你需要确保你的对象数据安全的话,你一般可以利用上面的方法来检查,并初始化,但对 于某些引用不好检查。解决方法就是对重要的部件进行保护性拷贝。这里推荐一个好方法,它不用保护性拷贝个别的域,而是直接保护性拷贝整个对象。这就是:


Object readResolve() throws ObjectStreamException;


  
这个方法的用途就是,他会紧接着readObject()调用。它将会利用返回的对象代替原来反序列化的对象。也就是原来readObject()反序列化的对象将会被立即的丢弃。


Object readResolve() throws ObjectStreamException{
 return new Serial2(this.xxx1,this.xxx2);// xxx1、xxx2是刚刚反序列化得来的,这是一种保护性拷贝
}


  
这样的话虽然在时间上有所浪费,但是对于特别的重要而安全的类,可以使用这种方法。如果数据一致性问题、约束问题通过逐一检查 来解决很麻烦,也可以利用这种方法,但要考虑好成本,和注意下面的局限性。 利用readResolve()有一个明显的缺点,就是当父类实现了readResolve(),子类将变得无丛下手。如果一个保护的或者是公有的父类的 readResolve()存在,并且子类也没有改写它,将会使得子类反序列化的时候最终得到一个父类的对象,这既不是我们要得结果,也不容易发现这种错 误。而让子类重写readResolve()无疑是一个负担。也就是说对于要继承的类而言,实现readResolve()来保护类不是一个好方法。我们 只能利用第一种方法写一个保护性的readObject()。

  所以我的建议是:一般情况下,只有对于final的类采用readResolve()来进行保护。

分享到:
评论

相关推荐

    2022年J2SE中的序列化的认识Java教程.docx

    在J2SE中,序列化主要通过实现`java.io.Serializable`接口来实现。这个接口非常特殊,因为它没有任何需要实现的方法,只需被标记在类上就表明该类的对象可以被序列化。 序列化的主要用途包括持久化数据、网络传输...

    J2SE中的序列化之继承

    在Java的J2SE平台中,序列化是一个重要的特性,它允许对象的状态被转换成字节流,以便存储或在网络中传输。当一个类实现`Serializable`接口时,该类的对象就能够被序列化。这个机制使得Java对象可以跨越不同的网络...

    J2SE 中文帮助文档

    5. **输入/输出(I/O)**:包括文件操作、流的使用,以及序列化和反序列化。 6. **多线程**:理解线程的概念,创建和管理线程,以及同步和互斥机制。 7. **网络编程**:使用Socket进行客户端和服务器端通信,理解...

    J2SE中的序默认序列化

    要保存的也被保存了下来。一般情况下,我们仅仅需要保存逻辑数据就可以了。不需要保存的数据我们可以用关键字transient标出。 以下是一个例子: import java.io.*;...因为序列化相当于一个只接受数

    J2SE中的序列化的认识

    java中处处体现着简单的程序设计风格,序列化作为最常用的功能之一,在java中的设计尤为“简单”。在ObjectInputStream 和ObjectOutputStream的帮助下,我们可以轻松的实现序列化。 只要我们的class 实现了java.io....

    J2SE基础知识大全

    - **对象序列化与反序列化**:通过Serializable接口实现对象的持久化存储。 5. **多线程** - **线程的创建**:通过Thread类和Runnable接口创建线程。 - **线程同步**:synchronized关键字、wait()、notify()、...

    J2SE常见面试题

    - **对象序列化**:了解Serializable接口,以及序列化和反序列化的实现。 7. **网络编程** - **Socket编程**:客户端和服务器端的创建,以及数据的发送和接收。 - **URL和URLConnection**:用于网络资源的访问和...

    J2SE中流 ,异常 的个人总结

    - ObjectOutputStream 用于序列化对象到流中。 - ObjectInputStream 用于从流中反序列化对象。 5. 转换流(桥梁): - InputStreamReader 和 OutputStreamWriter 将字节流转换为字符流。 二、Java异常处理 异常...

    中文版J2SE1.6开发文档

    - **JAXB 2.0**:提升了Java对象与XML之间的绑定能力,简化了XML数据的序列化和反序列化。 6. **Swing组件和图形用户界面** - **UI改进**:J2SE 1.6对Swing组件进行了优化,提供了更多的可定制外观,增强了用户...

    java j2se源码

    马士兵老师的课程会讲解字节流、字符流、缓冲流以及对象序列化等概念。 GUI(图形用户界面)编程也是J2SE的一部分,Java Swing和JavaFX提供了一系列组件来创建桌面应用。通过学习如何布局组件、处理事件,你可以...

    J2SE课件J2SE课件

    5. **输入/输出流**:掌握文件操作,包括读写文件、序列化与反序列化,以及使用BufferedReader和PrintWriter进行字符流处理。 6. **多线程**:理解线程的概念,学习如何创建线程,使用synchronized关键字实现线程...

    J2SE面试题总汇

    - **默认方法**:Java 8 引入的新特性,接口中可以有默认实现的方法。 七、抽象类: - **抽象方法**:没有具体实现的方法,必须在子类中实现。 - **抽象类与接口的区别**:抽象类可以有非抽象方法和实例变量,而...

    J2se坦克大战2.0.1网络版

    通过序列化技术,将游戏状态(如坦克位置、射击信息)转化为字节流在网络间传输,确保游戏的实时同步。 在游戏逻辑方面,多线程技术发挥了重要作用。服务端需要处理多个客户端的连接和通信,每个客户端的活动都在...

    J2SE 核心类库源代码

    在J2SE核心类库中,包括了许多关键模块: 1. **基础类**:如`java.lang`包,其中包含所有Java程序的基本类,如`Object`、`String`、`Integer`等。`Object`是所有类的父类,它的方法如`equals()`、`hashCode()`和`...

    j2se聊天室(无需资源分)

    JSON可以方便地序列化和反序列化Java对象,使得数据交换更加灵活和高效。 4. **GUI设计**:J2SE提供了一整套的JavaFX或Swing库来创建图形用户界面(GUI)。聊天室的界面可能包括输入框、发送按钮、聊天历史显示区域...

    J2SE,J2EE学习笔记

    - **I/O流**:文件操作、输入输出流的分类和使用,包括字节流、字符流、缓冲流和对象序列化。 - **多线程**:线程的创建与同步,包括Thread类、Runnable接口、线程池和synchronized关键字的应用。 - **网络编程**...

    j2se api中文版

    11. **XML处理**:`javax.xml`包提供了处理XML的工具,如解析、序列化、转换和验证。 12. **Java虚拟机(JVM)接口**:`java.lang.instrument`和`java.lang.management`包提供了对JVM进行监控和修改的能力,例如,...

    j2se6_api中文版

    Filter流允许在已有流的基础上添加功能,而ObjectInputStream和ObjectOutputStream则支持对象的序列化和反序列化。 3. 多线程:Java提供了Thread类和Runnable接口来实现并发执行。通过继承Thread或实现Runnable,...

    j2se练习小程序

    这对于插件系统,序列化,动态代理等高级功能非常有用。 10. **枚举和注解**:枚举类型增强了常量的使用,而注解提供了一种向编译器或运行时系统提供元数据的方式,它们在很多现代Java框架中被广泛使用。 通过这个...

Global site tag (gtag.js) - Google Analytics