Java序列化
什么是序列化?
一句话概括。序列化将对象的状态信息转换为可以存储或传输的形式的过程。而Java序列化,就是指把Java对象转换为“有序”字节序列的过程。相反的,把“有序”字节序列恢复为Java对象的过程称之为反序列化。
怎么理解上面的描述呢?从序列化的定义的可以看出,序列化其实是一个用来保障存储和传输的机制,而其针对的对象,肯定就是那些不便存储和传输的信息,而这体现在java编程中,就是类的实例 —— 类对象。
上面提到的“有序”,它并不是真的有顺序,其实是比较抽象化的表达,以此来表达经过序列化处理的对象,能够通过反序列化恢复成原来的样子这样一个过程
举个例子,你要搬家了,家里有个大鞋架,因为鞋架太大了,不便存储和运输,于是你把鞋机拆散了,然后把拆解步骤记录下来,运输到新家后,你再根据步骤记录把零件组装成鞋架原来的样子。拆鞋架和组装鞋架其实就是序列化和反序列的过程,而你记录的那份拆解步骤,就是序列化协议。
怎么实现序列化?
一、实现Serializable接口
定义一个类实现Serializable接口:
public class Rectangle implements Serializable {
private int width;
private int length;
public Rectangle(){}
public Rectangle(int width,int length){
this.width = width;
this.length = length;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", length=" + length +
'}';
}
}
二、实现Externalizable接口
实现
writeExternal
、readExternal
方法
public class Rectangle implements Externalizable {
private int width;
private int length;
public Rectangle(){}
public Rectangle(int width,int length){
this.width = width;
this.length = length;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", length=" + length +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
序列化和反序列化
public static void main(String[] args){ /* 序列化 */ Rectangle rectangle = new Rectangle(6,8); try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangle.out"))){ oos.writeObject(rectangle); }catch(Exception ex){ ex.printStackTrace(); } /* 反序列化 */ try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("rectangle.out"))){ Rectangle rectangle1 = (Rectangle) ois.readObject(); System.out.println(rectangle1); }catch(Exception ex){ ex.printStackTrace(); } }
正常的输出结果:
Rectangle{width=6, length=8}
当实现Externalizable接口,没有对
writeExternal
和readExternal
重写时,结果如下
Rectangle{width=0, length=0}
也就是说的,实现Externalizable接口,需要自己实现序列化和反序列逻辑,编程人员用比较高的灵活度,这点在接下来的自定义序列化会讲到
自定义序列化?
transient关键字
由transient
关键字修饰的成员的变量,序列化时将被忽略。该方法决定变量是否序列化。
该自定义方法级别最低,当使用的下面的任一方法时,
transient
关键字将失效。换句话说,使用下面各方法进行序列化自定义,那么transient修饰的变量同样会被序列化。
自定义readObject和writeObject方法
通过的类中自定义readObject
和writeObject
方法,可以控制该类的序列化和反序列化逻辑,jvm在进行序列化的时候会自动调用readObject
方法,反序列化调用writeObject
。
private void readObject(ObjectInputStream ois) throws IOException {
this.length = ois.readInt() / 100;
this.width = ois.readInt();
}
private void writeObject(ObjectOutputStream oos)throws IOException{
this.length = this.length * 100;
oos.writeInt(this.length);
oos.writeInt(this.width);
}
自定义writeReplace方法
writeReplace
方法没有入参,用于改变序列化对象的类型,且会使用默认的序列化机。也就是说,上面提到的自定义readObject和writeObject方法都将失效。以下为实例:
private Object writeReplace() throws ObjectStreamException {
List<Object> list = new ArrayList<>();
list.add(this.length * 1000);
list.add(this.width *1000);
return list;
}
相应的,在进行反序列化的时候,就应该使用新的类型进行接收,如该例子,应该用
List<Object>
接收反序列化对象。
自定义readResolve方法
readResolve
方法用于替换反序列化出来的对象,该方法同样没有入参。
private Object readResolve() throws ObjectStreamException {
System.out.println("自定义readResolve方法被调用");
return new Rectangle(9,10);
}
由于没发拿到序列化的信息,因此常用来控制单例模式下,反序列化出来的对象为原先的单例对象,以维护单例模式中反序列化对象的单一性。
实现Externalizable接口
这个已经在上面实例化方式中有所提及。实现该接口必须强制实现writeExternal
和readExternal
两个方法如下:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
this.length = this.length * 100;
out.writeInt(this.length);
out.writeInt(this.width);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.length = in.readInt() / 100;
this.width = in.readInt();
}
该效果等同于上面实现Serializable接口,然后自定义readObject
和writeObject
f方法
序列化的注意事项
-
serialVersionUID 序列化版本号
用来区分需要序列化类的版本。假如序列化的类中发生了变量的修改,那么该版本号一般也要跟着修改,才能够在反序列化的时候得到对应版本的类对象,否则会抛出
InvalidClassException
异常 -
序列化与类的初始化一样,是的一个递归的过程。
当一个类实现了序列化接口,该类的成员除了基本类型和String类型之外,其他的引用类型也必须是可序列化的;否则会抛
NotSerializableException
异常 -
同一对象多次序列化只会序列化一次
如果程序对同一个对象有多次序列化,那么只会在第一次进行序列化,后续实际上是返回一个编号进行区分
-
反序列化的顺序与序列化时的顺序一致,类似队列模型
序列化的使用场景
讲了这么多,那序列化到底用在什么地方呢?
由jvm的内存结构相关知识我们可以知道,java对象都被保存在堆内存中。在jvm处于运行状态的时候,我们能够对对象的进行复用。但一旦jvm生命周期结束,相关对象也随之被回收。也就是说的,如果希望在jvm停止后还能够拿到某些对象,这时候就需要用到java的序列化。
因此大概由以下几种场景会使用到java的序列化
- 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
- 当你想用套接字在网络上传送对象的时候;
- 当你想通过RMI(远程方法调用)传输对象的时候;