深入java 序列化的一些坑(转载)

来源:互联网 发布:编程语言实现模式 源码 编辑:程序博客网 时间:2024/06/11 14:21

这篇文章转载java序列化和反序列化过程中,值得注意的一些细节问题。更多的源代码请访问我的github:https://github.com/yangsheng20080808/deepIntoJava

参考内容来自:
https://www.ibm.com/developerworks/cn/java/j-lo-serial/

本文分为4大部分

  • 静态变量序列化
  • 父类的序列化与 Transient 关键字
  • 序列化存储规则
  • 对敏感字段加密

静态变量序列化

场景:静态变量可以被序列化吗?

public class Test implements Serializable {    private static final long serialVersionUID = 1L;    public static int staticVar = 5;    public static void main(String[] args) {        try {            //初始时staticVar为5            ObjectOutputStream out = new ObjectOutputStream(                    new FileOutputStream("result.obj"));            out.writeObject(new Test());            out.close();            //序列化后修改为10            Test.staticVar = 10;            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(                    "result.obj"));            Test t = (Test) oin.readObject();            oin.close();            //再读取,通过t.staticVar打印新的值            System.out.println(t.staticVar);        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (ClassNotFoundException e) {            e.printStackTrace();        }    }}

清单 2 中的 main 方法,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。依照清单 2,这个 System.out.println(t.staticVar) 语句输出的是 10 还是 5 呢?
最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

父类的序列化与 Transient 关键字

情境:一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

解决:要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

特性使用案例:
我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,形成类图如图 2 所示。
图 2. 案例程序类图
这里写图片描述
上图中可以看出,attr1、attr2、attr3、attr5 都不会被序列化,放在父类中的好处在于当有另外一个 Child 类时,attr1、attr2、attr3 依然不会被序列化,不用重复抒写 transient,代码简洁。

序列化存储规则

情境:多次写同一个对象到序列化文件中
清单 4. 存储规则问题代码

ObjectOutputStream out = new ObjectOutputStream(                   new FileOutputStream("result.obj"));   Test test = new Test();   //试图将对象两次写入文件   out.writeObject(test);   out.flush();   System.out.println(new File("result.obj").length());   out.writeObject(test);   out.close();   System.out.println(new File("result.obj").length());   ObjectInputStream oin = new ObjectInputStream(new FileInputStream(           "result.obj"));   //从文件依次读出两个文件   Test t1 = (Test) oin.readObject();   Test t2 = (Test) oin.readObject();   oin.close();   //判断两个引用是否指向同一个对象   System.out.println(t1 == t2);

清单 3 中对同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,然后从文件中反序列化出两个对象,比较这两个对象是否为同一对象。一般的思维是,两次写入对象,文件大小会变为两倍的大小,反序列化时,由于从文件读取,生成了两个对象,判断相等时应该是输入 false 才对,但是最后结果输出如图 4 所示。

图 4. 示例程序输出
这里写图片描述
我们看到,第二次写入对象时文件只增加了 5 字节,并且两个对象是相等的,这是为什么呢?
解答:Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

特性案例分析
查看清单 5 的代码。
清单 5. 案例代码

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));Test test = new Test();test.i = 1;out.writeObject(test);out.flush();test.i = 2;out.writeObject(test);out.close();ObjectInputStream oin = new ObjectInputStream(new FileInputStream(                    "result.obj"));Test t1 = (Test) oin.readObject();Test t2 = (Test) oin.readObject();System.out.println(t1.i);System.out.println(t2.i);

清单 4 的目的是希望将 test 对象两次保存到 result.obj 文件中,写入一次以后修改对象属性值再次保存第二次,然后从 result.obj 中再依次读出两个对象,输出这两个对象的 i 属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。
结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。

对敏感字段加密

情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,清单 3 展示了这个过程。

清单 3. 静态变量序列化问题代码:

/** * @author Yangsheng * 自定义序列化过程 */class hello implements Serializable{    int num;    private void writeObject(ObjectOutputStream out) throws IOException {        ObjectOutputStream.PutField putField = out.putFields();        System.out.println("原来的数字是:"+num);        num = 0;//模拟加密        putField.put("num",num);        System.out.println("加密后的数字是:"+ num);        out.writeFields();    }    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {        ObjectInputStream.GetField getField = in.readFields();        System.out.println("序列化后的数字是:"+getField.get("num", 100));        num = 19;//模拟解密        System.out.println("解密后的数字是:"+ num);    }    public hello(int num) {        this.num = num;    }    public hello(){}    @Override    public String toString() {        return "hello{" +                "num=" + num +                '}';    }}public class Client3 {    @Test    public void testVersion1() throws IOException, ClassNotFoundException {        hello hello1 = new hello(19);        String Path = "C:\\Users\\fsmsi\\Desktop\\fristVerPs.txt";        //序列化        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(Path));        //写信息出到文件        out.writeObject(hello1);        //反序列化        ObjectInputStream in = new ObjectInputStream(new FileInputStream(Path));        //读对象信息进到对象        hello hello2 = (hello) in.readObject();        System.out.println(hello2);    }}

运行结果:
这里写图片描述
在清单 3 的 writeObject 方法中,对密码进行了加密,在 readObject 中则对 password 进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。执行清单 3 后控制台输出如图 3 所示。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 美宝旅行证过期怎么办 施工员证过期3年怎么办 电脑上没声音了怎么办 电脑音频被删了怎么办 微信绑定不上qq怎么办 微信绑定qq频繁怎么办 qq绑定不了微信怎么办 微信绑定不了qq怎么办 微信号登录不上怎么办 微信号登不上去怎么办 微信忘记密码怎么办啊 微信密码忘记了怎么办 维a酸乳膏掉皮怎么办 用维a酸乳膏脱皮怎么办 微信红包发不了怎么办 红包一次发3个怎么办 红包没领删除了怎么办 不主动还钱的人怎么办 老婆要离婚我不想离怎么办 男人有外遇女人该怎么办 QQ里有人假冒你怎么办 微信有人冒充我怎么办 有人冒充我的qq怎么办 微信有人冒充你怎么办 海底捞8折会员卡怎么办 西安公交卡坏了怎么办 吃生大蒜胃疼怎么办 养竹子的水臭了怎么办 雾霾引起的咳嗽怎么办 招财竹叶子发黄怎么办 养的富贵竹水臭怎么办 3年水竹叶子发黄怎么办 蒸柜下面漏蒸汽怎么办 美国自驾游驾照怎么办 装修抽屉门大了怎么办 当私人教练老了怎么办 铁艺花架不赚钱怎么办 请的关公不要了怎么办 星月放久了黄了怎么办 信用卡多还了钱怎么办 还房贷的卡丢了怎么办