序列化和反序列化在面试中也经常考查,下面就总结一下 Java 中的序列化和反序列化。
什么是序列化和反序列化?
序列化是将 Java 对象转换成与平台无关的二进制流,而反序列化则是将二进制流恢复成原来的 Java 对象,二进制流便于保存到磁盘上或者在网络上传输。
如何实现序列化和反序列化?
如果想要序列化某个类的对象,就需要让该类实现 Serializable 接口或者 Externalizable 接口。
如果实现 Serializable 接口,由于该接口只是个 “标记接口”,接口中不含任何方法,序列化是使用 ObjectOutputStream(处理流)中的 writeObject(obj) 方法将 Java 对象输出到输出流中,反序列化是使用 ObjectInputStream 中的 readObject(in) 方法将输入流中的 Java 对象还原出来。
下面程序演示实现 Serializable 接口,将对象序列化到文件中,再从文件中反序列化对象。
Java Bean 类
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
// 此处没有提供无参的构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 省略 getter 和 setter 方法
}
序列化和反序列化
import java.io.*;
public class Test {
public static void main(String[] args) {
// 序列化
try (ObjectOutputStream outputStream = new ObjectOutputStream(new File("").getAbsolutePath()+"/object.txt"))) {
Person person = new Person("小明", 21);
outputStream.writeObject(person);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("").getAbsolutePath()+"/object.txt"))) {
Person person = (Person) objectInputStream.readObject();
System.out.println("name:" + person.getName() + ",age:" + person.getAge());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果为:name:小明,age:21
需要注意的是反序列化读取的仅仅是 Java 对象中的数据,而不是包含 Java 类的信息,所以在反序列化时还需要对象所属类的字节码(class)文件,否则会出现 ClassNotFoundException 异常。
如果实现 Externalizable接口,该接口继承自 Serializable 接口,在 Java Bean 类中实现接口中的 writeExternal(out) 和 readExternal(in) 方法,需要注意的是必须提供默认的无参构造函数,否则反序列化失败。
上面的 Java Bean 代码可修改为:
import java.io.*;
public class Person implements Externalizable {
private String name;
private int age;
// 需要提供默认的无参构造函数
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 省略 getter 和 setter 方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = in.readObject().toString();
this.age = in.readInt();
}
}
这两种序列化反序列化方式,前一种是使用默认的 Java 实现,而后一种是自定义实现,可以在序列化中选择如何序列化,比如对某个属性加密处理。
注意序列化属性的顺序要和属性反序列化中的顺序一样,否则在反序列化时不能恢复出原来的对象。
其实让类实现 Serializable 接口也是可以实现自定义序列化,只但需要在类中提供下面这三个方法。
private void writeObject(java.io.ObjectOutStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
writeObject() 方法的作用和上面 writeExternal() 方法类似,readObject() 方法的作用和上面 readExternal() 方法类似,而 readObjectNoData() 方法是在序列化流不完整、序列化和反序列化版本不一致导致不能正确反序列时调用的容错方法。
使用默认的序列化方式,会将对象中的每个实例属性依次进行序列化,如果某个属性是一个类类型,那么需要保证这个类也要是可序列化的类,否则将不能序列化该对象。在 Java 的序列化机制中,被序列化后的对象都有一个编号,多次序列化同一个对象,除了第一次真正序列化对象外,其他都是保存一个序列化编号。这样的机制带来的问题就是如果在序列化一个对象后,修改了对象中的属性,也不会生效。并不是对象中每个属性都需要序列化的,如被 static 修饰的属性是属于类的,而不是只属于某个对象。使用默认序列化方式,是不会将这些属性序列化的,在自定义的序列化方式中,我们也可以将这些属性忽略掉。除此之外,可以使用 transient 关键字来修饰某个属性,这样默认的序列化方式就不会序列化该属性了,自定义还是可以的。如果在反序列化时强行得到这些没有被序列化的值,得到的会是默认值(0 或 null)。
序列化和反序列化的版本问题
在 Java 的序列化机制中,允许给类提供一个 private static final 修饰的 SerialVersionUID 类常量,来作为类版本的代号。这样即使类被修改了(如修改了方法),也会把修改前的类和修改后的类当成同一版本的类,序列化和反序列化照样可以正常使用。如果我们不显式的定义这个 SerialVersionUID,Java 虚拟机会根据类的信息帮我们自动生成,修改前和修改后的计算结果往往不同,造成版本不兼容而发生反序列化失败,另外由于平台的差异性,在程序移植中也可能出现无法反序列化。强大的 IDE 工具,也都有自动生成 SeriaVersionUID 的方法,这里就不多说了。JDK 中自带的也有生成 SeriaVersionUID 值的工具 serialver.exe,使用 serialver 类名(编译后) 命令就能生成该类的 SeriaVersionUID 值啦!
总结: