java 序列化 transient_Java序列化1:序列化、反序列化和transient关键字的作用

  • Post author:
  • Post category:java


标签:

网上讲Java序列化的文章很多,感觉很多都讲得不全,这篇文章希望可以全面地剖析Java的序列化机制。为什么要进行序列化和反序列化?我们写了一个Object,但那是Java虚拟机堆内存里面的东西,利用Object进行网络通信、IO操作的时候怎么会认识Java堆内存里面的东西?所以,需要序列化和反序列化机制的保障。

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

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

默认序列化

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

1、默认的1L

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

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

1 public class SerializableObject implementsSerializable2 {3 private static final long serialVersionUID = 1L;4

5 privateString str0;6 private transientString str1;7 private static String str2 = “abc”;8

9 publicSerializableObject(String str0, String str1)10 {11 this.str0 =str0;12 this.str1 =str1;13 }14

15 publicString getStr0()16 {17 returnstr0;18 }19

20 publicString getStr1()21 {22 returnstr1;23 }24 }

1 public static void main(String[] args) throwsException2 {3 File file = new File(“D:” + File.separator + “s.txt”);4 OutputStream os = newFileOutputStream(file);5 ObjectOutputStream oos = newObjectOutputStream(os);6 oos.writeObject(new SerializableObject(“str0”, “str1”));7 oos.close();8

9 InputStream is = newFileInputStream(file);10 ObjectInputStream ois = newObjectInputStream(is);11 SerializableObject so =(SerializableObject)ois.readObject();12 System.out.println(“str0 = ” +so.getStr0());13 System.out.println(“str1 = ” +so.getStr1());14 ois.close();15 }

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

20180110164231583680.png

第1部分是序列化文件头

◇AC ED:STREAM_MAGIC序列化协议

◇00 05:STREAM_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:这个没弄清楚意思

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

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

◇74:这个没弄清楚意思

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

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

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

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

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

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

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

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

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

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

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

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

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

str0 = str0

str1 = null

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

手动指定序列化过程

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

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

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

1 public class SerializableObject implementsSerializable2 {3 private static final long serialVersionUID = 1L;4

5 privateString str0;6 private transientString str1;7 private static String str2 = “abc”;8

9 publicSerializableObject(String str0, String str1)10 {11 this.str0 =str0;12 this.str1 =str1;13 }14

15 publicString getStr0()16 {17 returnstr0;18 }19

20 publicString getStr1()21 {22 returnstr1;23 }24

25 private void writeObject(java.io.ObjectOutputStream s) throwsException26 {27 System.out.println(“我想自己控制序列化的过程”);28 s.defaultWriteObject();29 s.writeInt(str1.length());30 for (int i = 0; i < str1.length(); i++)31 s.writeChar(str1.charAt(i));32 }33

34 private void readObject(java.io.ObjectInputStream s) throwsException35 {36 System.out.println(“我想自己控制反序列化的过程”);37 s.defaultReadObject();38 int length =s.readInt();39 char[] cs = new char[length];40 for (int i = 0; i < length; i++)41 cs[i] =s.readChar();42 str1 = new String(cs, 0, length);43 }44 }

直接看一下运行结果

我想自己控制序列化的过程

我想自己控制反序列化的过程

str0 = str0

str1 = str1

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

20180110164231588562.png

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

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

标签:



版权声明:本文为weixin_31569663原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。