基本概念
- 什么是序列化和反序列化
序列化就是将对象写入到IO流中,反序列化就是从IO流中恢复对象。 - 为什么需要序列化
序列化后的流,可用于持久化到磁盘,也可以用于网络传输。使得Java对象可以跨进程、跨主机使用。 - 转成Json和XML算序列化吗
Java对象转成字符串、Json、XML等其实也称为“序列化”,但与JVM提供的序列化功能不太一样,可以说序列化是一个比较抽象的概念,但本文主要指JVM的序列化。
如何序列化和反序列化
- 实现Serializable接口
序列化最常用的方式是实现Serializable接口,然后让客户端去序列化。
public class Teacher implements Serializable {
private String name;
private int age;
public Teacher(String name, int age) {
System.out.println("调用了构造方法!");
this.name = name;
this.age = age;
}
// getter方法
}
public class Client {
public static void main(String[] args) {
System.out.println("序列化:");
try(ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("object.txt"))) {
Teacher teacher = new Teacher("Tom", 53);
System.out.println("序列化之前的hash:" + teacher.hashCode());
out.writeObject(teacher);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("反序列化:");
try(ObjectInputStream input = new ObjectInputStream(
new FileInputStream("object.txt"))) {
Teacher teacher = (Teacher) input.readObject();
System.out.println("Teacher的名字:" + teacher.getName());
System.out.println("Teacher的年龄:" + teacher.getAge());
System.out.println("序列化之后的hash:" + teacher.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
序列化:
调用了构造方法!
序列化之前的hash:142666848
反序列化:
Teacher的名字:Tom
Teacher的年龄:53
序列化之后的hash:310656974
根据输出结果,我们可以总结出以下三点信息:
- 不用提供无参构造方法
- 反序列化时,不会调用构造方法
- 如果没有重写hashCode,反序列化后,hashCode是不一样的
至于为什么hashCode会不一样?因为Java默认的hashCode是一个内存地址指针。反序列化后,对象自然不在原来的内存地址上,所以hashCode会不一样。所以这里我们提倡要覆盖hashCode方法和equals方法。
- transient关键字
如果一个字段使用了transient
关键字,那它就不会被序列化。在反序列化时,如果它的引用类型,值就是null,如果是基本类型,就是基本类型的默认值。这里我们尝试把age声明为transient
的。会输出:
Teacher的年龄:0
- 自定义序列化方法
transient关键字虽然使用方便,但“可定制化”不强,比如我虽然不想序列化age字段,但希望它反序列化时默认值是18,这样的需求就得使用自定义序列化方法来实现。
主要有这样几个方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
private Object writeReplace() throws ObjectStreamException;
private Object readResolve() throws ObjectStreamException;
通过重写writeObject与readObject方法,就可以实现自定义的序列化和反序列化。这里需要注意的是两个方法要有“对称性”,自己在重写这两个方法的时候,需要保证被序列化后,能够顺利地被反序列化。
示例代码:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
PS: 这两个类是私有的,是在什么时候被调用,如何被调用的呢?
答案是通过反射。详情可见 ObjectOutputStream 中的 writeSerialData 方法,以及
ObjectInputStream 中的 readSerialData 方法。
除此之外,还可以使用writeReplace与readResolve实现更高程度地定制化。
- writeReplace:在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象。
- readResolve:反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。
那readObjectNoData()方法用来干嘛的呢?详情可以看Serializable接口的注释。大意是:当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。
- 实现Externalizable接口
Externalizable
接口是继承自Serializable接口的。
使用Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;必须提供public的无参构造器,因为在反序列化的时候需要反射创建对象。
示例代码:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
StringBuffer reverse = new StringBuffer(name).reverse();
out.writeObject(reverse);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
常见问题
-
如果不实现Serializable接口会怎样?
如果不实现Serializable接口(这里包括也不实现Externalizable),客户端在序列化时,会报NotSerializableException异常。 -
如果内部属性是引用类型,怎么办?
如果要序列化的对象内部有引用类型的属性,那这个属性也必须实现序列化,否则同样会报NotSerializableException异常。 -
子类实现序列化,父类不实现序列化
子类实现序列化,父类不实现序列化,此时父类要实现一个无参数构造器,否则反序列化时会抛InvalidClassException异常。因为如果父类不实现序列化,反序列化时会调用父类的无参构造器。
// 父类:
public class Person {
private String nationality;
public Person(String nationality) {
this.nationality = nationality;
}
}
// 子类:
public class Teacher extends Person implements Serializable {
private String name;
private int age;
public Teacher(String name, int age) {
super("China"); // 调用父类的有参构造方法
System.out.println("调用了构造方法!");
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
// 客户端:
public class Client {
public static void main(String[] args) {
System.out.println("序列化:");
try(ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("object.txt"))) {
Teacher teacher = new Teacher("Tom", 53);
System.out.println("序列化之前的hash:" + teacher.hashCode());
out.writeObject(teacher);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("反序列化:");
try(ObjectInputStream input = new ObjectInputStream(
new FileInputStream("object.txt"))) {
Teacher teacher = (Teacher) input.readObject();
System.out.println("Teacher的名字:" + teacher.getName());
System.out.println("Teacher的年龄:" + teacher.getAge());
System.out.println("序列化之后的hash:" + teacher.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
序列化:
调用了构造方法!
序列化之前的hash:1060830840
反序列化:
java.io.InvalidClassException: serialize.Teacher; no valid constructor
at java.base/java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:159)
at java.base/java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:864)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2061)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
at serialize.Client.main(Client.java:20)
- 同一个对象多次序列化会发生什么?
Java序列化同一对象,并不会将此对象序列化多次得到多个对象。Java会给每个序列化成功的对象一个“序列化编号”。当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。否则,直接输出编号。
out.writeObject(teacher); // 序列化一次
// 反序列化两次,会抛EOFException异常
Teacher teacher = (Teacher) input.readObject();
Teacher teacher2 = (Teacher) input.readObject();
Teacher teacher = new Teacher("Tom", 53);
out.writeObject(teacher);
teacher.setAge(23)
out.writeObject(teacher); // 序列化两次
// 反序列化两次
Teacher teacher = (Teacher) input.readObject();
Teacher teacher2 = (Teacher) input.readObject();
System.out.println(teacher.equals(teacher2)); // true
System.out.println("Teacher的年龄:" + teacher.getAge()); // 53
- 序列化和反序列化的顺序?
反序列化时,取出对象的顺序与序列化是一致的。也就是说,先存先取,后存后取。
Teacher teacher1 = new Teacher("Tom", 53);
out.writeObject(teacher1);
Teacher teacher2 = new Teacher("Bob", 29);
out.writeObject(teacher2);
Teacher teacher1 = (Teacher) input.readObject();
System.out.println(teacher1.getName()); // Tom
Teacher teacher2 = (Teacher) input.readObject();
System.out.println(teacher2.getName()); // Bob