文章目录
一、简介
对象序列化的目的是将对象保存到磁盘, 或者允许在网络中传输。相对的, 反序列化就是根据磁盘中保存的文件恢复对象。
要让某个类的对象能够序列化, 需要实现下面2个接口之一:
-Serializable
-Externalizable
二、对象流实现序列化
通过实现Serializable接口让一个类的对象可序列化的步骤十分简单, 仅仅是在声明一个类时声明实现该接口即可, 无需实现任何方法
1. 成员变量是基本类型
// 用于测试的对象
class Animal implements Serializable {
private String name;
private double weight;
public Animal(String name, double weight) {
this.name = name;
this.weight = weight;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", weight=" + weight +
'}';
}
}
将一个对象输出到磁盘, 可使用对象流 java.io.ObjectOutputStream
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
Animal cat = new Animal("cat", 10.2);
out.writeObject(cat);
}
将一个对象从磁盘恢复可使用对象流 java.io.ObjectInputStream
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
Animal cat = (Animal)ois.readObject();
System.out.println(cat.toString());
}
// 输出:
Animal{name='cat', weight=10.2}
实现Serializable接口的对象反序列化通过构造器初始化对象?
当从输入流读取对象时, 并没有看到程序调用构造器, 因此反序列化并不需要通过构造器来初始化对象。
代码:略.
2.成员变量是引用类型
若一个类的成员变量是引用类型, 那么该成员变量也必须是可序列化的
// 用于测试的类1
class Animal implements Serializable {
private String name;
private double weight;
private Friend friend;
public Animal(String name, double weight, Friend friend) {
this.name = name;
this.weight = weight;
this.friend = friend;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", weight=" + weight +
", friend=" + friend +
'}';
}
}
// 用于测试的类2
class Friend implements Serializable{
private String name;
public Friend(String name) {
this.name = name;
}
@Override
public String toString() {
return "Friend{" +
"name='" + name + '\'' +
'}';
}
}
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Friend friend = new Friend("Ber");
Animal cat = new Animal("cat", 10.2, friend);
// 将对象写入输出流
out.writeObject(cat);
}
从输入流中恢复对象的代码没有变化
// 输出结果
Animal{name='cat', weight=10.2, friend=Friend{name='Ber'}}
3. 序列化多个对象
当保存多个对象并且恢复时, 读取的顺序与保存的顺序需要一致, 而且可以看到, 用于读取对象的方法readObject()
并没有参数。
// 多个对象序列化
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Friend friend = new Friend("Ber");
Animal cat = new Animal("cat", 10.2, friend);
// 将对象1写入输出流
out.writeObject(cat);
// 将对象2写入输出流
out.writeObject(friend);
}
// 反序列化
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
// 从输入流获取对象1
Animal cat = (Animal)ois.readObject();
// 从输入流获取对象2
Friend friend = (Friend) ois.readObject();
System.out.println(cat.toString());
System.out.println(friend.toString());
}
// 输出:
Animal{name='cat', weight=10.2, friend=Friend{name='Ber'}}
Friend{name='Ber'}
当一个对象被多次序列化时, 是否会输出多个对象?
当多次对同一个对象序列化时, 只有首次的序列化将表示对象的字节序列写到输出流, 后面在进行序列化时, 被写入输出流的只是序列化编号, 从而保证是从字节序列恢复的是同一个对象。
// 序列化代码片段
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Friend friend = new Friend("Ber");
// 将对象写入输出流
out.writeObject(friend);
// 将对象再次写入输出流
out.writeObject(friend);
}
// 反序列化代码片段
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
// 从输入流获取对象1
Friend friend = (Friend) ois.readObject();
// 从输入流获取对象2
Friend friend2 = (Friend) ois.readObject();
System.out.println(friend == friend2);
}
// 输出:
true
当一个对象被列化后, 修改该对象属性并再次序列化, 那么反序列化的结果是什么?
反序列化的结果都是首次序列化的属性值, 而这也符合了多次序列化对象的规则: 只有对象的首次序列化才会将表示对象的字节流写入输出流, 后面的调用只会写入序列化编号。
// 对象序列化片段
// 创建对象
Friend friend = new Friend("Ber");
// 将对象写入输出流
out.writeObject(friend);
//修改对象属性值并写入输出流
friend.setName("Hos");
out.writeObject(friend);
// 对象反序列化片段
// 从输入流获取对象
Friend friend = (Friend) ois.readObject();
Friend friend2 = (Friend) ois.readObject();
System.out.println(friend.toString());
System.out.println(friend2.toString());
// 输出:
Friend{name='Ber'}
Friend{name='Ber'}
三、自定义序列化(一)
1. transient关键字
在默认情况下, 将对象序列化时会将其所有的实例变量进行实例化。 若成员变量是引用类型, 则引用的对象也会被实例化。 假设想要忽略对某个对实例变量的序列化, 可使用transient关键字。
// 用于测试的类
class Friend implements Serializable{
private String name;
private transient int age;
public Friend(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Friend{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// 对象序列化片段
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Friend friend = new Friend("Ber", 20);
// 将对象写入输出流
out.writeObject(friend);
}
/ / 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
// 从输入流获取对象
Friend friend = (Friend) ois.readObject();
System.out.println(friend.toString());
}
输出:
Friend{name='Ber', age=0}
可以看到, 被transient修饰的成员变量age在反序列化后并不是序列化时的值。
2. 目标类提供writeObject、readObject方法
method | desc |
---|---|
private void readObject(ObjectInputStream in) | 从输入流中读取对象 |
private void writeObject(ObjectOutputStream out) | 将对象写到输出流 |
private void readObjectNoData() | 当序列化流不完整, 正确的初始化反序列化对象 |
class Friend implements Serializable{
private String name;
private transient int age;
public Friend(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
序列化和反序列化的代码与3.1相同, 没有变化。
首先, 通过提供writeObject方法, 我们可以决定序列化哪些变量, 以及如何序列化变量。
其次, 提高了在通过网络传输字节化序列时的安全性。
注意:
(1). readObject恢复实例变量的顺序必须与writeObject存储实例变量一致。
(2). readObject恢复实例变量的方式必须与writeObject存储实例变量的方式相对应. 在上例中:
writeObject序列化String类型变量: String —> StringBuffer —> StringBuffer.reverse
readObject反序列化String类型变量: (StringBuffer)Object —> StringBuffer.reverse —> toString
3. 目标类提供writeReplace方法
使用该方法时, 会将原本将要序列化的对象改为对另一个对象的序列化。
Java序列化保护机制保证: 在序列化对象之前, 先调用该对象的writeReplace()方法, 若该方法返回的是另一个Java对象, 则转为序列化该对象。
// 用于测试的类
class Friend implements Serializable {
private String name;
private transient int age;
public Friend(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() {
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
// 序列化对象代码
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Friend friend = new Friend("Ber", 50);
// 将对象写入输出流
out.writeObject(friend);
}
// 反序列化代码
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
// 从输入流获取对象
ArrayList list = (ArrayList)ois.readObject();
System.out.println(list);
}
// 输出:
[Ber, 50]
可以看到, 在将对象序列化代码中无需改变, 但是在反序列化时需要将对象转为与writeReplace方法中定义的类型(ArrayList)一致。因此可以知道, 对原对象的序列化实际上转为了对ArrayList对象的序列化。
4. 目标类提供readResolve()方法
writeReplace方法是在序列化时替换序列化的对象, 那么与之相对的, readResolve()方法的返回值会替换原本反序列化的对象, 因为readResolve()会在调用readObject()时被调用。
// 用于测试的bean
class Friend implements Serializable {
private String name;
private transient int age;
public Friend(String name, int age) {
this.name = name;
this.age = age;
}
// 提供readResolve方法
private Object readResolve() {
return String.valueOf("I am a String");
}
@Override
public String toString() {
return "Friend{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// 序列化对象
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Friend friend = new Friend("Ber", 50);
// 将对象写入输出流
out.writeObject(friend);
}
// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
// 从输入流获取对象
Object object = ois.readObject();
System.out.println(object.toString());
}
// 输出:
I am a String
根据例子可知, Friend对象反序列化后返回的对象是readResolve()中定义的String对象。
注意:
与writeReplace()方法相似, readResolve()方法可以使用任意的访问控制符。 假如readResolve()不使用 private 或final 修饰, 那么就会有被子类继承的风险, 也就意味着假如子类继承了这个方法且没有重写, 那么对子类反序列化时将会得到一个父类对象。 但总是让子类重写方法也是一个负担, 若是需要重写, 则最好使用private或final修饰。
四、自定义序列化(二)
实现Externalizable接口
与实现Serializable接口不同的是, 实现Externalizable接口需要做2件事:
(1) 实现 writeExternal(ObjectOutput out) 和 readExternal(ObjectInput in) 方法。
(2) 为该类提供无参数构造器, 无参数构造器在反序列化时被使用到。
除此之外, 序列化和反序列化的代码都是调用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject()方法完成。
class Person implements Externalizable {
private String name;
private int age;
public Person() {
System.out.println("无参构造器调用");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 实现接口方法
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
// 实现接口方法
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// 序列化代码
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
// 创建对象
Person person = new Person("Ber", 50);
// 将对象写入输出流
out.writeObject(person);
}
// 反序列化代码
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
// 从输入流获取对象
Person person = (Person) ois.readObject();
System.out.println(person.toString());
}
输出:
无参构造器调用
Person{name='Ber', age=50}
Serializable和Externalizable接口不同之处:
Serializable | Externalizable |
---|---|
自动存储对象信息 | 自定义存储对象的信息 |
易于实现, 只需声明实现该接口即可 | 需要实现接口的方法、提供无参构造器 |
性能略差 | 性能略好 |
Externalizable接口虽然能带来性能上的提升, 但是也增加了编程的复杂度, 因此大多数情况下使用实现Serializable的方法。
五、小结
- 对象的类名和成员变量都会被序列化, 方法、静态变量、transient修饰的成员变量不会被序列化。
- 实现Serializable接口的类可以使用transient修饰成员变量, 让该成员变量不会被序列化, 虽然使用static修饰成员变量也能达到这个效果, 但是不推荐这样使用。
- 要进行序列化的对象的成员变量也必须是可序列化的, 假如该成员变量是不可序列化的, 需要使用transient修饰, 否则该类对象时不可序列化的。
- 反序列化必须要有序列化对象的class文件
- 当通过文件、网络来进行反序列化时, 必须按实际写入的顺序读取。
六、序列化版本
当进行对象反序列化时需要提供对象的class文件, 那么当class文件的版本升级时, 如何保证两个class文件的兼容性?
通过在序列化类显式的提供一个private static final long serialVersionUID值。 这个值用于标识java类的序列化版本, 也就是说, 即使一个类升级后, 只要它的serialVersionUID不变, 序列化机制也会把它们当成同一版本。
该值获取方法:
(1) 这个值可以自己定
(2) 使用bin目录下的serialver.exe工具来获取该值. 命令的格式为serialver 类名:
(3)通过该开发工具生成
IDEA如何自动生成 serialVersionUID 的设置
假如不显式的提供serialVersionUID会发生什么情况?
(1) 该值由JVM根据类的信息进行计算, 一个类修改后计算出的值往往会发生变化, 造成反序列化因为类版本不兼容而失败。
(2) 不利于在不同的JVM进行移植, 不同的JVM可能会有不同的计算策略, 因此一个类虽然没有改变, 但是会因为序列化版本不兼容造成无法反序列化的现象。
假如一个类的修改导致了反序列化失败, 则应该重新分配serialVersionUID的值。