• 序列化与反序列化


    概念

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

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

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

      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

  • 相关阅读:
    P6057 [加油武汉]七步洗手法
    LC 1349. Maximum Students Taking Exam (Hungarian / Max Flow)
    P1879 [USACO06NOV]玉米田Corn Fields
    P1433 吃奶酪 (TSP)
    LC 1349. Maximum Students Taking Exam
    获取XML中的值
    TimeZoneInfo类的使用
    XML 克隆节点
    网络协议概述:物理层、连接层、网络层、传输层、应用层详解
    Vuex
  • 原文地址:https://www.cnblogs.com/zfyang2429/p/10314401.html
Copyright © 2020-2023  润新知