文章目录
- 一、序列化的定义
- 二、实现序列化的方式
- 1. Serializable
- 父类与子类的序列化关系
- 成员是引用的序列化机制
- 同一对象多次序列化机制
- 潜在问题
- 自定义序列化
- 2. Externalizable
- 3. 两种序列化对比
- 序列化版本号
- 总结
一、序列化的定义
- 序列化:将对象写入到 IO 流中
- 反序列化:从 IO 流中恢复对象
- 使用场景:将对象存入数据库或文件时,在网络通信时传输序列化后的对象时
二、实现序列化的方式
如果一个类想要实现序列化,那它就要实现Serializable或者Externalizable两个接口中的一个。这里放个 User 类 用来序列化:
User 类
package com.grh;
import java.io.Serializable;
public class User implements Serializable {
private String name;
private String password;
//get/set省略
public User() {
System.out.println("一个对象被创建了");
}
public User(String name, String password) {
System.out.println("一个对象被创建了");
this.name = name;
this.password = password;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
1. Serializable
相信对序列化好奇的人都点开过他的源码,然而他的源码是空的,只是一个没有任何函数和属性的接口:
public interface Serializable {
}
那么实现这个接口有什么用呢?
它只是个标记接口,用来提醒 JVM 这个类可以序列化,也就是说, Java 将类是否实现序列化的选择交给了程序员。
我们看个例子,它将 User 类进行序列化,并写入了 User.txt 文件中,然后再从中读出来进行输出。
Test 测试类
package com.grh;
import org.junit.Test;
import java.io.*;
public class serialize {
@Test
public void serializeBySerializable() {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));
User user = new User("张三","123");
oos.writeObject(user);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));
User user1 = (User)ois.readObject();
ois.close();
System.out.println(user1);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
一个对象被创建了
User{name='张三', password='123'}
我们打开项目所在位置,会发现多了一个 User.txt 文件,里面的内容如下
这就是通过字节流序列化后的 User 对象,我们可以看到他的类名:com.grh.User ,也就是说,序列化会将该对象的类的信息也存进去。
还有一个地方值得注意,输出结果中显示只调用了一次构造函数,也就是通过 new 创建对象时创建的,也就是说,JVM 有自己的方式反序列化来创建对象,而不需要通过构造函数。
父类与子类的序列化关系
- 如果一个类的父类实现了序列化,那么这个类也可以被实例化
- 如果一个子类实现了序列化,但父类没有实现,那么将子类反序列化时,会调用父类的无参构造函数构建父类对象,而且父类中的数据都是默认值,如 int 为 0 ,对象为 null。
我们定义一个 Person 类,并用 User 类继承它。
package com.grh;
import java.io.Serializable;
public class Person {
Person(){
System.out.println("构建一个person");
}
}
再次调用会得到一下结果:
构建一个person
一个对象被创建了
构建一个person
User{name='张三', password='123'}
我们可以看到父类 Person 的构造函数调用了两次,一次是 new 的时候,一次是反序列化 user 的时候。
成员是引用的序列化机制
如果一个可序列化的类的成员不是基本类型,也不是 String 类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
同一对象多次序列化机制
我们去掉 User 和 Person 的继承关系,将 User 作为 Person 中的一个成员,再加个年龄的成员,Person 类如下
package com.grh;
import java.io.Serializable;
public class Person implements Serializable{
private int age;
private User user;
public Person(){ }
public Person(int age, User user) {
this.age = age;
this.user = user;
}
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}
再写一个测试类,注意:反序列化的顺序与序列化时的顺序一致:
@Test
public void serializeBySerializable() {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));
User setUser = new User("张三","123");
Person setPerson = new Person(18,setUser);
Person setPerson1 = new Person(25,setUser);
oos.writeObject(setUser);
oos.writeObject(setPerson);
oos.writeObject(setPerson1);
oos.writeObject(setPerson1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));
User getUser = (User)ois.readObject();
Person getPerson = (Person)ois.readObject();
Person getPerson1 = (Person)ois.readObject();
Person getPerson2 = (Person)ois.readObject();
ois.close();
System.out.println(getPerson==getPerson2); //false
System.out.println(getPerson.getUser()==getPerson1.getUser()); //true
System.out.println(getUser==getPerson1.getUser()); //true
System.out.println(getUser==getPerson2.getUser()); //true
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
可以看到,不管怎么存 user ,读出来时也都是同一个,而年龄不同的 person 是不同的对象,读出来时也是不同的。
Java 序列化同一对象,并不会将此对象序列化多次得到多个对象。
潜在问题
每个序列化的对象都有一个编号,如果多次序列化,只会重复这个编号,并不会重新序列化。
因此,如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,只是保存序列化编号,会导致更改无效化。如:
@Test
public void serializeBySerializable() {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));
User setUser = new User("张三","123");
oos.writeObject(setUser);
setUser.setName("王五");
oos.writeObject(setUser);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));
User getUser = (User)ois.readObject();
ois.close();
System.out.println(getUser);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
输出结果:
User{name='张三', password='123'}
可以看到修改名字为王五并没有实现。
自定义序列化
- static 不会被序列化,因为序列化存的是对象,static 所代表的是类
- 在成员前加上 transient 关键字,就不会对这个成员进行序列化,反序列化时还会有这个成员,不过会赋值默认值。
- 重写 writeObject 与 readObject 方法,就是修改序列化的规则,比 transient 更灵活,还可以对数据进行加密,提高安全性。注意,writeObject 与 readObject 方法需要对应,怎么进行序列化,就应该怎样反序列化。如:
public class Person implements Serializable {
private String name;
private int age;
//省略构造方法,get及set方法
private void writeObject(ObjectOutputStream out) throws IOException {
//将名字反转写入二进制流
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
}
private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {
//将读出的字符串反转恢复回来
this.name = ((StringBuffer) ins.readObject()).reverse().toString();
this.age = ins.readInt();
}
}
- 重写 writeReplace 和 readResolve 方法,这两个方法会在写入之前调用,一般用于替换掉要写入或读取的对象。
package com.grh;
import java.io.*;
import java.util.ArrayList;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<Object>(2);
list.add(this.name);
list.add(this.age);
return list;
}
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
Person person = new Person("张三", 23);
oos.writeObject(person);
ArrayList list = (ArrayList) ios.readObject();
System.out.println(list);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//输出结果:
//[张三, 23]
在上面的例子中,本来写入的是 Person,但换成了 list,读取出来的时候也可以看到已经是 list 类型而不是 Person 类型了。
package com.grh;
import java.io.*;
import java.util.HashMap;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
private Object readResolve() throws ObjectStreamException {
return new Person("李四", 23);
}
public static void main(String[] args){
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
Person person1 = new Person("张三", 23);
Person person2 = new Person("王五", 23);
oos.writeObject(person1);
oos.writeObject(person2);
Person person3 = (Person) ios.readObject();
Person person4 = (Person) ios.readObject();
System.out.println(person3);
System.out.println(person4);
System.out.println(person3==person4);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在这个类中,无论序列化了多少个对象进入文件中,当他读出的时候,始终会调用 readResolve() 方法 new 一个李四的对象出来,序列化中的数据已经不重要了。
一般来说,重写 readResolve() 主要用于防止破坏单例模式的规则,因为反序列化后会重新构建一个一样的对象,如果在 readResolve() 方法中直接返回已经存在的单例对象,反序列化后就不会生成对象了。
注:writeReplace() 在writeObject前调用,readResolve() 在 readObject() 后调用。
2. Externalizable
Externalizable接口中,有两个方法,writeExternal() 和 readExternal() ,因此要想实现这个接口来序列化,必须实现这两个函数
package com.grh;
import java.io.*;
public class ExPerson implements Externalizable {
private String name;
private int age;
//注意,必须加上pulic 无参构造器
public ExPerson() {
System.out.println("无参构造函数构建对象");
}
public ExPerson(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "ExPerson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void writeExternal(ObjectOutput out) throws IOException {
//将name反转后写入二进制流
StringBuffer reverse = new StringBuffer(name).reverse();
System.out.println(reverse.toString());
out.writeObject(reverse);
out.writeInt(age);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//将读取的字符串反转后赋值给name实例变量
this.name = ((StringBuffer) in.readObject()).reverse().toString();
System.out.println(name);
this.age = in.readInt();
}
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"));
oos.writeObject(new ExPerson("brady", 23));
ExPerson ep = (ExPerson) ois.readObject();
System.out.println(ep);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
ydarb
无参构造函数构建对象
brady
ExPerson{name='brady', age=23}
这个例子跟 Serializable 接口中的自定义序列化基本一致,将用户名反转再序列化和反序列化。
注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。
3. 两种序列化对比
Serializable | Externalizable |
系统自动存储必要的信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
序列化版本号
java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况:
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
- 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
总结
- 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
- 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
- 如果想让某个变量不被序列化,使用transient修饰。
- 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
- 反序列化时必须有序列化对象的class文件。
- 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
- 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
- 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
- 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。