自定义序列化
当一个类里包含的某些实例变量是敏感信息,这个时候不希望系统将该实例变量进行序列化,或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,从而避免报java.io.NotSerializableException
当对某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化,如果被引用的对象的实例变量引用了其他对象,则被应用的对象也会被序列化,这种情况称为递归序列化
transient关键字
序列化
通过在实例变量前使用transient关键修饰,告诉Java序列化的时候无需理会该实例变量,如下代码所示:
public class Person
implements java.io.Serializable
{
private String name;
private transient int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
transient关键字只能用于修饰实例变量
反序列化
import java.io.*;
public class TransientTest
{
public static void main(String[] args)
{
try (
// 创建一个ObjectOutputStream输出流
var oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
// 创建一个ObjectInputStream输入流
var ois = new ObjectInputStream(new FileInputStream("transient.txt")))
{
var per = new Person("孙悟空", 500);
// 系统会per对象转换字节序列并输出
oos.writeObject(per);
// 从序列化文件中读取该Person对象
var p = (Person) ois.readObject();
// 输入该Person对象的age实例变量值,输入0,因为age被transient关键字修饰
System.out.println(p.getAge());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
自定义序列化I
使用transient修饰的实例变量被完全隔离在序列化机制以外,反序列化的时候完全无法获取,除了这种情况外Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量,需要做这种特殊处理的类必须提供如下特殊签名方法,这些方法用于实现自定义序列化
private void writeObject(java.io.ObjectOutputStream out)throws IOException
: 负责写入特定类的实力状态,从而相应的readObject()方法可以恢复它,通过重写该方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要如何序列化,默认情况下,该方法会调用out.defaultWriteObject来保存Java对象的各实例变量,从而可以实现序列化Java对象private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException
: 负责从流中读取并恢复对象实例变量,通过重写该方法,可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,以及如何进行反序列化,默认情况下,该方法会调用in.defaultReadObject来恢复Java对象的没有被transient修饰的实例变量。通常情况下readObject()方法与writeObject()方法对应,如果writeObject()方法中对Java对象的实例变量进行了一些处理,则应该在readObject()方法中对其实例变量进行相应的饭处理从而正确恢复该对象private void readObjectNoData()throws ObjectStreamException
: 当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化对象
import java.io.*;
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer) in.readObject()).reverse()
.toString();
this.age = in.readInt();
}
}
自定义序列化II
还有一种更彻底的自定义机制,它甚至可以在序列化对象的时候将该对象替换成其他对象,要实现此种替换,必须为序列化类提供方法ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
只要该方法存在,序列化机制就会调用它
import java.util.*;
import java.io.*;
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
// 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
private Object writeReplace() throws ObjectStreamException
{
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
Java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象
import java.io.*;
import java.util.*;
public class ReplaceTest
{
public static void main(String[] args)
{
try (
// 创建一个ObjectOutputStream输出流
var oos = new ObjectOutputStream(new FileOutputStream("replace.txt"));
// 创建一个ObjectInputStream输入流
var ois = new ObjectInputStream(new FileInputStream("replace.txt")))
{
var per = new Person("孙悟空", 500);
// 系统将per对象转换字节序列并输出
oos.writeObject(per);
// 反序列化读取得到的是ArrayList
var list = (ArrayList) ois.readObject();
System.out.println(list);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
writeReplace()方法是递归的,系统调用writeObject()方法的时候,发现传入的对象里有writeReplace()则会执行它,从而序列化另一个对象,然而如果另一个对象里还有writeReplace()方法,则系统会转而执行该writeReplace()方法,以此类推,直到不再返回另一个对象为止
与writeReplace()方法相对应的,序列化机制里还有一个特殊的方法,他可以实现保护性复制整个对象ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
这个方法会紧接着readObject()之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象会被立刻丢弃
import java.io.*;
public class Orientation
implements java.io.Serializable
{
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;
private Orientation(int value)
{
this.value = value;
}
// 为枚举类增加readResolve()方法
private Object readResolve() throws ObjectStreamException
{
if (value == 1)
{
return HORIZONTAL;
}
if (value == 2)
{
return VERTICAL;
}
return null;
}
}
Orientation类的构造器私有,程序只有两个Orientation对象,分别通过HORIZONTAL和VERTICAL 两个常量来引用,如果让该类实现Serializable接口,则会引发一个问题,例如将一个Orientation.HORIZONTAL值序列化后再读出
import java.io.*;
public class ResolveTest
{
public static void main(String[] args)
{
try (
// 创建一个ObjectOutputStream输入流
var oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
// 创建一个ObjectInputStream输入流
var ois = new ObjectInputStream(new FileInputStream("transient.txt")))
{
oos.writeObject(Orientation.HORIZONTAL);
var ori = (Orientation) ois.readObject();
System.out.println(ori == Orientation.HORIZONTAL);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
这个时候如果直接用ori和Orientation.HORIZONTAL值进行比较,将会返回false,也就是说ori是一个新的Orientation对象,并不等于Orientation类中的任何一个枚举值,反序列化机制在恢复Java对象时无需调用构造器来初始化Java对象,从这个层面讲序列化机制可以用来克隆对象,这个时候readResolve()方法就有了意义
所有的单例类、枚举类在实现序列化的时候都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常
存在的弊端是writeReplace()和readResolve()方法都可以使用任意的访问控制符,因此父类的该两个方法可以被子类继承,而如果子类没有重写该两个方法,子类反序列化的时候将会得到一个父类的对象,因此建议使用final类重写readResolve()和writeReplace()方法,否则应尽量使用private修饰该两个方法
自定义序列化III
Java还可以由开发者决定存储和恢复哪些对象数据,要实现这种方式必须实现Externalizable接口,该接口定义了如下两个方法:
- void readExternal(ObjectInput in): 需要序列化的类实现readExternal()方法来实现反序列化,该方法调用DataInput(它是ObjectInput的父接口)的方法来恢复基本类型的实例变量值,调用ObjectInput的readObject()方法来恢复应用类型的实例变量值
- void writeExternal(ObjectOutput out): 需要序列化的类实现writeExternal()方法来保存对象的状态,该方法调用DataOutput(它是ObjectOutput的父接口)的方法来保存基本类型的实力变量值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值
import java.io.*;
public class Person implements java.io.Externalizable
{
private String name;
private int age;
// 注意必须提供无参数的构造器,否则反序列化时会失败。
public Person(){}
public Person(String name, int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
public void writeExternal(java.io.ObjectOutput out)
throws IOException
{
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(java.io.ObjectInput in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
- Person类实现了java.io.Externalizable接口,该Person类还实现了readExternal()、writeExternal()两个方法,这两个方法除方法签名和readObject()、writeObject()两个方法的方法签名不同外,其方法体完全一样
- 如果程序需要序列化实现Externalizable接口的对象,一样调用ObjectOutputStream的writeObject()方法输出该对象即可;反序列化该对象,则调用ObjectInputStream的readObject()方法
- 当使用Externalizable机制反序列化对象时,程序会先使用public的无参数构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参数构造器
注意事项
- 对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量(即static修饰的成员变量)、transient实例变量都不会被序列化
- 实现Serializable接口的类如果需要让某个实例变量不被序列化,则可以使用transient修饰,而不是加static关键字,虽然它也可以达到效果
- 保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例变量,否则该类是不可序列化的
- 反序列化对象时必须有序列化对象的class文件
- 当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取
版本
反序列化对象时必须提供该对象的class文件,而class可能会升级,Java如何保证两个class文件的兼容性?,序列化机制允许为序列化类提供一个private static final的serialVersionUID值,用于标识该Java类的序列化版本,如果该Java类升级了,但是标识不变序列化机制仍会当成同一个序列化版本来处理
public class Test
{
// 为该类指定一个serialVersionUID类变量值
private static final long serialVersionUID = 512L;
...
}
为了反序列化时确保序列化版本的兼容性,最后在每个要序列化的类中加入private static final long serialVersionUID这个类变量,否则该类变量有JVM根据类的相关信息计算,而java类升级后,JVM计算的值就会发生变化