Java 对象的序列化

Java 对象的序列化

引言: 通过对一个类实现 Serializable接口, 使得这个类可以被序列化以及反序列化。通过序列化一个对象, 是的这个对象可以转变为对应的字节流, 由于这个过程是基于 JVM 独立的, 因此在一台机器上被序列化的对象也可以在别的机器上再次进行反序列化。对象的序列化在很多情况下都是需要使用的, 比如:保存当前对象状态到文件或者数据库;将对象通过套接字传送到别的地方等。

将一个对象设置为可序列化很简单, 只需要实现 java.io.Serializable 接口即可。
比如, 以下的 Person 类实现了 java.io.Serializable接口, 它就是可序列化的。

import java.io.Serializable;

/**
 * 实现 java.io.Serializable 接口, 这个类是可以序列化的
 */
public class Person implements Serializable {
}

java.io.Serializable 是一个空接口, 没有任何需要实现的方法。

  1. 序列化ID
      JVM是否允许序列化, 不仅取决于类功能和类路径是否一致, 而且还取决与两个类的序列化ID是否一致。序列化 ID 的设置有两种方式:设置为1L和随机生成。通常, 将序列化ID设置为1L即可, 这样可以保证反序列化成功。但是随机化的序列化ID也有适用的场合。
      随机序列化ID适用场合:1
    Façade 模式,为应用程序提供统一的访问接口,如下图所示:
    在这里插入图片描述
    Client 端通过 Façade Object 才可以与业务逻辑对象进行交互。而客户端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通过网络将二进制对象数据传给 Client,Client 负责反序列化得到 Façade 对象。该模式可以使得 Client 端程序的使用需要服务器端的许可,同时 Client 端和服务器端的 Façade Object 类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的 Façade Object 类的序列化 ID 再次生成,当 Client 端反序列化 Façade Object 就会失败,也就是强制 Client 端从服务器端获取最新程序。

  2. 序列化对象中的静态变量。由于序列化保存的是对象的状态, 而不是类的状态, 因此对于类的静态变量的修改直接导致对象中的改变。如下面的示例所示:

/**
 * 实现 java.io.Serializable 接口, 这个类是可以序列化的
 */
public class Person implements Serializable {
    @Serial
    private static final long serialVersionUID = 1891161721856655565L;

    public static int sValue = 5;

    public static void main(String[] args) {
        try {
            /*
                将一个新的 Person 对象写入到文件中
             */
            ObjectOutputStream outputStream = new ObjectOutputStream(
                    new FileOutputStream("person.obj"));
            outputStream.writeObject(new Person());
            outputStream.close();
            // 写入对象到文件结束

            // 修改 Person 的静态变量 sValue 从 5 改为 10
            Person.sValue = 10;

            /*
                再次从之前写入的对象文件中读取写入的对象
             */
            ObjectInputStream inputStream = new ObjectInputStream(
                    new FileInputStream("person.obj"));
            Person person = (Person) inputStream.readObject();
            inputStream.close();
            // 读取写入对象结束

            // 打印读取的 Person 对象的静态变量的 sValue 的值
            System.out.println(person.sValue);
            /*
                结果应当为10, 这是因为静态变量是每个类共有的, 不是某个类特有的变量,
                因此任何对静态变量的修改都将直接导致所有对象的静态变量的改变。
                此外, 由于序列化对象保存的是对象的状态,也就是说, 序列化时并不保存静态变量的值
                因此在反序列化时也会重新获取这个静态变量的值。
             */
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 父类的序列化
      如果父类是实现了 java.io.Serializable 接口的话, 那么这不会有问题, 按照正常的序列化与反序列化进行操作即可。但是如果父类没有实现 java.io.Serializable 接口的话, 那么 JVM 将不会序列化父对象, 由于 java 类的初始化是由父类——>子类, 因此在这种情况下将会调用父类的无参构造函数。在这种情况下, 请在父类的无参构造函数中初始化父类的属性, 否则将会将父类的属性设置为默认值,如:int 属性 设置为 0, String 属性设置为 null。除了这种方式避免某些属性被序列化之外, 对于不想被序列化的属性, 可以在变量声明前加入 transient 关键字, 以避免该属性被序列化。
 /*
 	使用 transient 关键字修饰属性, 以避免该属性被序列化
*/
public transient String gender;

如果想要避免某些字段被序列化, 将父类不实现 java.io.Serializable, 在将属性放到父类中, 子类再继承父类, 再实现java.io.Serializable接口, 这是一个很好的解决方案。

  1. 自定义的 writeObject()readObject() 方法。
      在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。1, 基于这个原理, 我们可以在序列化和反序列化的过程中添加对指定字段的处理。比如:数据的加密和解密。
    示例如下:
import java.io.*;

import static java.io.ObjectInputStream.*;
import static java.io.ObjectOutputStream.*;

public class UserInfo implements Serializable {
    @Serial
    private static final long serialVersionUID = 8264901208656365184L;

    private String password = "123456";

    public String getPassword() {
        return password;
    }

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

    @Serial
    private void writeObject(ObjectOutputStream out) {
        try {
            PutField putField = out.putFields();
            System.out.println("原始密码:" + this.password);
            this.password = "password"; // 加密后的密码
            putField.put("password", this.password);
            System.out.println("加密后的密码:" + this.password);
            out.writeFields();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Serial
    private void readObject(ObjectInputStream in) {
        try {
            /*
                从 ObjectInputStream 中获取对象的所有属性名
             */
            GetField getField = in.readFields();
            /*
                从属性名中获取指定的属性,
                第一个参数为属性名, 第二个参数为这个属性的默认值
             */
            Object obj = getField.get("password", "");
            System.out.println("要解密的字符串:" + obj.toString());
            this.password = "123456"; // 解密后的密码
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("userInfo.obj"));
            UserInfo obj = new UserInfo();
            obj.setPassword("123456");
            out.writeObject(obj);
            out.close();

            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream("userInfo.obj"));
            UserInfo userInfo = (UserInfo) in.readObject();
            in.close();
            System.out.println("解密后的密码:" + userInfo.getPassword());
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述

  1. 序列化的存储规则。Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。1
      下面的一个示例将说明这个规则:
import lombok.Data;

import java.io.*;

@Data
public class  Book implements Serializable {
    @Serial
    private static final long serialVersionUID = -1517777911086472150L;

    private long isbn;

    private String name;

    public Book(long isbn, String name) {
        this.isbn = isbn;
        this.name = name;
    }
    
    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("book.obj"));
            /*
                创建的一个 Book 对象
             */
            Book obj1 = new Book(9787115388087L, "从零开始学炒股");
            // 第一次写入 Book 对象
            out.writeObject(obj1);
            out.flush();
            /*
                第一次写入 Book 对象后文件的总长度(字节)
             */
            System.out.println("file length: " + new File("book.obj").length());

            // 第二次写入 Book 对象
            out.writeObject(obj1);
            out.flush();
            /*
                第二次写入 Book 对象后文件的总长度(字节),
                按照一般的理解,在第二次写入后文件的总长度应当是第一次写入 Book 对象的两倍
             */
            System.out.println("file length: " + new File("book.obj").length());

            /*
                从保存的序列化对象中读取保存的对象
             */
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.obj"));
            Book book1 = (Book) in.readObject();
            System.out.println(book1.toString());

            Book book2 = (Book) in.readObject();
            System.out.println(book2.toString());

            // 判断读取的两个对象是否引用的是同一个对象
            System.out.println(book1 == book2);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
在这里, 第二次写入序列化的Book 对象之后, 文件的长度并没有变为原来的两倍, 这是由于第二次写入序列化的对象时,检测到了第一次的写入对象与这次的写入对象是一致的, 因此它在写入文件时只是新增了一份引用和一些控制信息, 从而节省磁盘空间。

值得注意的是, Java 在多次写入同一序列化的对象时, 只会保存第一次写入的对象信息。 因此, 在第一次写入序列化对象后, 即使修改了属性的值,再次写入文件也只会保存第一次的引用
  下面的示例展示了这个现象。

import lombok.Data;

import java.io.*;

@Data
public class  Book implements Serializable {
    @Serial
    private static final long serialVersionUID = -1517777911086472150L;

    private long isbn;

    private String name;

    public Book(long isbn, String name) {
        this.isbn = isbn;
        this.name = name;
    }

    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("book.obj"));
            /*
                创建的一个 Book 对象
             */
            Book obj1 = new Book(9787115388087L, "从零开始学炒股");
            /*
                第一次写入 Book 对象到文件
             */
            out.writeObject(obj1);
            out.flush();
            System.out.println("file length: " + new File("book.obj").length());
            /*
                修改 Book 对象的 name 属性为 "数学之美 第三版"
             */
            obj1.setName("数学之美 第三版");
            /*
                将修改了 name 属性的 Book 对象再次写入文件
             */
            out.writeObject(obj1);
            out.flush();
            System.out.println("file length: " + new File("book.obj").length());

            ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.obj"));
            /*
                第一次读取的 Book 对象
             */
            Book book1 = (Book) in.readObject();
            System.out.println(book1.toString());

            /*
                第二次读取的 Book 对象。
                按照一般的理解, 这里读取的 Book 对象的
                name 属性的值应当是 "数学之美 第三版"
             */
            Book book2 = (Book) in.readObject();
            System.out.println(book2.toString());

            System.out.println(book1 == book2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:
在这里插入图片描述因此, 在将序列化对象写入到文件中时, 需要尤其注意这个问题。


  1. https://developer.ibm.com/zh/articles/j-lo-serial/ ↩︎ ↩︎ ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值