七十五、考虑使用自定义的序列化形式:

设计一个类的序列化形式和设计该类的API同样重要,因此在没有认真考虑好默认的序列化形式是否合适之前,不要贸然使用默认的序列化行为。在作出决定之前,你需要从灵活性、性能和正确性多个角度对这种编码形式进行考察。一般来讲,只有当你自行设计的自定义序列化形式与默认的形式基本相同时,才能接受默认的序列化形式。比如,当一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。见如下代码示例:

  1. public class Name implements Serializable {
  2. private final String lastName;
  3. private final String firstName;
  4. private final String middleName;
  5. ... ...
  6. }

从逻辑角度而言,该类的三个域字段精确的反应出它的逻辑内容。然而有的时候,即便默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性,如上例代码中,firstName和lastName不能为null等。

下面我们再看一个极端的例子:

  1. public final class StringList implements Serializable {
  2. private int size = 0;
  3. private Entry head = null;
  4. private static class Entry implements Serializable {
  5. String data;
  6. Entry next;
  7. Entry previous;
  8. }
  9. }

对于上面的示例代码,如果采用默认形式的序列化,将会导致双向链表中的每一个节点的数据以及前后关系都会被序列化。因此这种物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下几个缺点:

1. 它使这个类的导出API永远的束缚在该类的内部表示法上,即使今后找到更好的的实现方式,也无法摆脱原有的实现方式。

2. 它会消耗过多的空间。事实上对于上面的示例代码,我们只需要序列化数据部分,可以完全忽略链表节点之间的关系。

3. 它会消耗过多的时间。

4. 它会引起栈溢出。

根据以上四点,我们修订了StringList类的序列化实现方式,见如下代码:

  1. public final class StringList implements Serializable {
  2. private transient int size = 0;
  3. private transient Entry head = null;
  4. private static class Entry {
  5. String data;
  6. Entry next;
  7. Entry previous;
  8. }
  9. private void writeObject(ObjectOutputStream s) throws IOException {
  10. s.defaultWriteObject();
  11. s.writeInt(size);
  12. for (Entry e = head; e != null; e = e.next)
  13. s.writeObject(e.data);
  14. }
  15. private void readObject(ObjectInputStream s)
  16. throws IOException, ClassNotFoundException {
  17. s.defaultReadObject();
  18. int numElemnet = s.readInt();
  19. for (int i = 0; i < numElements; i++)
  20. add((String)s.readObject());
  21. }
  22. public final void add(String s) { ... }
  23. ... ...
  24. }

在修订代码中,所有的域字段都是transient,但writeObject和readObject方法的首要任务仍然是先调用defaultWriteObject和defaultReadObject方法,即便这对于缺省序列化形式并不是必须的。因为在今后的修改中,很有可能会为该类添加非transient域字段,一旦忘记同步修改writeObject或readObject方法,将会导致序列化和反序列化的数据处理方式不一致。

对于默认序列化还需要进一步说明的是,当一个或多个域字段被标记为transient时,如果要进行反序列化,这些域字段都将被初始化为其类型默认值,如对象引用域被置为null,数值基本域的默认值为0,boolean域的默认值为false。如果这些值不能被任何transient域所接受,你就必须提供一个readObject方法。它首先调用defaultReadObject,然后再把这些transient域恢复为可接受的值。

最后需要说明的是,无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步,见如下代码:

  1. private synchronized void writeObject(ObjectOutputStream s) throws IOException {
  2. s.defaultWriteObject();
  3. }

七十六、保护性的编写readObject方法:

在条目39中介绍了一个不可变的日期范围类,它包含可变的私有Date域。该类通过在其构造器和访问方法中保护性的拷贝Date对象,极力的维护其约束条件和不可变性。见如下代码:

  1. public final class Period {
  2. private final Date start;
  3. private final Date end;
  4. public Period(Date start, Date end) {
  5. this.start = new Date(start.getTime());
  6. this.end = new Date(end.getTime());
  7. if (this.start.compareTo(this.end) > 0)
  8. throw new IllegalArgumentException();
  9. }
  10. public Date start() {
  11. return new Date(start.getTime());
  12. }
  13. public Date end() {
  14. return new Date(end.getTime());
  15. }
  16. public String toString() {
  17. return start + " - " + end;
  18. }
  19. ... ...
  20. }

这个对象的物理表示法和其逻辑表示法完全匹配,所以我们可以使用默认的序列化形式。因此在声明该类的地方增加" implements Serializable "。然而,如果你真是这样做了,那么这个类将不再保证他的关键约束了。

问题在于,如果反序列化的数据源来自于该类实例的正常序列化,那么将不会引发任何问题。如果恰恰相反,反序列化的数据源来自于一组伪造的数据流,事实上,反序列化的机制就是从一组有规则的数据流中实例化指定对象,那么我们将不得不面对Period实例对象的内部约束被破坏的危险,见如下代码:

  1. public class BogusPeriod {
  2. private static final byte[] serializedForm = new byte[] {
  3. ... ... //这里是一组伪造的字节流数据
  4. };
  5. public static void main(String[] args) [
  6. Period p = (Period)deserialize(serializedForm);
  7. System.out.println(p);
  8. }
  9. private static Object deserialize(byte[] sf) {
  10. try {
  11. InputStream is = new ByteArrayInputStream(sf);
  12. ObjectInputStream ois = new ObjectInputStream(is);
  13. return ois.readObject();
  14. } catch (Exception e) {
  15. throw new IllegalArgumentException(e);
  16. }
  17. }
  18. }

如果执行上面的代码就会发现Period的约束被打破了,end的日期早于start。为了修正这个问题,可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象的有效性。如果检查失败,则抛出InvalidObjectException异常,使反序列化过程不能成功地完成。

  1. private void readObject(ObjectInputStream s)
  2. throws IOException,ClassNotFoundException {
  3. s.defaultReadObject();
  4. if (start.compareTo(end) > 0)
  5. throw new InvalidObjectException(start + " after " + end);
  6. }

如果执行上面的代码就会发现Period的约束被打破了,end的日期早于start。为了修正这个问题,可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象的有效性。如果检查失败,则抛出InvalidObjectException异常,使反序列化过程不能成功地完成。

  1. private void readObject(ObjectInputStream s)
  2. throws IOException,ClassNotFoundException {
  3. s.defaultReadObject();
  4. if (start.compareTo(end) > 0)
  5. throw new InvalidObjectException(start + " after " + end);
  6. }

除了上面的***方式之外,还存在着另外一种更为隐匿的***方式,它也是通过伪造序列化数据流的方式来骗取反序列化方法的信任。它在伪造数据时,将私有域字段的引用在外部保存起来,这样当对象实例反序列化成功后,由于外部仍然可以操作其内部数据,因此危险仍然存在。如何避免该风险呢?见如下修订后的readObject方法:

  1. private void readObject(ObjectInputStream s)
  2. throws IOException,ClassNotFoundException {
  3. s.defaultReadObject();
  4. //执行保护性copy
  5. start = new Date(start.getTime());
  6. end = new Date(end.getTime());
  7. if (start.compareTo(end) > 0)
  8. throw new InvalidObjectException(start + " after " + end);
  9. }

注意,保护性copy一定要在有效性检查之前进行。

这里给出一个基本的规则,可以用来帮助确定默认的readObject方法是否可以被接受。规则是增加一个公有的构造器,其参数对应于该对象中每个非transient域,并且无论参数的值是什么,都是不进行检查就可以保存到相应的域中的。对于这样的做法如果仍然可以接受,那么默认的readObject就是合理,否则就需要提供一个显式的readObject方法。

对于非final的可序列化类,在readObject方法和构造器之间还有其他类似的地方,readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调都不可以。如果违反了该规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能会失败。

转载于:https://blog.51cto.com/andra/780485

java 75-76相关推荐

  1. [Leedcode][JAVA][第76题][最小覆盖子串]滑动窗口]

    [问题描述][第76题][最小覆盖子串][中等] 给你一个字符串 S.一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串.示例:输入: S = "ADOBECODEBAN ...

  2. 康佳应急广播 EmergRadioBZ(KBTA100-C)项目

    目录 测试信息 创建广播 适配器,资源调度器分别指什么? 播放媒体.通道指什么? 广播的输入内容 输出通道.调频.IP具体指什么? 适配器,资源调度器 演练播发类型 广播内容 参数设置 资源编码设置的 ...

  3. Java后端WebSocket的Tomcat实现

    转自: http://blog.chenzuhuang.com/archive/28.html http://www.cnblogs.com/xdp-gacl/p/5193279.html 一.Web ...

  4. java固定资产管理系统代码_Java 固定资产管理系统(课程设计)

    Java课程设计作业--固定资产管理系统,Eclipse+SERVER2000构架,对照综合的Java编程实战,学习Swing的好资料,SQL库资料在AMSConfiguration\db目录下,附加 ...

  5. Java项目:考试系统Java基础Gui(java+Gui)

    源码获取:博客首页 "资源" 里下载! 功能简介: 所属课程.题目内容.题目选项.题目答案.题目等级.学生管理.试卷管理.题目管理.时间控制 服务页面: public class ...

  6. 【原】Java学习笔记020 - 面向对象

    1 package cn.temptation; 2 3 public class Sample01 { 4 public static void main(String[] args) { 5 // ...

  7. 【Java】Lucene检索引擎详解

    基于Java的全文索引/检索引擎--Lucene Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能. L ...

  8. [JAVA] java仿windows 字体设置选项卡

    想用java做一个像windows里一样的txt编辑软件,涉及到字体设置选项卡,在网上找了很久都没找到,就生气啦自己写一个,现在贴这里分享一下,下次再遇到这样的问题就不用自己亲自打代码啦! 1 pac ...

  9. Java基础学习总结(3)——抽象类

    2019独角兽企业重金招聘Python工程师标准>>> 一.抽象类介绍 下面通过一下的小程序深入理解抽象类 因此在类Animal里面只需要定义这个enjoy()方法就可以了,使用ab ...

  10. JAVA增删改查XML文件

    2019独角兽企业重金招聘Python工程师标准>>> 最近总是需要进行xml的相关操作. 不免的要进行xml的读取修改等,于是上网搜索,加上自己的小改动,整合了下xml的常用操作. ...

最新文章

  1. 【最大团】【HDU1530】【Maximum Clique】
  2. NeHe教程Qt实现——lesson09
  3. innodb主键的长度为什么不能大于767字节
  4. react 的props和state
  5. Socket套接字 =======================
  6. android代码混淆作用,Android分享:代码混淆那些事
  7. skywalking 引起 spring-cloud-gateway 的内存溢出 skywalking的bug
  8. 学生渐进片add如何给_【收藏】为青少年验配渐进多焦点时,如何选择合适ADD?...
  9. 搜索引擎特征码(转)
  10. Android Studio插件安装
  11. linux手误rm可能不需要跑路
  12. 搭建Nodejs环境 创建Express应用
  13. usermod+用户密码管理+mkpasswd
  14. 软件测试行业前景,人才稀缺
  15. verilog学习笔记——三段式状态机
  16. DDOS攻击器常见的三种方式
  17. 计算机需要权限来执行此操作 win7,Win7系统下“文件夹访问被拒绝 您需要权限来执行操作”解决方法...
  18. 炼数成金 Oracle EBS R12 DBA培训视频教程
  19. 如何获取easyclick手机安装包
  20. 【读书笔记】文案创作完全手册

热门文章

  1. 期末总结:LINUX内核分析与设计期末总结
  2. JavaScript正则表达式在线测试工具
  3. Java Servlet关键点详解
  4. session 安全问题(关闭页面时自动清除session)
  5. iol植入手术过程_Phaco+IOL植入术病人的护理
  6. impala连接使用方法
  7. 每天5分钟玩转python3算法:二分查找法
  8. 在ubuntu16.04中一键创建LAMP环境
  9. 【自动化__持续集成】___java___重载
  10. 【IDEAEclipse】2、从Eclipse转移到IntelliJ IDEA一点心得