Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方把该Byte 流里的数据读出来,重新构造一个相同的对象。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
把Java对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为Java对象的过程称为对象的反序列化。
1、序列化的用途
利用对象的序列化可以保存应用程序的当前工作状态,下次再启动的时候将自动地恢复到上次执行的状态。
对象的序列化主要有两种用途:
(a) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
(b) 在网络上传送对象的字节序列。
2、序列化的实现
(1)JDK类库中的序列化API
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
(2)对象序列化与反序列化的过程
将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
对象序列化包括如下步骤:
(a)创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
(b)通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
(a)创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
3、serialVersionUID作用:
序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。
附序列化面试题:
1) Java中Serializable和Externalizable接口的不同是什么?
Serializable接口存在于java.io包中,它组成了Java序列化机制的核心。它没有任何方法,因此被称为标识(Marker)接口。当你的类实现了Serializable接口时,它在Java中变成了可序列化的,这给编译器一个标识,可以用于序列化机制来序列化该对象。
3)什么是serialVersionUID?如果你不定义它会发生什么?
SerialVersionUI是一个在对象序列化时在对象上的时间戳的ID,通常是对象的哈希码,你可以使用工具serialver来查看一个序列化对象的serialVersionUID。serialVersionUID用于对象的版本控制。你也可以在你的类文件上定义serialVersionUID。不指定serialVersionUID的结果是当你添加或修改类的任意字段时,已经序列化的类将被不能被反序列化,因为新类的serialVersionUID与之前序列化的对象不同。Java序列化过程依赖正确的serialVersionUID来恢复序列化对象的状态,如果serialVersionUID不匹配,则会抛出java.io.InvalidClassException。
4)当序列化时,有一些属性你不希望被序列化,你怎么来实现?
这有时候也会以什么是transient变量的问题出现,或者是transient和static变量是否会序列化的形式出现。因此,如果你不希望属性作为对象状态的一部分,那么你可以基于你的需求将其定义为static或transient变量,然后在Java序列化过程中,这些变量将不会被序列化。
5)如果类中的一个成员没有实现Serializable接口,将会发生什么?
如果你试图序列化实现了Serializable接口的类的一个对象,但是该类引用了其他非Serializable类,那么运行时就会抛出”NotSerializableException”。
6)如果类是Serializable的,但是它的父类不是,那么在反序列化后它从父类中继承的实例变量的状态是什么?
Java序列化过程在类的继承体系中会查找实现了Serializable接口的类,继承自父类的实例变量在序列化过程中将通过调用非Serializable父类的构造方法来初始化。一旦构造器链开始了,它将不能停止,即使父类实现了Serializable接口,它们的构造方法也会被执行。
7)你能自定义序列化过程吗?或者你可以覆盖Java中默认的序列化过程吗?
答案是,是的,你可以。我们都知道为了序列化对象,objectOutputStream.writeObject(saveThisObject)会被调用,为了读取对象ObjectOutputStream.readObject()会被调用,但是Java虚拟机提供了另外的东西给你,使你可以在类中定义这两个方法。如果你在类中定义了这两个方法,那么JVM将会调用你定义的方法而不是使用默认的序列化机制。你可以自定义序列化和反序列化的行为。要注意的重点是将这两个方法定义为私有的,以避免其被继承、覆盖。因为只有JVM可以调用私有方法,而你的序列化过程也可以正常进行。
8)假设一个新的类的父类实现了Serializable接口,你如何避免新的类被序列化?
如果一个类的父类实现了Serializable接口,那么在Java中,它已经是可序列化的了,因为你不能取消对接口的实现,所以不可能使得它非可序列化,但是没错,有一种方法来避免序列化新的类。要避免Java序列化,你需要实现writeObject和readObject方法,然后在这些方法中抛出NotSerializableException。这是自定义Java序列化过程的又一个好处,也是覆盖序列化过程的问题的后续问题。
9)在Java序列化和反序列化过程中使用了哪个方法?
这是一个常问的问题,面试官试图了解你是否熟悉readObject()、writeObject()、readExternal()和writeExternal()的用法。Java序列化是通过java.io.ObjectOutputStream类来实现的。该类是一个过滤器流,它包装了底层的字节流来处理序列化机制。为了通过序列化机制来存储任何对象,我们调用ObjectOutputStream.writeObject(saveThisObject)方法,为了反序列化,我们调用ObjectInputStream.readObject()方法。调用writeObject方法触发了Java中的序列化机制。有一个重要的需要注意的是readObject方法用于从已存储的文件中读取字节并创建相应的对象,对象创建完毕后需要自己转换为相应的正确的类型。
10)假设你有一个类你对其进行序列化并将其存储在文件中,后面你添加了新的属性。那么对已经序列化的类在反序列化时,会发生什么?
这取决于该类是否有自己的serialVersionUID。从上面的问题中,我们已经知道,如果我们不提供serialVersionUID,那么JVM将为其生成一个,通常它等于该对象的哈希码。如果添加了新的属性,那么该类的版本号就会发生变化,那么在这种情况下,Java序列化API将抛出java.io.InvalidClassException异常,这就是我们为什么推荐你添加自己的serialVersionUID的原因,这能确保单独的类中它是一致的。
11)我们可以在网络上传输序列化的对象吗?
是的,你可以在网络上传输序列化的对象,因为序列化后的对象是已二进制形式存在,因此我们可以在网络上对其进行传输。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
把Java对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为Java对象的过程称为对象的反序列化。
1、序列化的用途
利用对象的序列化可以保存应用程序的当前工作状态,下次再启动的时候将自动地恢复到上次执行的状态。
对象的序列化主要有两种用途:
(a) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
(b) 在网络上传送对象的字节序列。
2、序列化的实现
(1)JDK类库中的序列化API
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
(2)对象序列化与反序列化的过程
将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
对象序列化包括如下步骤:
(a)创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
(b)通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
(a)创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
(b)通过对象输入流的readObject()方法读取对象。
例子:
import java.io.*;
class Student implements Serializable{
private static final long serialVersionUID = 1L;
String name ;
int age;
Student(){}
Student(String n , int a){
this.name = n;
this.age = a;
}
public String toString(){
return "姓名" + name + " 年龄" + age ;
}
}
public class SerializableDemo{
public static void main(String[] args) throws Exception{
File f = new File("D:"+java.io.File.separator+"ser.txt");
OutputStream os = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(os);
Student stu[] = {new Student("bob",19),new Student("back",30)};
oos.writeObject(stu);
oos.close();
InputStream in = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(in);
Student [] newStu = (Student[])ois.readObject();
for(int i = 0 ; i<newStu.length ; i++){
System.out.println(newStu[i].toString());
}
ois.close();
}
}
3、serialVersionUID作用:
序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。
附序列化面试题:
1) Java中Serializable和Externalizable接口的不同是什么?
在是Java序列化面试最常被问到的问题。下面是我对该问题的回答:Externalizable为我们提供了writeExternal()和readExternal方法,可以给我们灵活的控制Java的序列化机制而不需要依赖默认的Java序列化实现。正确的实现Externalizable接口可以大幅度的提高应用的性能。
Serializable接口存在于java.io包中,它组成了Java序列化机制的核心。它没有任何方法,因此被称为标识(Marker)接口。当你的类实现了Serializable接口时,它在Java中变成了可序列化的,这给编译器一个标识,可以用于序列化机制来序列化该对象。
3)什么是serialVersionUID?如果你不定义它会发生什么?
SerialVersionUI是一个在对象序列化时在对象上的时间戳的ID,通常是对象的哈希码,你可以使用工具serialver来查看一个序列化对象的serialVersionUID。serialVersionUID用于对象的版本控制。你也可以在你的类文件上定义serialVersionUID。不指定serialVersionUID的结果是当你添加或修改类的任意字段时,已经序列化的类将被不能被反序列化,因为新类的serialVersionUID与之前序列化的对象不同。Java序列化过程依赖正确的serialVersionUID来恢复序列化对象的状态,如果serialVersionUID不匹配,则会抛出java.io.InvalidClassException。
4)当序列化时,有一些属性你不希望被序列化,你怎么来实现?
这有时候也会以什么是transient变量的问题出现,或者是transient和static变量是否会序列化的形式出现。因此,如果你不希望属性作为对象状态的一部分,那么你可以基于你的需求将其定义为static或transient变量,然后在Java序列化过程中,这些变量将不会被序列化。
5)如果类中的一个成员没有实现Serializable接口,将会发生什么?
如果你试图序列化实现了Serializable接口的类的一个对象,但是该类引用了其他非Serializable类,那么运行时就会抛出”NotSerializableException”。
6)如果类是Serializable的,但是它的父类不是,那么在反序列化后它从父类中继承的实例变量的状态是什么?
Java序列化过程在类的继承体系中会查找实现了Serializable接口的类,继承自父类的实例变量在序列化过程中将通过调用非Serializable父类的构造方法来初始化。一旦构造器链开始了,它将不能停止,即使父类实现了Serializable接口,它们的构造方法也会被执行。
7)你能自定义序列化过程吗?或者你可以覆盖Java中默认的序列化过程吗?
答案是,是的,你可以。我们都知道为了序列化对象,objectOutputStream.writeObject(saveThisObject)会被调用,为了读取对象ObjectOutputStream.readObject()会被调用,但是Java虚拟机提供了另外的东西给你,使你可以在类中定义这两个方法。如果你在类中定义了这两个方法,那么JVM将会调用你定义的方法而不是使用默认的序列化机制。你可以自定义序列化和反序列化的行为。要注意的重点是将这两个方法定义为私有的,以避免其被继承、覆盖。因为只有JVM可以调用私有方法,而你的序列化过程也可以正常进行。
8)假设一个新的类的父类实现了Serializable接口,你如何避免新的类被序列化?
如果一个类的父类实现了Serializable接口,那么在Java中,它已经是可序列化的了,因为你不能取消对接口的实现,所以不可能使得它非可序列化,但是没错,有一种方法来避免序列化新的类。要避免Java序列化,你需要实现writeObject和readObject方法,然后在这些方法中抛出NotSerializableException。这是自定义Java序列化过程的又一个好处,也是覆盖序列化过程的问题的后续问题。
9)在Java序列化和反序列化过程中使用了哪个方法?
这是一个常问的问题,面试官试图了解你是否熟悉readObject()、writeObject()、readExternal()和writeExternal()的用法。Java序列化是通过java.io.ObjectOutputStream类来实现的。该类是一个过滤器流,它包装了底层的字节流来处理序列化机制。为了通过序列化机制来存储任何对象,我们调用ObjectOutputStream.writeObject(saveThisObject)方法,为了反序列化,我们调用ObjectInputStream.readObject()方法。调用writeObject方法触发了Java中的序列化机制。有一个重要的需要注意的是readObject方法用于从已存储的文件中读取字节并创建相应的对象,对象创建完毕后需要自己转换为相应的正确的类型。
10)假设你有一个类你对其进行序列化并将其存储在文件中,后面你添加了新的属性。那么对已经序列化的类在反序列化时,会发生什么?
这取决于该类是否有自己的serialVersionUID。从上面的问题中,我们已经知道,如果我们不提供serialVersionUID,那么JVM将为其生成一个,通常它等于该对象的哈希码。如果添加了新的属性,那么该类的版本号就会发生变化,那么在这种情况下,Java序列化API将抛出java.io.InvalidClassException异常,这就是我们为什么推荐你添加自己的serialVersionUID的原因,这能确保单独的类中它是一致的。
11)我们可以在网络上传输序列化的对象吗?
是的,你可以在网络上传输序列化的对象,因为序列化后的对象是已二进制形式存在,因此我们可以在网络上对其进行传输。