美文网首页Java
序列化机制—Serializable需要注意的几点问题

序列化机制—Serializable需要注意的几点问题

作者: 未见哥哥 | 来源:发表于2019-05-12 14:43 被阅读2次
Serializable 需要注意的坑

多引用写入

//有 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号

相关文章

网友评论

    本文标题:序列化机制—Serializable需要注意的几点问题

    本文链接:https://www.haomeiwen.com/subject/ljjqaqtx.html