序列化与反序列化

概念

序列化:把对象转换为字节序列的过程称为对象的序列化。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

对象的序列化主要有两种用途:

  1.把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

  2.在网络上传送对象的字节序列。

  在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的session象还原到内存中。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

  有人知道User类添加序列化的ID有什么用吗?

默认序列化

序列化只需要实现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也不会变化。但如果你进行序列化之后,更改了类中的信息却没有再次进行序列化,那么此时进行反序列化就会报错,因为更改类中信息之后serialVersionUID改变了,但是字节流中的serialVersionUID还是更改信息之前的,所以两个serialVersionUID不同,报错如下:local class incompatible: stream classdesc serialVersionUID = 4899167466734800618, local class serialVersionUID=-3966288979410271124

即序列化的版本号不一致。

但如果显式定义一个serialVersionUID,那么就算你更改了类中的信息,没有再次序列化,反序列化的时候也不会抛错误。

参考例子:Java基础学习总结——Java对象的序列化和反序列化

static关键字,transient关键字修饰的变量与序列化?

举例:

 1 public class SerializableObject implements Serializable{
 2     private static final long serialVersionUID = -3966288979410271124L;
 3     private String str0;
 4     private transient String str1;
 5     private static String str2 = "abc";
 6 
 7     public SerializableObject(String str0, String str1) {
 8         this.str0 = str0;
 9         this.str1 = str1;
10     }
11 
12     public String getStr0() {
13         return str0;
14     }
15 
16     public String getStr1()
17     {
18         return str1;
19     }
20 }
21 
22 //测试
23 public class Test {
24     public static void main(String[] args) throws Exception {
25         File file = new File("D:" + File.separator + "test.txt");
26         OutputStream outputStream = new FileOutputStream(file);
27         ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
28         SerializableObject serializableObject = new SerializableObject("str0","str1");
29         objectOutputStream.writeObject(serializableObject);
30         objectOutputStream.close();
31         outputStream.close();
32 
33         InputStream inputStream = new FileInputStream(file);
34         ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
35         SerializableObject serializableObject1 = (SerializableObject) objectInputStream.readObject();
36         System.out.println("str0 = " + serializableObject1.getStr0());
37         System.out.println("str1 = " + serializableObject1.getStr1());
38         objectInputStream.close();
39         inputStream.close();
40     }
41 }

结果:

str0 = str0
str1 = null

根据结果和序列化后的二进制文件的解析,可以得出:

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

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

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

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

关于transient和static的序列化和反序列化

 自定义序列化过程

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

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

这是非常有用的。比如:

1、有些场景下,某些字段我们并不想要使用Java提供给我们的序列化方式,而是想要以自定义的方式去序列化它,比如ArrayList的elementData、HashMap的table,就可以通过将这些字段声明为transient,然后在writeObject和readObject中去使用自己想要的方式去序列化它们

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

上面的例子SerializableObject这个类修改一下:

public class SerializableObject implements Serializable{
    private static final long serialVersionUID = -3966288979410271124L;
    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(ObjectOutputStream objectOutputStream) throws IOException {
        System.out.println("自定义序列化的过程");
        objectOutputStream.defaultWriteObject();
        str1 = "selfDefine";
        objectOutputStream.writeInt(str1.length());
        objectOutputStream.writeChars(str1);
    }
  
  //自定义反序列化方法
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { System.out.println("自定义反序列化的过程"); objectInputStream.defaultReadObject(); int length = objectInputStream.readInt(); char[] chars = new char[length]; for (int i = 0; i < length; i++){ chars[i] = objectInputStream.readChar(); } str1 = String.valueOf(chars); } } public class Test { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test.txt"); OutputStream outputStream = new FileOutputStream(file); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); SerializableObject serializableObject = new SerializableObject("str0","str1"); objectOutputStream.writeObject(serializableObject); objectOutputStream.close(); outputStream.close(); InputStream inputStream = new FileInputStream(file); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); SerializableObject serializableObject1 = (SerializableObject) objectInputStream.readObject(); System.out.println("str0 = " + serializableObject1.getStr0()); System.out.println("str1 = " + serializableObject1.getStr1()); objectInputStream.close(); inputStream.close(); } }

运行结果:

自定义序列化的过程
自定义反序列化的过程
str0 = str0
str1 = selfDefine

看到,程序走到了我们自己写的writeObject和readObject中,而且被transient修饰的str1也成功序列化、反序列化出来了----因为手动将str1写入了文件和从文件中读了出来。

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

复杂序列化情况总结

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

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

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

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

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

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

serialVersionUID的取值

serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值

显式地定义serialVersionUID有两种用途:

  1、 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

  2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

参考资料:

https://www.cnblogs.com/xrq730/p/4821958.html

https://www.cnblogs.com/xdp-gacl/p/3777987.html

posted @ 2019-01-24 14:43  吹灭读书灯  阅读(219)  评论(0编辑  收藏  举报