12.8 Java 9改进的对象序列化


对象序列化的目标是将对象保存到磁盘中,或允许在网络上直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得这种二进制流(无论从磁盘中获取的,还是通过网络获取的),都可以将这种二进制流恢复成原来的Java对象。

一、序列化的含义和意义

1.1 意义

序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以被保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

1.2 含义

对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象。
Java 9增强了对象序列化的机制,它允许对读入的序列化数据进行过滤,这种过滤可以在反序列化之前对数据执行校验,从而提高安全性和健壮性。

1.3 实现

如果需要让某个对象可以支持序列化机制,必须让它的类是可序列化的(serializable),为了让某个类是可序列化的,该类必须实现如下两个接口之一:
★Serializable
★Externalizable
Java很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须是西安任何方法,它只是表明该类的实例时可序列化的。

二、使用对象流实现序列化

如果需要将某个对象保存在磁盘上或通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口 。
这里先介绍Serializable实现序列化。

2.1 序列化

一旦某个类实现了Serializable接口,则该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象。
(1)创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上。

//创建一个ObjectOutputStream输出流
var oos=new ObjectOutputStream(new FileOutputStream("object.txt"));

(2)调用ObjectOutputStream对象的writeObject()方法输出可序列化对象

//将一个Person对象输出到输出流中
oos.writeObject(per);

下面程序中定义了一个Person类,这个Person类就是一个普通Java类,只是实现Serializable接口,该接口表示该类的对象是可序列化的:

package section8;
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;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

下面程序使用ObjectOutputStream将一个Person对象写入磁盘文件中

package section8;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class WriteObject
{
    public static void main(String[] args)
    {
        try(
                //创建一个ObjectOutputStrem输出流
                var oos=new ObjectOutputStream(new FileOutputStream("src//section8//object.txt"))
                )
        {
            var per=new Person("孙悟空",500);
            oos.writeObject(per);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上面代码创建了一个ObjectOutputStream输出流,这个ObjectOutputStream输出流建立在一个文件输出流的基础上;oos.writeObject(per);使用writeObject()方法将一个Person对象写入输出流。执行上面的程序将生成一个object.txt文件,文件的内容就是Person对象。

2.2 反序列化

反序列化的步骤
(1)创建一个ObjectInputStream,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。

//创建一个ObjectInputStream输入流
var ois=new ObjectInputStream(new FileInputStream("object.txt"));

(2)调用ObjectInputStream对象的readObject()对象读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型。

//从输出流中读入一个Java对象,并将其强制转化为Person类
var p=(Person)ois.readObject();

下面程序示范从object.txt文件中读取Person对象的步骤:

package section8;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class ReadObject
{
    public static void main(String[] args)
    {
        try(
                //创建一个ObjectInputStream输入流
                var ois=new ObjectInputStream(new FileInputStream("src//section8//object.txt"))
                )
        {
            //从输入流中读取一个Java对象,并将其强制转化为Person对象
            var p=(Person)ois.readObject();
            System.out.println("名字为:"+p.getName()+"\n年龄为"+p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}
"D:\IntelliJ IDEA 2019.2.4\jbr\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=54409:D:\IntelliJ IDEA 2019.2.4\bin" -Dfile.encoding=UTF-8 -classpath E:\Java\mysql-connector-java-8.0.13.jar;E:\Java\chap15\out\production\chap15 section8.ReadObject
名字为:孙悟空
年龄为500
Process finished with exit code 0

必须指出,反序列化读取的仅仅只是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,否则将会引发ClassNotFoundException异常。
Person类只有一个有参数构造器,没有无参数构造器,而且该构造器内部有一个普通的打印语句。当反序列化读取Java对象时,并没有看到程序调用该构造器,这表明反序列化机制无须通过构造器来初始化Java对象。

2.3 注意事项

如果使用序列化机制向文件中写入多个Java对象,使用反序列化机制恢复对象时必须按照实际写入顺序读取。


但一个可序列化的类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数构造器,要么时可序列化的——否则反序列化时将抛出InvalidClassException异常。如果父类是不可序列化的,只要是带有无参数构造器,该父类定义的成员变量的值不会反序列化到二进制流中。

三、对象引用的序列化

3.1 对象引用的序列化的基本约束

如果某个类的成员变量不是基本类型或String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类的成员变量也不是可序列化的。
如果Teacher类持有一个Person类的引用,只有Person类也是可序列化的,Teacher类也才是可序列化的。如果Person类不是可序列化的,则无论Teacher类是否实现Serializable、Extenalizable接口,则Teacher类都是不可序列化的。

package section8;

public class Teacher implements java.io.Serializable
{
    private String name;
    private Person student;
    public Teacher(String name,Person student)
    {
        this.name=name;
        this.student=student;
    }

    public Person getStudent() {
        return student;
    }

    public void setStudent(Person student) {
        this.student = student;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

注意:当程序初始化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用,为了在反序列化时可以恢复该Teacher对象,程序会顺带将该Person对象也进行序列化,所以Person类也必须是可序列化的,否则Teacher类将不可序列化。

3.2 多个对象引用同一个引用变量序列化时存在的问题

假设有一种特殊情形:程序中有两个Teacher对象,他们的student实例变量都引用到同一个Person对象,而且该Person对象还有一个引用变量引用它。

var per=new Person("孙悟空",500);
var t1=new Teacher("唐僧",per);
var t2=new Teacher("菩提祖师",per);

上面代码创建了两个Teacher对象和一个Person对象,这三个对象在内存中的存储示意图如下:

这里产生一个问题——如果先序列化t1对象,则系统将t1对象所引用的Person对象一起序列化;如果程序在序列化t2对象,系统一样会序列化该t2对象,并且再次序列化t2对象所引用的Person对象;如果程序再显示序列化per对象,系统将再次序列化该Person对象。这个过程是否会输出三个Person对象。
如果系统向输入流写入三个Person对象,那么后果是当程序从输入流中反序列化这些对象时,将会得到三个Person对象,从而引起t1和t2所引用的Person对象不是同一个对象,着与上图所示的效果不一致——这样违背了Java序列化机制的初衷。

3.3 Java序列化机制的序列化算法

Java序列化机制采用了一种特殊的序列化算法,其算法内容是:
(1)所有保存到磁盘中的对象都有一个序列化编号。
(2)当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有当该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
(3)如果某个对象是已经序列化过的,程序将直接只是输出一个序列化编号,而不是再次重新序列化该对象。
根据上面的序列化算法,可以得到一个结论——当第二次、第三次序列化Person对象时,程序不会再次将Person对象对象转化为字节序列并输出,而是仅仅只输出一个序列化编号。

3.4 实例演示

假设有如下顺序序列化代码:

oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);

上面代码依次序列化t1、t2和per对象,序列化后磁盘文件的存储示意图如下图所示:

从上图可以看出,多次调用writeObject()方法输出同一个对象时,只有第一次调用writeObject()方法才会将该对象转化为字节序列并输出。
下面程序序列化两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象,而且程序两次调用writeObject()方法输出同一个Teacher对象:

package section8;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class WriteTeacher
{
    public static void main(String[] args)
    {
        try(
                //创建一个ObjectOutputStream输出流
                ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("src//section8//teacher.txt"))
                )
        {
            var per=new Person("孙悟空",500);
            var t1=new Teacher("唐僧",per);
            var t2=new Teacher("菩提祖师",per);
            //依次将四个对象写入输出流中
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(per);
            oos.writeObject(t2);
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

上面依次调用了四次writeObject()方法来输出对象,实际上只初始化了三个对象,而且序列化的两个Teacher对象的student引用实际上都是同一个Person对象。
下面程序读取序列化文件中的对象即可证明这一点:

package section8;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ReadTeacher
{
    public static void main(String[] args)
    {
        try(
                //创建一个ObjectInputStream输入流
                var ois=new ObjectInputStream(new FileInputStream("src//section8//teacher.txt"))
                )
        {
            //依次读取ObjectOutputStream输入流中的4个对象
            var t1=(Teacher)ois.readObject();
            var t2=(Teacher)ois.readObject();
            var per=(Person)ois.readObject();
            var t3=(Teacher)ois.readObject();
            System.out.println("t1的student引用是否与p相同:"+(t1.getStudent()==per));//true
            System.out.println("t2的student引用是否与p相同:"+(t2.getStudent()==per));//true
            System.out.println("t2与t3对象是否为同一个对象:"+(t2==t3));//true
        }
        catch (IOException | ClassNotFoundException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

上面程序运行结构就证明序列化机制的算法。

3.5 Java序列化机制引入新的问题

由于Java序列化机制使然:如果多次序列同一个Java对象时,只有第一次序列化时才会把该Java对象转换成字节序列并输出,这样可能引起一个潜在的问题:当程序序列化一个可变对象时,程序只有在第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,即使后面该对象的属性已被改变,当程序再次调用writeObject方法时,程序只是输出前面的序列化编号,所以改变的的属性值不会被输出。
只有当第一次调用wirteObject()方法来输出对象时才会将对象转换成字节序列,并写出到ObjectOutputStream;在后面程序中如果该对象的属性发生了改变,即再次调用writeObject方法输出该对象时,改变后的属性不会被输出

package section8;

import java.io.*;

public class SerializableMutable
{
    public static void main(String[] args)
    {
        try(
                //创建一个ObjectOutputStream输出流
                var oos=new ObjectOutputStream(new FileOutputStream("src//section8//mutable.txt"));
                //创建一个ObjectInputStream输出流
                var ois=new ObjectInputStream(new FileInputStream("src//section8//mutable.txt"))
                )
        {
            var per=new Person("孙悟空",500);
            oos.writeObject(per);
            //改变per对象的实例变量的值
            per.setName("猪八戒");
            //系统只是序列化编号,所以改变后的name不会被序列化
            oos.writeObject(per);
            var p1=(Person)ois.readObject();
            var p2=(Person)ois.readObject();
            //下面输出true,因为反序列化后p1等于p2
            System.out.println(p1==p2);
            //下面将依然看到孙悟空,因为改变后的实例变量没有被序列化
            System.out.println(p2.getName());
        }
        catch (IOException | ClassNotFoundException ioe)
        {
            ioe.printStackTrace();
        }
    }
}
有参数的构造器
true
孙悟空

四、Java 9增加的过滤功能

Java 9为ObjectInputStream增加了setObjectInputFilter()、getObjectInputFilter()两个方法,其中第一个方法用于为对象输入流设置过滤器。当程序通过ObjectInputStream反序列化对象时,过滤器的checkInput()方法会被自动激发,用于检查序列化数据是否有效。
使用checkInput()方法检查序列化数据时有3种返回值。
★Status.REJECTED:拒绝恢复。
★Status.ALLOWED:允许恢复。
★Status.UNDECIDED:未决定状态,程序继续执行检查。
ObjectInputStream将会根据ObjectInputFilter的检查结果来决定是否执行反序列化,如果checkInput()方法返回Status.REJECTED,反序列化将会被阻止;如果checkInput()方法返回Status.ALLOWED,程序将可执行反序列化。
程序可通过checkInput的FilterInfo参数来获取序列化数据的信息。
下面程序对前的ReadObject.java进行改进,该程序将会在反序列化之前对数据执行检查。

package section8;

import java.io.*;

public class FilterTest
{
    public static void main(String[] args)
    {
        try (
                // 创建一个ObjectInputStream输入流
                var ois = new ObjectInputStream(new FileInputStream("src//section8//object.txt")))
        {
            ois.setObjectInputFilter((info) -> {
                System.out.println("===执行数据过滤===");
                ObjectInputFilter serialFilter = ObjectInputFilter.Config.getSerialFilter();
                if (serialFilter != null) {
                    // 首先使用ObjectInputFilter执行默认的检查
                    ObjectInputFilter.Status status = serialFilter.checkInput(info);
                    // 如果默认检查的结果不是Status.UNDECIDED
                    if (status != ObjectInputFilter.Status.UNDECIDED) {
                        // 直接返回检查结果
                        return status;
                    }
                }
                // 如果要恢复的对象不是1个
                if (info.references() != 1)
                {
                    // 不允许恢复对象
                    return ObjectInputFilter.Status.REJECTED;
                }
                if (info.serialClass() != null &&
                        // 如果恢复的不是Person类
                        info.serialClass() != Person.class)
                {
                    // 不允许恢复对象
                    return ObjectInputFilter.Status.REJECTED;
                }
                return ObjectInputFilter.Status.UNDECIDED;
            });
            // 从输入流中读取一个Java对象,并将其强制类型转换为Person类
            var p = (Person) ois.readObject();
            System.out.println("名字为:" + p.getName()
                    + "\n年龄为:" + p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}
===执行数据过滤===
名字为:孙悟空
年龄为:500

上面ois.setObjectInputFilter((info) -> {...为ObjectInputStream设置了ObjectInputFilter过滤器(程序使用了Lambda表达式创建了过滤器),重写了checkInput()方法。
重写checkInput()方法时先用默认的ObjectInputFilter执行检查,如果检查结果不是Status.REJECTED,程序直接返回检查结果。接下来程序通过FilterInfo检验序列化数据,如果序列化数据对象不唯一(数据已被污染),程序拒绝执行反序列化;如果序列化的对象不是Person对象(数据已被污染),程序拒绝执行反序列化。通过这种检查,程序可以保证反序列化出来的就是唯一的Person对象,这样让程序更安全、健壮。

五、自定义序列化

5.1 应用场景

在一些特殊场景,如果一个类里包含某些实例变量是敏感信息,例如银行账户信息等,这是不希望将该实例变量的值进行序列化;或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSerializableException异常。

5.2 递归序列化

当某个对象进行序列化时,系统会把该对象的所有实例变量进行序列化,如果某个实例变量引用了另一个对象时,则被引用的对象也会被序列化;如果被引用的对象的实例变量耶引用了其他对象,则被引用的对象也会被实例化,这种情况称为递归序列化。

5.3 自定义序列化

5.3.1 transient关键字

在属性前面加上transient关键字,可以指定Java序列化时无需理会该属性值。transient关键字只能用于修饰实例变量,不可修饰Java程序的其他成分。
下面Person类与前面的基本一样,只是age使用了transient关键字修饰

package section8.lesson5;
public class Person implements java.io.Serializable
{
    private String name;
    private transient int age;
    //注意此处没有提供无参数构造器
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

下面程序先序列化一个Person对象,然后再反序列化该Person对象,得到了反序列化的Person对象后程序输出该对象的age实例变量的值。

package section8.lesson5;

import java.io.*;

public class TransientTest
{
    public static void main(String[] args) throws IOException {
        try(
                //创建一个ObjectOutputStream输出流
                var oos=new ObjectOutputStream(new FileOutputStream("src//section8//lesson5//transient.txt"));
                //创建一个ObjectInputStream输入流
                var ois=new ObjectInputStream(new FileInputStream("src//section8//lesson5//transient.txt"))
                )
        {
            var per=new Person("孙悟空",500);
            //系统将per对象转换成字节序列并输出
            oos.writeObject(per);
            var p=(Person)ois.readObject();
            System.out.println(p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}
0

Person类的age实例变量使用了transient修饰,所以程序不会将实例变量的值给序列化,而是序列化实例变量的默认值,所以输出0.
使用transient关键字修饰实例变量虽然简单、方便,但被transient修饰的实例变量完全被隔离在序列化机制之外,这样导致再反序列化恢复Java对象时无法获得该实例变量的值。

5.3.2 为可序列化的类提供writeObjec()、readObject()、readObjectNoData()方法

Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何让序列化个实例变量,甚至完全不序列化某些实例变量(与transient)。
在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化:
(1)private void writeObject(java.io.ObjectOutputStream out)throws IOException
(2)private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
(3)private void readObjectNoData()throws ObjectStreamException;
说明:
★writeObject()方法负责写入特定类的实力状态,以便相应的reaadObject()方法可以恢复它。通过重写该方法,程序员完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要通过怎样序列化。在默认情况下,该方法会调用out.defaultWriteObject()来保存Java对象的各实例变量,从而可以实现序列化Java对象的目的。
★readObject()方法负责从流中读取并恢复对象实例变量,通过重写该方法,程序员完全获得对反序列化机制的控制,可以自主决定需要反序列化的实例变量,以及如何进行反序列化。在默认情况下,该方法会调用in.defaultReadObject来恢复Java对象的非瞬时实例变量。在通常情况下,readObject()方法与writeObject()方法相对应,如果writeObject()方法中对Java对象的实例变量进行一些处理,则应该在readObject()方法中对其实例变量进行相应的反处理,以便正确恢复该反序列化的对象。
★当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如接收方使用的反序列化的版本不同于发送方,或者接受方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统会调用readObjectNoData()方法来初始化反序列化的对象。
下面Person类提供了writeObject()和readObject()方法,其中writeObject()方法在保存Person对象时将其name变量包装成StringBuffer,并将其字符序列反转后写入;在readObject()方法中处理name的策略与此对应——先将读取的数据强制转换为StringBuffer,在将其反转后赋给name实例变量。

package section8.lesson5;

import java.io.IOException;

public class PersonSerializable implements java.io.Serializable
{
    private String name;
    private int age;
    //注意此处没有提供无参数构造器
    public PersonSerializable(String name,int age)
    {
        System.out.println("有参数的构造器");
        this.name=name;
        this.age=age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = 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();
    }
}

上面的private void writeObject(java.io.ObjectOutputStream out)、private void readObject(java.io.ObjectInputStream in)用于实现自定义序列化,对于这个Person对象而言,序列化、反序列化Person实例并没有什么区别——区别在于序列化后的对象流,即使有Cracker截获到Person对象流,它看到的name也是加密后name值,这样提高了序列化的安全性。
writeObject()方法存储实例变量的顺序应该和readObject()方法中恢复实例变量的顺序一致,否则将不能正确恢复该Java对象。

5.3.3 在序列化对象时替换成另一个对象——writeReplace()

还有一种更加彻底的自定义序列化机制,它甚至可以在序列化对象时将该对象替换成其他对象。如果需要将序列化对象替换成其他对象时,则应为序列化类提供如下特殊方法:

ANY-ACCESS-MODIFIER  Object writeReplace() throws ObjectStreamException;

此writeReplace()方法将由序列化机制调用,只要该方法存在。因为调用该方法可以拥有私有(private)、受保护的(protected)和包私有(package-private)等访问权限,所以其子类可能获取该方法。例如PersonReplace类提供了writeReplace()方法,这样在写入PersonReplace对象时将该对象替换成ArrayList。

package section8.lesson5;

import java.io.ObjectStreamException;
import java.util.ArrayList;

public class PersonRepalce implements java.io.Serializable
{
    private String name;
    private int age;
    //注意到此处没有提供无参数构造器
    public PersonRepalce(String name,int age)
    {
        System.out.println("有参数构造器");
        this.name=name;
        this.age=age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    //重写writeReplace()方法,程序在序列化该对象之前先调用该方法
    private Object writeReplace()
        throws ObjectStreamException
    {
        ArrayList<Object> list=new ArrayList<Object>();
        list.add(name);
        list.add(age);
        return list;
    }
}

java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回另一个Java对象,则系统为序列化另一个对象。如下程序表面上是序列化Person对象,但实际上序列化的是ArrayList.

package section8.lesson5;

import java.io.*;
import java.util.ArrayList;

public class ReplaceTest
{
    public static void main(String[] args)
    {
        try(
                //创建一个ObjectOutputStream输出流
                var oos=new ObjectOutputStream(new FileOutputStream("src//section8//lesson5//replace.txt"));
                //创建一个ObjectInputStream输入流
                var ois=new ObjectInputStream(new FileInputStream("src//section8//lesson5//replace.txt"))
                )
        {
            var per=new PersonRepalce("孙悟空",500);
            oos.writeObject(per);
            //反序列化得到的是ArrayList
            var list=(ArrayList)ois.readObject();
            System.out.println(list);
        }
        catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
有参数构造器
[孙悟空, 500]

根据上面的介绍,可以知道系统在序列化某个对象之前,会先调用该对象得writeReplace()和writeObject()两个方法,系统总是先调用被序列化对象的writeReplace()方法,如果该方法返回另一个对象,系统将再次调用另一个对象的writeReplace()方法......直到该方法不会返回另一个对象为止,程序最后将调用该对象的writeObject()方法来保存该对象的状态。

5.3.4 单例类、枚举类反序列化保证唯一性——readResolve()

与WriteReplace()方法相对的是readResolve(),序列化机制里还有一种特殊的方法,可以保护性地复制整个对象。这个方法就是:

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

这个方法会紧跟着readObject()之后被调用,该方法的返回值会代替原来反序列化的对象,而原来readObject()反序列化的对象将会被立即丢弃。
readObject()方法在序列化单例类、枚举类时尤其有效。当然如果使用Java 5提供的enum来定义枚举类,则完全不用担心,程序没有人任何问题。但如果应用中有早期一楼下的枚举类,例如下面的Orientation类就是一个枚举类。

package section8.lesson5;

public class Orientation
{
    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;
    }
}

在Java 5之前,这种代码很常见。Orientation类的构造器私有,程序只有两个Orientation对象,分别通过Orientation的HORIZONTAL、VERTICAL两个常量来引用。但如果让该类实现Serializable接口,则会引发一个问题,如果将一个Orientation.HORIZONTAL的值序列化后再读取,则如下面程序片段所示:

package section8.lesson5;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class OritationTest
{
    public static void main(String[] args)
    {
        try(
                var oos=new ObjectOutputStream(new FileOutputStream("src//section8//lesson5//orientation.txt"));
                var ois=new ObjectInputStream(new FileInputStream("src//section8//lesson5//orientation.txt"))
                )
        {
            oos.writeObject(Orientation.HORIZONTAL);
            var ori=(Orientation)ois.readObject();
            System.out.println(Orientation.HORIZONTAL==ori);//false
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

如果立即拿出ori和Orientation.HORIZONTAL值进行比较,将会发现返回false。也就是说,ori是一个新的Orientation对象,而不等于Orientation中的任何枚举值——虽然Orientation的构造器是private的,但是反序列化依然可以创建Orientation对象。
前面指出,反序列化机制在恢复Java对象时无须调用构造器来初始化java对象。从这个意义上来说,序列化机制可以“克隆”对象。
在这种情况下,可以通过为Orientation类提供一个readResolve()方法来解决该问题,readResolve()方法的返回值将会代替原来的反序列化的对象,也就是让反序列化得到的Orientation对象直接被丢弃。下面为Orientation类提供readResolve()方法:

package section8.lesson5;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class Orientation implements 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;
    }
    private Object readResolve()
        throws ObjectStreamException
    {
        if(value==1)
        {
            return HORIZONTAL;
        }
        if(value==2)
        {
            return VERTICAL;
        }
        return null;
    }
}

这样通过重写readResolve()方法可以保证反序列化得到的依然是Orientation的HORIZONTAL或VERTICAL两个枚举值之一。再次调用上面的OritationTest.java程序将得到true。
所有单例类、枚举类在实现序列化时都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常。

readResolve()方法可以使用任何访问控制符,因此父类的readResolve()方法可能被其子类继承,这样利用readResolve()方法就会存在一个明显的缺点,就是当父类已经实现了readResolve()方法后,子类将变得无从下手。如果父类包含一个protected或public的readResolve()方法,而且子类耶没有重写该方法,将会使得子类的反序列化时得到一个父类的对象——这显然不是我们想要的结果,而且不容易发现这个错误。总是让子类重写readResolve()方法无疑是一个负担。
因此建议是,对于final类(不可被继承)而言,重写readResolve()方法没有任何问题;否则,重写readResolve()方法时尽量使用private修饰该方法。

六、另一种自定义序列化机制

6.1 实现Extenalizable接口

这种序列化方式完全由程序员决定存储和恢复对象的数据。要实现该目标,Java类必须实现Exteenalization接口,该接口里定义了如下两个方法。
(1)void readExternal(ObjectInput in):需要序列化的类实现 readExternal方法来实现反序列化。该方法调用DataInput(它是ObjectInput的父接口)的方法来恢复基本类型的属性值,调用ObjectInput的readObject方法来来恢复引用类型的属性值。
(2)void writeExternal(ObjectOutput out):需要序列化的类实现writeExternal方法来保存对象的状态。该方法调用DataInput(它是ObjectInput的父接口)的方法来保存基本类型的属性值,调用 ObjectOutput的writeObject方法来保存引用类型的属性值。
实际上采用实现Externalizable接口方式的序列化与前面介绍的自定义序列化非常像,只是Externalizable接口强制自定义序列化。
下面的Person类实现了Externalizable接口,并且实现该接口里提供的两个方法,用于实现自定义序列化:

package section8.lesson6;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements java.io.Externalizable
{
    private String name;
    private int age;
    //注意必须此处提供无参数的构造器,否则反序列化时会失败
    public Person(){}
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public void writeExternal(ObjectOutput objectOutput) throws IOException {
        //将name实例变量的值反转后写入二进制流中
        objectOutput.writeObject(new StringBuffer(name).reverse());
        objectOutput.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException {
        this.name=((StringBuffer)objectInput.readObject()).reverse().toString();
        this.age=objectInput.readInt();
    }
}

上面的程序实现了java.io.Externalizable接口,该Person类还实现了writeExternal()、readExternal()两个方法,这两个方法签名和writeObject()、readObject()方法是类似的。下面程序序列化和反序列化步骤和前面的基本一样:

package section8.lesson6;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ExternalizableTest
{
    public static void main(String[] args)
    {
        try(
                var oos=new ObjectOutputStream(new FileOutputStream("src//section8//lesson6//external.txt"));
                var ois=new ObjectInputStream(new FileInputStream("src//section8//lesson6//external.txt"))
                )
        {
            var per=new Person("孙悟空",500);
            oos.writeObject(per);
            var p=ois.readObject();
            System.out.println(p);
            System.out.println(p==per);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

}
section8.lesson6.Person@67424e82
false

需要注意的是,当使用Externalizable机制反序列化对象时,程序会使用public无参数的构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化必须提供public无参数构造器。

6.2 关于两种序列化机制的对比:

实现Serializable接口 实现Extenalizable接口
系统自动存储必要必要信息 程序员决定存储必要的信息
java内建支持,易于实现,只需要实现该接口即可,无须任何代码支持 仅仅提供两个空方法,实现该接口为两个空方法提供实现
性能略差 性能略好

虽然Extenalizable接口能带来一定性能提升,但是,实现Extenalizable接口导致编程复杂度增加,所以大部分的时候采用Serializable接口来实现序列化。

6.3 Serializable接口和Externalizable接口自定义序列化注意事项:

关于对象序列化的注意事项:
(1)对象的类名、实例变量都会被序列化;方法、类变量(即static修饰的成员变量)、transient实例变量(也成为瞬态实例变量)都不会被序列化。
(2)实现Serializable接口的类如果需要让某个实例变量不会被序列化,则可以在实例变量前添加transient修饰符,而不是加static修饰符。虽然static修饰符也能达到这个效果,但是不能这样用。
(3)保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例变量,要不然,该类是不可序列化的。
(4)反序列化对象时必须由序列化对象的class文件
(5)当通过文件、网络来读取序列化的的对象时,必须按实际写入的顺序读取。
(6)使用Extenalizable接口实现自定义序列胡必须提供无参数构造器,否则在反序列化时会失败。

七、版本

7.1 问题

思考一个问题?

7.2 serialVersionUID属性值

Java序列化机制允许为序列化类提供一个private static final的serialVersionUID属性值,该属性值用于标识该Java类的序列化版本,也就是说如果一个类升级后只要它的serialVersionUID属性值保持不变,序列化机制也会把它们当成同一个序列化版本。
分配serialVersionUID类变量的值非常简单,如下片段:

public class Test
{
//为该类指定一个serialVersionUID类变量的值
private static final long serialVersionUID=521L;
....

为了在反序列化时确保序列化版本的兼容性,最好在每个序列化的类中加入private static final long serialVersionUID这个类变量,具体数值自己定义。这样,即使某个对象在被序列化之后,它所对应的类被修改了,该对象依然可以正确反序列化。
如果不显示定义serialVersionUID类变量的值,该类变量的值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前类的计算结果往往不同,从而导致对象的反序列化因为版本不兼容而失败。
下面可以通过JDK安装路径bin目录下的serialver.exe工具来获取serialVersionUID类变量的值。命令如下:

Serialver Person;

示例:

不显示指定serialVersionUID类变量的值的另一个坏处,不利于程序在不同JVM之间移植。因为不同编译器对该类变量的计算策略可能不同,从而造成虽然类完全没有改变,但是因为JVM不同,也会出现序列化版本不兼容而无法正确反序列化的现象。

7.3 哪些对类的修改会导致类实例反序列化失败

如果类的修改确实会导致类的反序列化失败,则应该为该类的serialVersionUID重新分配值,那么哪些对类的修改会导致类实例反序列化失败呢?
(1)仅仅修改类的放啊,则反序列化不受任何影响,类定义无须修改serialVersionUID类变量的值。
(2)仅仅修改静态变量或瞬态实例变量,则反序列化不受任何影响,类定义无须修改serialVersionUID类变量的值。
(3)如果修改了类的瞬态的实例变量的值,则可能导致序列化版本不兼容。
如果对象流中的对象和新类中包含同名的实例变量,而实例变量的类型不同,则反序列化会失败,类定义应该更新serialVersionUID类变量的值。
如果对象流中的对象包含了比新类中包含更多的实例变量,则多的实例变量的值将会被忽略,序列化版本可以兼容,类定义可以不更新serialVersionUID类变量的值;如果新类比对象流中的对象包含更多的实例变量,则序列化版本可以兼容,类定义可以不更新serialVersionUID类变量的值;但反序列化得到的新对象中多出的实例变量的值都是null(引用类型实例变量)或0(基本类型实例变量)。

posted @ 2020-05-03 23:39  小新和风间  阅读(245)  评论(0编辑  收藏  举报