java序列化相关

序言

序列化是一种对象持久化的手段,普遍应用在网络传输、保存本地文件、数据库场景中

基本概念

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

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

为什么要序列化

Java平台允许我们在内存中创建可复用的Java对象,但只有当JVM(Java虚拟机)处于运行时,这些对象才可能存在,也就是这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象(持久化对象),并在将来重新读取被保存的对象。Java对象序列化就实现了该功能。

网络通信时,无论是何种类型的数据,都会转成字节序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。Java对象序列化也实现了该功能。

所以序列化机制会把内存中的Java对象转换成与平台无关的二进制流,从而永久地保存在磁盘上或是通过网络传输到另一个网络节点。

举个场景例子

...

什么情况下你需要用到

  • 网络上传输各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都是以二进制序列的形式在网络上传送的,那么发送方就需要将这些数据序列化为字节流后传输,而接收方接到字节流后需要反序列化为相应的数据类型。
  • 当然接收方也可以将接收到的字节流存储到磁盘中,等到以后想恢复的时候再恢复。

可以得出对象的序列化和反序列化主要有两种用途:

  • 把对象的字节序列永久地保存到磁盘上。(持久化对象)
  • 可以将Java对象以字节序列的方式在网络中传输。(网络传输对象)

serialVersionUID

情境:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。

问题:C 对象的全类路径假设为 com.inout.Test,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。

解决:虚拟机是否允许反序列化,不仅取决于类路径功能代码是否一致,一个非常重要的一点是两个类的序列化ID是否一致(就是 private static final long serialVersionUID = 1L)。

实现

  • Serializable
  public interface Serializable {
    }
  • Externalizable
    public interface Externalizable extends java.io.Serializable {
         
        void writeExternal(ObjectOutput out) throws IOException;

        void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
    }

Externalizable这里不做描述

静态变量序列化

    public class Main2 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("static_serializable_demo.obj"));
                out.writeObject(new Main2());
                out.close();

                //序列化后修改为10
                Main2.staticVar = 10;

                ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                        "static_serializable_demo.obj"));
                Main2 t = (Main2) oin.readObject();
                oin.close();

                //再读取,通过t.staticVar打印新的值
                System.out.println(t.staticVar);

            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

输出的结果是: 10

之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

对敏感字段加密

情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作

    public class Main3 implements Serializable {

        private static final long serialVersionUID = 1L;
        private String password = "pass";

        public String getPassword() {
            return password;
        }
        public void setPassword(String password) {
            this.password = password;
        }

        private void writeObject(ObjectOutputStream out) {
            try {
                ObjectOutputStream.PutField putFields = out.putFields();
                System.out.println("原密码:" + password);
                password = "encryption";//假装加密
                putFields.put("password", password);
                System.out.println("加密后的密码" + password);
                out.writeFields();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        private void readObject(ObjectInputStream in) {
            try {
                ObjectInputStream.GetField readFields = in.readFields();
                Object object = readFields.get("password", "");
                System.out.println("要解密的字符串:" + object.toString());
                password = "pass";//假装解密,需要获得本地的密钥
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }

        }

        public static void main(String[] args) {
            try {
                ObjectOutputStream out = new ObjectOutputStream(
                        new FileOutputStream("password_serializable_demo.obj"));
                out.writeObject(new Main3());
                out.close();

                ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                        "password_serializable_demo.obj"));
                Main3 t = (Main3) oin.readObject();
                System.out.println("解密后的字符串:" + t.getPassword());
                oin.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

输出:

原密码:pass
加密后的密码encryption
要解密的字符串:encryption
解密后的字符串:pass

序列化存储规则1

    public class Main4 implements Serializable {

        public static void main(String[] args) {
            try {
                ObjectOutputStream out = new ObjectOutputStream(
                        new FileOutputStream("rule_serializable_demo.obj"));
                Main4 test = new Main4();
                //试图将对象两次写入文件
                out.writeObject(test);
                out.flush();
                System.out.println(new File("rule_serializable_demo.obj").length());
                out.writeObject(test);
                out.close();
                System.out.println(new File("rule_serializable_demo.obj").length());

                ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                        "rule_serializable_demo.obj"));
                //从文件依次读出两个文件
                Main4 t1 = (Main4) oin.readObject();
                Main4 t2 = (Main4) oin.readObject();
                oin.close();

                //判断两个引用是否指向同一个对象
                System.out.println(t1 == t2);

            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

    }

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

56
61
true

我们看到,第二次写入对象时文件只增加了 5 字节,并且两个对象是相等的,这是为什么呢?

再看下面的例子

序列化存储规则2

    public class Main5 implements Serializable {
        public int i;
        public String mString;
        public static String fileName = "main5_serializable_demo.obj";

        public static void main(String[] args) {
            try {
                ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName));
                Main5 test = new Main5();
                test.i = 1;
                test.mString = "liu";
                out.writeObject(test);
                out.flush();
                test.i = 2;
                test.mString = "zhang";
                out.writeObject(test);
                out.close();
                ObjectInputStream oin = new ObjectInputStream(new FileInputStream(fileName));
                Main5 t1 = (Main5) oin.readObject();
                Main5 t2 = (Main5) oin.readObject();
                System.out.println(t1.toString());
                System.out.println(t2.toString());
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        @Override
        public String toString() {
            return "Main5{" +
                    "i=" + i +
                    ", mString='" + mString + '\'' +
                    '}';
        }
    }

输出结果:

Main5{i=1, mString='liu'}
Main5{i=1, mString='liu'}

原因是:

Java序列化遵循以下算法:

  • 所有序列化过的,包括磁盘中的的实例对象都有一个序列化编号
  • 当试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,当对象在本次虚拟机中从未被序列化过,则系统将其序列化为字节序列并输出
  • 如果某个对象在本次虚拟机中已经序列化过,则直接输出这个序列化编号

鉴于以上的算法可能会造成一个潜在的问题:当序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会输出字节序列,而第二次调用时仅仅输出一个序列化编号,即使我们改变了这个对象的一些属性,这些改变后的属性也不会序列化到磁盘上

总结

  • 在Java中,只要一个类实现了java.io.Serializable、Externalizable接口,那么它就可以被序列化。
  • 通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化
  • 虚拟机是否允许反序列化,取决于类路径&&功能代码&&两个类的序列化ID是否一致(就是 private static final long serialVersionUID)
  • 要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
  • Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  • 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
  • 对象的类名、属性都会被序列化;而方法、static属性(静态属性)、transient属性(即瞬态属性)都不会被序列化
  • 要序列化的对象的引用属性也必须是可序列化的,否则该对象不可序列化,除非以transient关键字修饰该属性使其不用序列化。

参考

https://blog.csdn.net/ShuSheng0007/article/details/80629348
https://blog.csdn.net/zcl_love_wx/article/details/52126876
https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html

你可能感兴趣的:(java序列化相关)