序列化与反序列化
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。
Java对象的序列化与反序列化
在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。
但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
Java对象序列化与反序列化相关接口及类
Java为了方便开发人员将Java对象进行序列化及反序列化,专门提供了一套方便的API来支持。其中包括以下接口和类:
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
Serializable 接口
类通过实现 java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出 NotSerializableException
。
虽然Serializable接口中并没有定义任何属性和方法,但是如果一个类想要具备序列化能力也比必须要实现它。其实,主要是因为序列化在真正的执行过程中会使用instanceof判断一个类是否实现类Serializable,如果未实现则直接抛出异常。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成java.io.Serializable
接口。
实例:User对象实现java.io.Serializable
接口
package com.fei8meng.top.util;
import java.io.Serializable;
/**
* ━━━━━━如来保佑━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┻ ┃
* ┗━┓ ┏━┛
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┗┻┛ ┗┻┛
* ━━━━━━永无BUG━━━━━━
*
* @ClassName User
* @Author Eflying
* @Description User类实现了Serializable序列化接口
* @Date 2019/11/28
* @Version 1.0
**/
public class User implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
将User对象进行序列化及反序列化
package com.fei8meng.top.util;
import java.io.*;
/**
* ━━━━━━如来保佑━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┻ ┃
* ┗━┓ ┏━┛
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┗┻┛ ┗┻┛
* ━━━━━━永无BUG━━━━━━
*
* @ClassName SerializableUser
* @Author Eflying
* @Description 将User对象进行序列化及反序列化
* @Date 2019/11/28
* @Version 1.0
**/
public class SerializableUser {
public static void main(String[] args) {
User user = new User();
user.setName("eflying");
user.setAge(18);
System.out.println(user);
try {
//将user对象序列化到文件中
FileOutputStream fos = new FileOutputStream("userFile");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(user);
} catch (Exception e) {
e.printStackTrace();
}
try {
//从文件中反序列化user对象
File file = new File("userFile");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
User user1 = (User) ois.readObject();
System.out.println(user1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果
User{name='eflying', age=18}
User{name='eflying', age=18}
分析:
上面的代码中,我们将代码中定义出来的User对象通过序列化的方式保存到文件中,然后再从文件中将他到序列化成Java对象。结果是我们的对象的属性均被持久化了下来。
注意:测试代码中使用了IO流,但是并没有显式的关闭IO流。这其实是Java 7中的新特性try-with-resources。这其实是Java中的一个语法糖,背后原理其实是编译器帮我们做了关闭IO流的工作。
Externalizable接口
除了Serializable 之外,java中还提供了另一个序列化接口Externalizable
通过下面代码来了解Externalizable接口和Serializable接口的区别:
package com.fei8meng.top.util;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* ━━━━━━如来保佑━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┻ ┃
* ┗━┓ ┏━┛
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┗┻┛ ┗┻┛
* ━━━━━━永无BUG━━━━━━
*
* @ClassName User1
* @Author Eflying
* @Description User1类实现了Externalizable接口
* @Date 2019/11/28
* @Version 1.0
**/
public class User1 implements Externalizable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
@Override
public String toString() {
return "User1{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
package com.fei8meng.top.util;
import java.io.*;
/**
* ━━━━━━如来保佑━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┻ ┃
* ┗━┓ ┏━┛
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┗┻┛ ┗┻┛
* ━━━━━━永无BUG━━━━━━
*
* @ClassName ExternalizableUser1
* @Author Eflying
* @Description 对一个实现了Externalizable接口的User1类进行序列化及反序列化
* @Date 2019/11/28
* @Version 1.0
**/
public class ExternalizableUser1 {
public static void main(String[] args) {
User1 user1 = new User1();
user1.setName("eflying");
user1.setAge(18);
System.out.println(user1);
try {
//将user1对象序列化到文件中
FileOutputStream fos = new FileOutputStream("user1File");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(user1);
} catch (Exception e) {
e.printStackTrace();
}
try {
//从文件中反序列化user1对象
File file = new File("user1File");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
user1 = (User1) ois.readObject();
System.out.println(user1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
User1{name='eflying', age=18}
User1{name='null', age=0}
分析:
通过上面的实例的输出结果可以发现,对User1类进行序列化及反序列化之后得到的对象的所有属性的值都变成
了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和Serializable接口的区别。
Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。
当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。
User2类实现了Externalizable接口,并重写writeExternal和readExternal方法:
package com.fei8meng.top.util;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* ━━━━━━如来保佑━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┻ ┃
* ┗━┓ ┏━┛
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┗┻┛ ┗┻┛
* ━━━━━━永无BUG━━━━━━
*
* @ClassName User2
* @Author Eflying
* @Description User2类实现了Externalizable接口,并重写了writeExternal和readExternal方法
* @Date 2019/11/28
* @Version 1.0
**/
public class User2 implements Externalizable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "User2{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
package com.fei8meng.top.util;
import java.io.*;
/**
* ━━━━━━如来保佑━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┻ ┃
* ┗━┓ ┏━┛
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┗┻┛ ┗┻┛
* ━━━━━━永无BUG━━━━━━
*
* @ClassName ExternalizableUser2
* @Author Eflying
* @Description 对一个实现了Externalizable接口的User2类进行序列化及反序列化
* @Date 2019/11/28
* @Version 1.0
**/
public class ExternalizableUser2 {
public static void main(String[] args) {
User2 user2 = new User2();
user2.setName("eflying");
user2.setAge(18);
System.out.println(user2);
try {
//将user1对象序列化到文件中
FileOutputStream fos = new FileOutputStream("user2File");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(user2);
} catch (Exception e) {
e.printStackTrace();
}
try {
//从文件中反序列化user1对象
File file = new File("user2File");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
user2 = (User2) ois.readObject();
System.out.println(user2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
User2{name='eflying', age=18}
User2{name='eflying', age=18}
分析:
通过运行结果可以看出,这次将之前的对象状态持久化下来
ObjectOutput和ObjectInput 接口
上面的writeExternal方法和readExternal方法分别接收ObjectOutput和ObjectInput类型参数。
这两个类作用如下:
ObjectInput 扩展自 DataInput 接口以包含对象的读操作
DataInput 接口用于从二进制流中读取字节,并根据所有 Java 基本类型数据进行重构。同时还提供根据 UTF-8
修改版格式的数据重构 String 的工具。
对于此接口中的所有数据读取例程来说,如果在读取所需字节数之前已经到达文件末尾 (end of file),则将抛
出 EOFException(IOException 的一种)。如果因为到达文件末尾以外的其他原因无法读取字节,则将抛出
IOException 而不是 EOFException。尤其是,在输入流已关闭的情况下,将抛出 IOException。
ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作
DataOutput 接口用于将数据从任意 Java 基本类型转换为一系列字节,并将这些字节写入二进制流。同时还提
供了一个将 String 转换成 UTF-8 修改版格式并写入所得到的系列字节的工具。
对于此接口中写入字节的所有方法,如果由于某种原因无法写入某个字节,则抛出 IOException。
ObjectOutputStream、ObjectInputStream类
通过上面实例代码知道,一般使用ObjectOutputStream的writeObject
方法把一个对象进行持久化。再使用ObjectInputStream的readObject
从持久化存储中把对象读取出来。
transient 关键字
transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
序列化ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID
)
序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。