本帖最后由 逆风TO 于 2020-4-28 14:20 编辑
最近阅读Serializable接口和Externalizable接口的源码,并结合了一些资料,对面试过程中与序列化相关的内容做了一些总结。
一、序列化、反序列化、使用场景、意义。
序列化:将对象写入IO流中;
反序列化:从IO流中恢复对象;
意义:序列化机制允许将实现序列化的Java对象转换为字节序列,并将字节序列保存在磁盘中,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使地对象可以脱离程序的运行而独立存在。
使用场景:所有在网络上传输的对象都必须是可序列化的。如:RMI (远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错。所有必须保存到磁盘的java对象都必须是可序列化的。程序创建的JavaBean最好都实现Serializable接口。
二、实现序列化的方式
实现序列化有两种方式:实现Serializable接口或Externalizable接口,通常情况下,实现Serializable接口即可。两种接口的对比如下:
[Java] 纯文本查看 复制代码 实现Serializable接口:
1) 系统自动存储必要的信息;
2) Java内建支持,易于实现,只需要实现接口接口,不需要任何代码支持;
3) 性能略差;
实现Externalizable接口:
1) 自己决定要序列化哪些属性;
2) 必须实现该接口内的两个方法:
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
3) 性能略好;
三、使用Serializable接口实现序列化。
Serializable接口是一个标记接口,不用实现任何方法,一旦某个类实现了该方法,则该类的对象是可序列化的。
1、通过以下步骤实现序列化:
1)创建一个ObjectOutputStream输出流;
2)调用OjectOutputSteam对象的writeObject ()输出可序列化对象。
[Java] 纯文本查看 复制代码 public class Person implements Serializable {
private String name;
private String age;
public Person() {
System.out.println("调用Person的无参构造函数");
}
public Person(String name, String age) {
this.name = name;
this.age = age;
System.out.println("调用Person的有参构造函数");
}
@Override
public String toString() {
// TODO 自动生成的方法存根
return "Person{'name' :" + name + ",'age' :" + age + "}";
}
}
[Java] 纯文本查看 复制代码 public class WriteObject {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.txt"));
Person p = new Person("baby", "12");
oos.writeObject(p);
} catch (Exception e) {
// TODO: handle exception
}
}
}
输出的序列化文件如下:
[Java] 纯文本查看 复制代码 aced 0005 7372 0017 7365 7269 616c 697a
6162 6c65 5465 7374 2e50 6572 736f 6e4e
aff9 165f 38dd f602 0002 4c00 0361 6765
7400 124c 6a61 7661 2f6c 616e 672f 5374
7269 6e67 3b4c 0004 6e61 6d65 7100 7e00
0178 7074 0002 3132 7400 0462 6162 79
2、通过以下步骤实现反序列化:
1)创建一个ObjectInputStream输入流;
2)调用ObjectInputStream对象的readObject ()得到序列化对象。
[Java] 纯文本查看 复制代码 public class WriteObject {
public static void main(String[] args) {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.txt"));
Person p = (Person) ois.readObject();
System.out.println(p.toString());
} catch (Exception e) {
// TODO: handle exception
}
}
}
输出结果如下:
[Java] 纯文本查看 复制代码 Person{'name' :baby,'age' :12}
通过输出结果,我们知道反序列化没有调用类的构造方法,而是由JVM自己生成对象。
3、当类的成员是引用数据类型时
若一个类的成员不是基本数据类型,也不是String类型的时候,则该成员必须是可序列化的,否则会导致该类无法完成序列化。如下例子所示:
[Java] 纯文本查看 复制代码 // 去掉Person类实现的序列化接口
public class Teacher implements Serializable {
private String name;
private Person person;
public Teacher(String name, Person person) {
this.name = name;
this.person = person;
}
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Teacher.txt"));
Person p = new Person("baby", "16");
Teacher t = new Teacher("mom", p);
oos.writeObject(t);
}
}
执行时会抛出下面的异常,异常指出,因为Person类不可序列化,导致Teacher类无法完成序列化操作。
4、序列化过程中存在的问题。
1)同一对象,会被序列化多次吗?
依次将p、t1、t2、t1序列化到文件SerializableMore中。
[Java] 纯文本查看 复制代码 public class WriteMore {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerializableMore.txt"));
Person p = new Person("baby", "16");
Teacher t1 = new Teacher("mom", p);
Teacher t2 = new Teacher("dad", p);
oos.writeObject(p);
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(t1);
}
}
接下来将反序列化文件SerializableMore。
[Java] 纯文本查看 复制代码 public class ReadMore {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SerializableMore.txt"));
// 注意:反序列化的顺序和序列化时的顺序一致
Person p = (Person) ois.readObject();
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
System.out.println("t1 == t2 ---------------------------->" + (t1 == t2));
System.out.println("t1.getPerson() == p ----------------->" + (t1.getPerson() == p));
System.out.println("t2.getPerson() == p ----------------->" + (t2.getPerson() == p));
System.out.println("t2 == t3 ---------------------------->" + (t2 == t3));
System.out.println("t1.getPerson() == t2.getPerson() ---->" + (t1.getPerson() == t2.getPerson()));
}
}
输出结果如下所示:
[Java] 纯文本查看 复制代码 t1 == t2 ---------------------------->false
t1.getPerson() == p ----------------->true
t2.getPerson() == p ----------------->true
t2 == t3 ---------------------------->false
t1.getPerson() == t2.getPerson() ---->true
可以看到:针对同一对象进行多次序列化,Java并不会序列化多次,而是沿用第一次序列化获得的序列化编码。
2)由于Java序列化算法不会重复序列化同一个对象,只会记录已序列化对象的序列化编号。而当一个可变的对象中的内容发生改变时,此时进行序列化,却不会重新将此对象转换为字节序列,而是保存序列化编号。如下所示。
[Java] 纯文本查看 复制代码 public class WirteOnChange {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("WriteOnchange.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("WriteOnchange.txt"));
Person person = new Person("索隆", "20");
System.out.println("修改前:" + person.toString());
oos.writeObject(person);
person.setName("香吉士");
System.out.println("修改后:" + person.toString());
oos.writeObject(person);
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
System.out.println(p1 == p2);
System.out.println(p1.getName().equals(p2.getName()));
}
}
输出结果如下:
[Java] 纯文本查看 复制代码 修改前:Person{'name' :索隆,'age' :20}
修改后:Person{'name' :香吉士,'age' :20}
true
true
5、Java序列化算法
[Java] 纯文本查看 复制代码 1)所有保存到磁盘的对象都有一个序列化编号;
2)当试图序列化一个对象时,会先检查该对象是否已经序列化过,只有该对象未被JVM序列化过,才会将该对象序列化为字节序列输出;
3)如果此对象已经被序列化过,则直接输出序列化编码号即可。
如下图所示:
6、可选的自定义序列化
1)使用transient关键字指定不进行序列化的字段。
使用transient修饰的属性,java序列化时会忽略该属性。而当反序列化时,被transient修饰的属性则赋予默认值。对于引用类型则为null,boolean类型为false,基本类型为0。
[Java] 纯文本查看 复制代码 public class Teacher implements Serializable {
private String name;
private transient String age;
private transient int height;
private Person person;
public Teacher(String name, String age, int height, Person person) {
this.name = name;
this.age = age;
this.height = height;
this.person = person;
}
// ...省略getter、setter方法
[Java] 纯文本查看 复制代码 public class WirteOnChange {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"));
Person person = new Person("索隆", "20");
Teacher teacher = new Teacher("鹰眼", "30", 190, person);
System.out.println("序列化之前:" + teacher.toString());
oos.writeObject(teacher);
Teacher t1 = (Teacher) ois.readObject();
System.out.println("序列化之后:" + t1.toString());
}
}
输出结果如下所示:[Java] 纯文本查看 复制代码 序列化之前:Teacher{"name" : "鹰眼"; "age" : "30"; "height" : 190; "person" : Person{'name' :索隆,'age' :20}
序列化之后:Teacher{"name" : "鹰眼"; "age" : "null"; "height" : 0; "person" : Person{'name' :索隆,'age' :20}
2)通过下面的方法可以实现自定义序列化,可以控制序列化的方式或对序列化数据进行编码加密等。
[Java] 纯文本查看 复制代码 private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
通过重写writeObject()与readObject()方法,可选择哪些属性要序列化。如果writeObject使用了某种规则进行序列化,则readObject要使用相反的规则进行反序列化,以便能正确反序列化对象。
[Java] 纯文本查看 复制代码 // 对字符串name进行反转加密
public class Person implements Serializable {
private String name;
private String age;
private int height;
// 省略构造函数和getter、setter方法
private void WriteObject(ObjectOutputStream oos) throws IOException {
oos.writeObject(new StringBuilder(this.name).reverse()); // 利用StringBuilder实现字符串反转
oos.writeInt(height);
}
private void ReadObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
this.name = ((StringBuilder) ois.readObject()).reverse().toString();
this.height = ois.readInt();
}
当序列化流不完整时,readObjectNoData()方法可以正确地初始化反序列化的对象。例如,使用不同类接收反序列化对象,或者序列化流被篡改,系统都会调用readObjectNoData()来初始化反序列化对象。
3)彻底的自定义序列化
以下两个方法会在序列化前或反序列化后自动调用,可以实现更加彻底的自定义序列化。
[Java] 纯文本查看 复制代码 ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
writeReplace()方法:在序列化前,会先调用该方法,再调用writeObject方法。此方法可以使用任意对象代替目标序列化对象。
[Java] 纯文本查看 复制代码 public class Person implements Serializable {
private String name;
private String age;
// 省略构造方法、getter和setter方法
private Object writeReplace() throws ObjectStreamException {
ArrayList<String> list = new ArrayList<>();
list.add(this.name);
list.add(this.age);
return list;
}
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
Person person = new Person("罗宾", "18");
oos.writeObject(person);
ArrayList<String> list = (ArrayList) ios.readObject();
System.out.println(list);
}
输出结果如下:
[Java] 纯文本查看 复制代码 [罗宾, 18]
readResolve()方法:替代反序列化输出的对象,反序列化出来的对象会被立即丢弃,此方法在readObject()后调用。
[Java] 纯文本查看 复制代码 public class Person implements Serializable {
private String name;
private String age;
private int height;
// 省略构造方法、getter和setter方法
private Object readResolve() throws ObjectStreamException {
return new Person("娜美", "23");
}
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
Person person = new Person("罗宾", "18");
oos.writeObject(person);
Person p1 = (Person) ios.readObject();
System.out.println(p1);
}
输出结果如下:
[Java] 纯文本查看 复制代码 Person{'name' :娜美,'age' :23}
四、使用Externalizable接口实现序列化。
Externalizable接口不同于Serializable接口,该接口需要强制重写两个方法。
[Java] 纯文本查看 复制代码 public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
测试程序如下:[Java] 纯文本查看 复制代码 public class PersonExternal implements Externalizable {
private String name;
private int age;
// 必须提供无参构造函数
public PersonExternal() {
System.out.println("调用无参构造方法!!");
}
public PersonExternal(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 将读取的字符串反转后赋值给name实例变量
this.name = ((StringBuilder) in.readObject()).reverse().toString();
System.out.println("将name按相同的规则反序列化输出:" + name);
this.age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 将name反转后写入二进制流
StringBuilder reverse = new StringBuilder(name).reverse();
System.out.println("将name反转并序列化写入二进制流:" + reverse.toString());
out.writeObject(reverse);
out.writeInt(age);
}
@Override
public String toString() {
// TODO 自动生成的方法存根
return "Person{'name' :" + name + ", 'age' :" + age + "}";
}
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("PersonExternal.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("PersonExternal.txt"));
System.out.println("序列化ing");
oos.writeObject(new PersonExternal("cindy", 23));
System.out.println("反序列化ing");
PersonExternal pe = (PersonExternal) ois.readObject();
System.out.println(pe.toString());
}
}
输出结果:[Java] 纯文本查看 复制代码 序列化ing
将name反转并序列化写入二进制流:ydarb
反序列化ing
调用无参构造方法!!
将name按相同的规则反序列化输出:brady
Person{'name' :brady, 'age' :23}
可以看到的是,实现Externalizable接口必须提供pulic的无参构造器,因为在反序列化的时候需要通过反射创建对象。
五、序列化版本号serialVersionUID 。
介绍了那么多关于序列化的内容,我们知道,反序列必须要有class文件,但随着项目的升级,class文件也会随之升级。那么,序列化怎么保证升级前后的兼容性呢?
Java序列化提供了一个serializableVersionUID的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以被正确地反序列化回来。
[Java] 纯文本查看 复制代码 public class Person implements Serializable {
// 序列化版本号
private static final long serialVersionUID = 1227593270102525184L;
private String name;
private String age;
private int height;
但是,如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;
不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
接下来列出几种序列化的情况:
[Java] 纯文本查看 复制代码 a) 只是修改了方法,反序列化不影响,则无需修改版本号
b) 只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号
c) 修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;
六、总结 。
[Java] 纯文本查看 复制代码 a) 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
b) 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
c) 如果想让某个变量不被序列化,使用transient修饰。
d) 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
e) 反序列化时必须有序列化对象的class文件。
f) 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
g) 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
i) 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
j) 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
k) 数组不能显式地声明serialVersionUID,因为它们始终都有默认的计算值,但是对于数组类,无需匹配serialVersionUID。
l) 可以通过序列化和反序列化的方式实现对象的深复制。
转自CSDN
|