1,Java 序列化介绍
序列化是指对象通过写出描述自己状态的数值来记录自己的过程, 即将对象表示成一系列有序字节, Java 提供了将对象写入流和从流中恢复对象的方法. 对象能包含其它的对象, 而其它的对象又可以包含另外的对象. Java 序列化能够自动的处理嵌套的对象. 对于一个对象的简单域, writeObject() 直接将其值写入流中. 当遇到一个对象域时, writeObject() 被再次调用, 如果这个对象内嵌另一个对象, 那么, writeObject() 又被调用, 直到对象能被直接写入流为止. 程序员所需要做的是将对象传入 ObjectOutputStream 的 writeObject() 方法, 剩下的将有系统自动完成.
要实现序列化的类必须实现的 java.io.Serializable 或 java.io.Externalizable 接口, 否则将产生一个 NotSerializableException. 该接口内部并没有任何方法, 它只是一个 "tagging interface", 仅仅 "tags" 它自己的对象是一个特殊的类型. 类通过实现 java.io.Serializable 接口以启用其序列化功能. 未实现此接口的类将无法使其任何状态序列化或反序列化. 可序列化类的所有子类型本身都是可序列化的. 序列化接口没有方法或字段, 仅用于标识可序列化的语义. Java 的 "对象序列化" 能让你将一个实现了 Serializable 接口的对象转换成一组 byte, 这样日后要用这个对象时候, 你就能把这些 byte 数据恢复出来, 并据此重新构建那个对象了.
2, 序列化必要性及目的
Java 中, 一切都是对象, 在分布式环境中经常需要将 Object 从这一端网络或设备传递到另一端. 这就需要有一种可以在两端传输数据的协议. Java 序列化机制就是为了解决这个问题而产生.
Java 序列化支持的两种主要特性:
Java 的 RMI 使本来存在于其他机器的对象可以表现出就象本地机器上的行为.
将消息发给远程对象时, 需要通过对象序列化来传输参数和返回值
Java 序列化的目的 (我目前能理解的):
支持运行在不同虚拟机上不同版本类之间的双向通讯;
提供对持久性和 RMI 的序列化;
3, 关于序列化的一些例子
下面我们通过一个简单的例子来看下 Java 默认支持的序列化. 我们先定义一个类, 然后将其序列化到文件中, 最后读取文件重新构建出这个对象. 在序列化一个对象的时候, 有几点需要注意下:
当一个对象被序列化时, 只序列化对象的非静态成员变量, 不能序列化任何成员方法和静态成员变量.
如果一个对象的成员变量是一个对象, 那么这个对象的数据成员也会被保存.
如果一个可序列化的对象包含对某个不可序列化的对象的引用, 那么整个序列化操作将会失败, 并且会抛出一个 NotSerializableException. 可以通过将这个引用标记为 transient, 那么对象仍然可以序列化. 对于一些比较敏感的不想序列化的数据, 也可以采用该标识进行修饰.
下面我们先通过一个简单的例子来看一下 Java 内置的序列化过程.
- class SuperClass implements Serializable{
- private String name;
- private int age;
- private String email;
- public String getName() {
- return name;
- }
- public int getAge() {
- return age;
- }
- public String getEmail() {
- return email;
- }
- public SuperClass(String name,int age,String email) {
- this.name=name;
- this.age=age;
- this.email=email;
- }
- }
复制代码
下面我们来看下 main 方法里面的序列化过程, 代码如下:
- public static void main(String[] args) throws IOException,ClassNotFoundException {
- System.out.println("序列化对象开始!");
- SuperClass superClass=new SuperClass("gong",27, "1301334028@qq.com");
- File rootfile=new File("C:/data");
- if(!rootfile.exists()) {
- rootfile.mkdirs();
- }
- File file=new File("C:/data/data.txt");
- if(!file.exists()) {
- file.createNewFile();
- }
- FileOutputStream fileOutputStream=new FileOutputStream(file);
- ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
- objectOutputStream.writeObject(superClass);
- objectOutputStream.flush();
- objectOutputStream.close();
- System.out.println("序列化对象完成!");
- System.out.println("反序列化对象开始!");
- FileInputStream fileInputStream=new FileInputStream(new File("C:\\data\\data.txt"));
- ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
- SuperClass getObject=(SuperClass) objectInputStream.readObject();
- System.out.println("反序列化对象数据:");
- System.out.println("name:"+getObject.getName()+"\nage:"+getObject.getAge()+"\nemail:"+getObject.getEmail());
- }
复制代码
代码运行结果如下:
序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
- name:gong
- age:27
- email:1301334028@qq.com
复制代码
通过上面的例子, 我们看到 Java 默认提供了序列化与反序列化机制, 对于单个实体类来说, 整个过程都是自动完成的, 无需程序员进行额外的干预. 如果我们想让某些关键的域不参与序列化过程呢? Java 提供了方法, 接着往下看.
transient 关键字与序列化
如果我们现在想让上面 SuperClass 类走 age 和 email 不参与序列化过程, 那么只需要在其定义前面加上 transient 关键字即可:
- private transient int age;
- private transient String email;
复制代码
这样我们在进行序列化的时候, 字节流中不不包含 age 和 email 的数据的, 反序列的时候会赋予这两个变量默认值. 还是运行刚才的工程, 这时候我们结果如下:
序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
- name:gong
- age:0
- email:null
复制代码
自定义序列化过程
如果默认的序列化过程不能满足需求, 我们也可以自定义整个序列化过程. 这时候我们只需要在需要序列化的类中定义 writeObject 方法和 readObject 方法即可. 我们还是以 SuperClass 为例, 现在我们添加自定义的序列化过程, transient 关键字让 Java 内置的序列化过程忽略修饰的变量, 我们通过自定义序列化过程, 还是序列化 age 和 email, 我们来看看改动后的结果:
- private String name;
- private transient int age;
- private transient String email;
- public String getName() {
- return name;
- }
- public int getAge() {
- return age;
- }
- public String getEmail() {
- return email;
- }
- public SuperClass(String name,int age,String email) {
- this.name=name;
- this.age=age;
- this.email=email;
- }
- private void writeObject(ObjectOutputStream objectOutputStream)
- throws IOException {
- objectOutputStream.defaultWriteObject();
- objectOutputStream.writeInt(age);
- objectOutputStream.writeObject(email);
- }
- private void readObject(ObjectInputStream objectInputStream)
- throws ClassNotFoundException,IOException {
- objectInputStream.defaultReadObject();
- age=objectInputStream.readInt();
- email=(String)objectInputStream.readObject();
- }
复制代码
运行结果如下:
反序列化对象数据:
- name:gong
- age:27
- email:1301334028@qq.com
复制代码
我们看到, 执行结果和默认的结果是一致的, 我们通过自定义序列化机制, 修改了默认的序列化过程 (让 transient 关键字失去了作用).
注意:
细心的同学可能发现了我们在自定义序列化的过程中调用了 defaultWriteObject() 和 defaultReadObject() 方法. 这两个方法是默认的序列化过程调用的方法. 如果我们自定义序列化过程仅仅调用了这两个方法而没有任何额外的操作, 这其实和默认的序列化过程没任何区别, 大家可以试一下.
4, 存在继承关系下的序列化
子类支持序列化, 超类不支持序列化
我们先来了解这一种情况, 如果子类支持序列化, 父类不支持序列化, 那么我们在序列化子类实例的时候必须显式的保存父类的状态. 我们将前面的例子稍作修改:
- class SuperClass{
- protected String name;
- protected int age;
- public String getName() {
- return name;
- }
- public int getAge() {
- return age;
- }
- public SuperClass(String name,int age) {
- this.name=name;
- this.age=age;
- }
- }
- class DeriveClass extends SuperClass implements Serializable{
- private String email;
- private String address;
- public DeriveClass(String name,int age,String email,String address) {
- super(name,age);
- this.email=email;
- this.address=address;
- }
- public String getEmail() {
- return email;
- }
- public String getAddress() {
- return address;
- }
- private void writeObject(ObjectOutputStream out) throws IOException {
- out.defaultWriteObject();
- out.writeObject(name);
- out.writeInt(age);
- }
- private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
- in.defaultReadObject();
- name=(String)in.readObject();
- age=in.readInt();
- }
- @Override
- public String toString() {
- return "name:"+getName()+"\nage:"+getAge()+"\nemail:"+getEmail()+"\naddress"+getAddress();
- }
- }
复制代码
main 方法我们修改为序列化子类对象即可:
- DeriveClass superClass=new DeriveClass("gong",27,"1301334028@qq.com","NJ");
- DeriveClass getObject=(DeriveClass) objectInputStream.readObject();
- System.out.println("反序列化对象数据:");
- System.out.println(getObject);
复制代码
运行代码发现报错了, 报错如下:
- Exception in thread "main" java.io.InvalidClassException: com.learn.example.DeriveClass; no valid constructor
- at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
- at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
- at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
- at java.io.ObjectInputStream.readObject0(Unknown Source)
- at java.io.ObjectInputStream.readObject(Unknown Source)
- at com.learn.example.RunMain.main(RunMain.java:88)
复制代码
我们来仔细分析下, 为什么会这样. DeriveClass 支持序列化, 其父类不支持序列化, 所以这种情况下, 子类在序列化的时候需要额外的序列化父类的域 (如果有这个需要的话). 那么在反序列的时候, 由于构建 DeriveClass 实例的时候需要先调用父类的构造函数, 然后才是自己的构造函数. 反序列化时, 为了构造父对象, 只能调用父类的无参构造函数作为默认的父对象, 因此当我们取父对象的变量值时, 它的值是调用父类无参构造函数后的值. 如果你考虑到这种序列化的情况, 在父类无参构造函数中对变量进行初始化. 或者在 readObject 方法中进行赋值.
来源: https://juejin.im/post/5b56f7b75188256256697281