博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java对象表示方式1:序列化、反序列化和 transient 关键字的作用
阅读量:5044 次
发布时间:2019-06-12

本文共 6037 字,大约阅读时间需要 20 分钟。

前言

平时我们接触到的 Java 对象全都是在内存中的对象(它像磁盘中的一个个文件那样实实在在的存在着),所以它是无法进行 IO 操作或者网络通信的,因为在进行 IO 操作或者网络通信的时候,人家根本不知道内存中的对象是个什么东西,因此必须将对象以某种方式表示出来,即内存对象的存储状态。一个 Java 对象的表示有各种各样的方式,Java 本身也提供给了用户一种表示对象的方式,那就是序列化。换句话说,序列化只是表示对象的一种方式而已。OK,有了序列化,那么必然有反序列化,我们先看一下序列化、反序列化是什么意思。

序列化:将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。

反序列化:将字节数组重新构造成对象。

 

默认序列化

序列化只需要实现 java.io.Serializable 接口就可以了。序列化的时候有一个 serialVersionUID 参数,Java序列化机制是通过在运行时判断类的 serialVersionUID来 验证版本一致性的。在进行反序列化,Java 虚拟机会把传过来的字节流中的 serialVersionUID 和本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的实体类,可以进行反序列化,否则 Java 虚拟机会拒绝对这个实体类进行反序列化并抛出异常(换句话说,这个 serialVersionUID 主要的作用是为了反序列化的时候的校验,而序列化的时候只是将它以二进制流的形式保存下来,不涉及校验)。serialVersionUID 有两种生成方式:

1、默认的 1L

2、根据类名、接口名、成员方法以及属性等来生成一个 64 位的 Hash 字段

如果实现 java.io.Serializable 接口的实体类没有显式定义一个名为 serialVersionUID、类型为 long 的变量时,Java 序列化机制会根据编译的 .class 文件自动生成一个 serialVersionUID,如果 .class 文件没有变化,那么就算编译再多次,serialVersionUID 也不会变化。换言之,Java 为用户定义了默认的序列化、反序列化方法,其实就是 ObjectOutputStreamdefaultWriteObject 方法和 ObjectInputStreamdefaultReadObject 方法。看一个例子:

public class SerializableObject implements Serializable{    private static final long serialVersionUID = 1L;        private String str0;    private transient String str1;    private static String str2 = "abc";        public SerializableObject(String str0, String str1)    {        this.str0 = str0;        this.str1 = str1;    }        public String getStr0()    {        return str0;    }    public String getStr1()    {        return str1;    }}
public static void main(String[] args) throws Exception{    File file = new File("D:" + File.separator + "s.txt");    OutputStream os = new FileOutputStream(file);      ObjectOutputStream oos = new ObjectOutputStream(os);  // objectOutputStream 最终要写入文件还是要调用 FileOutputStream 的    oos.writeObject(new SerializableObject("str0", "str1"));  // 把对象写入文件    oos.close();            InputStream is = new FileInputStream(file);    ObjectInputStream ois = new ObjectInputStream(is);    SerializableObject so = (SerializableObject)ois.readObject();    System.out.println("str0 = " + so.getStr0());    System.out.println("str1 = " + so.getStr1());    ois.close();}

先不运行,用一个二进制查看器查看一下 s.txt 这个文件,并详细解释一下每一部分的内容。

第 1 部分是序列化文件头

  ◇AC EDSTREAM_MAGIC 序列化协议

  ◇00 05STREAM_VERSION 序列化协议版本

  ◇73:TC_OBJECT 声明这是一个新的对象

第 2 部分是要序列化的类的描述,在这里是 SerializableObject 类

  ◇72:TC_CLASSDESC 声明这里开始一个新的 class

  ◇00 1F:十进制的 31,表示 class 名字的长度是31个字节

  ◇63 6F 6D ... 65 63 74:表示的是 “com.xrq.test.SerializableObject” 这一串字符,可以数一下确实是 31 个字节

  ◇00 00 00 00 00 00 00 01:SerialVersion,序列化ID,1

  ◇02:标记号,声明该对象支持序列化

  ◇00 01:该类所包含的域的个数为 1 个

第 3 部分是对象中各个属性项的描述

  ◇4C:字符 "L",表示该属性是一个对象类型而不是一个基本类型

  ◇00 04:十进制的 4,表示属性名的长度

  ◇73 74 72 30:字符串 “str0”,属性名

  ◇74:TC_STRING,代表一个 new String,用 String 来引用对象

第 4 部分是该对象父类的信息,如果没有父类就没有这部分。有父类和第 2 部分差不多

  ◇00 12:十进制的 18,表示父类的长度

  ◇4C 6A 61 ... 6E 67 3B:“L/java/lang/String;” 表示的是父类属性

  ◇78:TC_ENDBLOCKDATA,对象块结束的标志

  ◇70:TC_NULL,说明没有其他超类的标志

第 5 部分输出对象的属性项的实际值,如果属性项是一个对象,这里还将序列化这个对象,规则和第 2 部分一样

  ◇00 04:十进制的 4,属性的长度

  ◇73 74 72 30:字符串 “str0”,str0 的属性值

从以上对于序列化后的二进制文件的解析,我们可以得出以下几个关键的结论:

1、序列化之后保存的是对象的信息

2、被声明为 transient 的属性不会被序列化,这就是 transient 关键字的作用

3、被声明为 static 的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是 static 修饰的变量是属于类的而不是属于对象的,因此序列化的时候不会序列化它

接下来运行一下上面的代码看一下

str0 = str0str1 = null

因为 str1 是一个 transient 类型的变量,没有被序列化,因此反序列化出来也是没有任何内容的,显示的 null,符合我们的结论。

手动指定序列化过程

Java 并不强求用户非要使用默认的序列化方式,用户也可以按照自己的喜好自己指定自己想要的序列化方式----只要你自己能保证序列化前后能得到想要的数据就好了。手动指定序列化方式的规则是:

进行序列化、反序列化时,虚拟机会首先试图调用对象里的 writeObjec t和 readObject 方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,那么默认调用的是 ObjectOutputStream 的 defaultWriteObject 以及ObjectInputStream 的defaultReadObject 方法。换言之,利用自定义的 writeObject 方法和 readObject 方法,用户可以自己控制序列化和反序列化的过程。

这是非常有用的。比如:

1、有些场景下,某些字段我们并不想要使用 Java 提供给我们的序列化方式,而是想要以自定义的方式去序列化它,比如 ArrayList 的 elementData、HashMap 的 table(至于为什么在之后写这两个类的时候会解释原因),就可以通过将这些字段声明为 transient,然后在 writeObjec 和 readObject 中去使用自己想要的方式去序列化它们

2、因为序列化并不安全,因此有些场景下我们需要对一些敏感字段进行加密再序列化,然后再反序列化的时候按照同样的方式进行解密,就在一定程度上保证了安全性了。要这么做,就必须自己写 writeObject 和readObject,writeObject 方法在序列化前对字段加密,readObject 方法在序列化之后对字段解密

上面的例子 SerializObject 这个类修改一下,主函数不需要修改:

public class SerializableObject implements Serializable{    private static final long serialVersionUID = 1L;        private String str0;    private transient String str1;    private static String str2 = "abc";        public SerializableObject(String str0, String str1)    {        this.str0 = str0;        this.str1 = str1;    }        public String getStr0()    {        return str0;    }    public String getStr1()    {        return str1;    }        private void writeObject(java.io.ObjectOutputStream s) throws Exception    {        System.out.println("我想自己控制序列化的过程");        s.defaultWriteObject();        s.writeInt(str1.length());        for (int i = 0; i < str1.length(); i++)  // 说实话如果想学号自定义的序列化,就要对 ObjectOutputStream 和 ObjectInputStream 中的方法要很清楚            s.writeChar(str1.charAt(i));    }        private void readObject(java.io.ObjectInputStream s) throws Exception    {        System.out.println("我想自己控制反序列化的过程");        s.defaultReadObject();        int length = s.readInt();  // 和上面的相呼应,上面是 s.writeInt(str1.length()); 写进去的是 str1 的 length;那么这里通过 s.readInt() 读出来的也是 length        char[] cs = new char[length];        for (int i = 0; i < length; i++)            cs[i] = s.readChar();        str1 = new String(cs, 0, length); //     }}

运行结果

我想自己控制序列化的过程我想自己控制反序列化的过程str0 = str0str1 = str1

看到,程序走到了我们自己写的 writeObject 和 readObject 中,而且被 transient 修饰的 str1 也成功序列化、反序列化出来了----因为手动将 str1 写入了文件和从文件中读了出来。不妨再看一下 s.txt 文件的二进制:

看到橘黄色的部分就是 writeObject 方法追加的 str1 的内容。至此,总结一下 writeObject 和 readObject 的通常用法:

先通过 defaultWriteObject 和 defaultReadObject 方法序列化、反序列化对象,然后在文件结尾追加需要额外序列化的内容/从文件的结尾读取额外需要读取的内容。

复杂序列化情况总结

虽然 Java 的序列化能够保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是比较难处理的,最后对一些复杂的对象情况作一个总结:

1、当父类继承 Serializable 接口时,所有子类都可以被序列化

2、子类实现了 Serializable 接口,父类没有,父类中的属性不能序列化(不报错,数据丢失),但是在子类中属性仍能正确序列化

3、如果序列化的属性是对象,则这个对象也必须实现 Serializable 接口,否则会报错

4、反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错

5、反序列化时,如果 serialVersionUID 被修改,则反序列化时会失败

转载于:https://www.cnblogs.com/tkzL/p/8903338.html

你可能感兴趣的文章