多引用写入
//有 bug 版本
//SerializableTest.java
private static void serializeBug() throws Exception {
System.out.println("序列化...");
Student student = new Student("六号表哥", 25);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(student);
//修改属性再次写入
student.setName("王武");
oos.writeObject(student);
oos.flush();
oos.close();
}
//SerializableTest.java
private static void deserialize() throws Exception {
System.out.println("反序列化...");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Student student = (Student) ois.readObject();
Student student2 = (Student) ois.readObject();
ois.close();
System.out.println(student);
System.out.println(student2);
}
测试输出结果
从输出结果来看,name 属性并没有发生改变
序列化...
反序列化...
Student{name='六号表哥', age=25',color = null}
Student{name='六号表哥', age=25',color = null}
使用以下两种方法来修复这个问题:
//bugfix 版本
private static void serializeBugFix() throws Exception {
System.out.println("序列化...");
Student student = new Student("六号表哥", 25);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(student);
//方式一
oos.reset();
student.setName("王武");
oos.writeObject(student);
//方式二
//oos.writeUnshared(student);
oos.close();
}
private static void deserialize() throws Exception {
System.out.println("反序列化...");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Student student = (Student) ois.readObject();
Student student2 = (Student) ois.readObject();
ois.close();
System.out.println(student);
System.out.println(student2);
}
输出结果:
序列化...
反序列化...
Student{name='六号表哥', age=25',color = null}
Student{name='王武', age=25',color = null}
序列化的属性如果没有实现序列化接口会在序列化时会报错
public class Student implements Serializable {
private String name;
private int age;
/**
* Color 类也是需要实现序列化接口的。
*/
private Color color;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
}
Exception in thread "main" java.io.NotSerializableException: com.example._super.Color
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.example.seriable.SerializableTest.serialize(SerializableTest.java:43)
at com.example.seriable.SerializableTest.main(SerializableTest.java:18)
枚举序列化问题
下图是 ObjectOutputStream 对枚举的序列化和反序列化的描述,大概的意思是,枚举的的序列化和反序列化和普通的对象不一样,它只会序列化枚举的 name 值,而反序列化是根据得到的 name 值去调用 enum 的 valueOf(name)
得到枚举对象。还有一点不同的是枚举的序列化和反序列化过程是不能被定制的,因此类似于 readObject,writeObject,writeExternal 等方法是无效的,并且对应的 serialVersionUID 也被指定为0,设置为其他值也是无效的,这些内容都会被忽略掉。
栗子一:下面通过一个栗子来验证一下:
public enum Color /*implements Serializable*/ {//枚举本身就是实现了 Serializable 因此这里不需要去实现哦~
RED, GREEN, BLUE;
//枚举类的 serialVersionUID 会被 JVM 忽略
private static final long serialVersionUID = 1L;
public static void main(String[] args) {
ObjectStreamClass desc = ObjectStreamClass.lookup(Color.RED.getClass());
//枚举类的 serialVersionUID 都是 0
//因此枚举类在增加新的枚举对象时并不会出现反序列化失败问题
long serialVersionUID = desc.getSerialVersionUID();//0
System.out.println(serialVersionUID);
}
}
上面是用于验证枚举的 serialVersionUID 值的,从输出结果可以知道尽管 Color 类设置了 serialVersionUID = 1L,但是我们通过 ObjectStreamClass#getSerialVersionUID 得到的值仍然是0,有兴趣的读者可以验证一下。
栗子二:验证枚举只序列化了 name 属性,而没有序列化 ordinal 属性
枚举的序列化与反序列化从输出结果来看,序列化确实是没有序列化 orinal 属性,而只是序列化了 name 属性,而在反序列化时会根据 name 属性去调用 valueOf(name) 得到对应的枚举对象。
public enum Color2 {
//序列化前的排序:RED,GREEN, BLUE;
// RED,GREEN, BLUE;
//序列化后的排序:GREEN, RED, BLUE;
GREEN, RED, BLUE;
private static final String PATH = "./color.bin";
/**
* 序列化前的结果
* RED-0
* GREEN-1
* BLUE-2
* ---------------------
* 反序列化得到的输出结果
* RED-1
* GREEN-0
* BLUE-2
*/
public void printValue() {
System.out.println(RED.name() + "-" + RED.ordinal());
System.out.println(GREEN.name() + "-" + GREEN.ordinal());
System.out.println(BLUE.name() + "-" + BLUE.ordinal());
}
public static void main(String[] args) throws Exception {
serialEnum();
// deserialEnum();
}
private static void serialEnum() throws IOException {
/**
* 序列化 Color2.GREEN
*/
Color2.GREEN.printValue();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(PATH));
oos.writeObject(Color2.GREEN);
oos.flush();
oos.close();
}
private static void deserialEnum() throws Exception {
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(PATH));
Color2 color2 = (Color2) ois.readObject();
color2.printValue();
}
}
类结构演化
例如在序列化后反序列化前将类中增加或者删除某一个属性,对反序列化过程是不会报错的(前提是你将 serialVersionUID 值固定了),如果你是改变的属性的类型,那么肯定是会报错的。
如果类型发生改变,那么在反序列化时会报如下错误:
Exception in thread "main" java.io.InvalidClassException: com.example.seriable.Student; incompatible types for field age
at java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2299)
at java.io.ObjectStreamClass.getReflector(ObjectStreamClass.java:2193)
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:669)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at com.example.seriable.SerializableTest.deserialize(SerializableTest.java:83)
单例的序列化和反射问题
下面来看一个比较简单的单例实现:
public class Singleton implements Serializable {
private static final String PATH ="./singleton.bin" ;
private volatile static Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
public static void main(String[] args) throws Exception{
serialSington();
deserialSington();
}
//序列化
private static void serialSington() throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(PATH));
oos.writeObject(Singleton.getInstance());
oos.flush();
oos.close();
}
private static void deserialSington() throws Exception {
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(PATH));
Singleton singleton = (Singleton) ois.readObject();
System.out.println(singleton);
System.out.println(Singleton.getInstance());
}
}
上面的 main 函数的执行结果如下:
很明显这两个对象是不一样的,违背了单例的原则。
com.example.singleton.Singleton@58372a00
com.example.singleton.Singleton@330bedb4
解决方法呢?我们在 Singleton 类中加入一下方法,并返回 INSTANCE 该单例的对象
private Object readResolve() {
return INSTANCE;
}
上面的 main 函数的执行结果如下:
com.example.singleton.Singleton@330bedb4
com.example.singleton.Singleton@330bedb4
这样通过 readResolve() 就解决了单列序列化问题了。
接下来反射问题呢?
通过一个静态变量 flag 来标识类对象是否已经被创建过,创建过对象之后,如果再次反射去创建那么将会抛出异常。但是呢?我也是可以反射去修改 flag 值呢,所以说单例在反射面前就有点尴尬了。
public class Singleton2 {
private volatile static Singleton2 INSTANCE = null;
private static boolean flag = false;
private Singleton2() {
if (!flag) {
flag = true;
} else {
throw new RuntimeException("单例不允许多次创建");
}
}
public static Singleton2 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton2.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton2();
}
}
}
return INSTANCE;
}
public static void main(String[] args) throws Exception {
Singleton2 instance = Singleton2.getInstance();
Class<Singleton2> singletonClazz = Singleton2.class;
Constructor<Singleton2> constructor = singletonClazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton2 singleton2 = constructor.newInstance();
System.out.println(singleton2);
System.out.println(instance);
}
}
项目地址:https://github.com/liaowjcoder/study4Java/tree/master/10_seriable
本文是笔者学习之后的总结,方便日后查看学习,有任何不对的地方请指正。
记录于 2019年5月12号
网友评论