Java 对象序列化

对象序列化

对象序列化机制允许把内存中的Java对象转换成与平台无关的二进制流,从而可以保存到磁盘或者进行网络传输,其它程序获得这个二进制流后可以将其恢复成原来的Java对象。 序列化机制可以使对象可以脱离程序的运行而对立存在

序列化的含义和意义

序列化

序列化机制可以使对象可以脱离程序的运行而对立存在

序列化(Serialize)指将一个java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该java对象

如果需要让某个对象可以支持序列化机制,必须让它的类是可序列化(serializable),为了让某个类可序列化的,必须实现如下两个接口之一:

  • Serializable:标记接口,实现该接口无须实现任何方法,只是表明该类的实例是可序列化的

  • Externalizable

所有在网络上传输的对象都应该是可序列化的,否则将会出现异常;所有需要保存到磁盘里的对象的类都必须可序列化;程序创建的每个JavaBean类都实现Serializable;

使用对象流实现序列化

实现Serializable实现序列化的类,程序可以通过如下两个步骤来序列化该对象:

1.创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上

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

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

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

定义一个NbaPlayer类,实现Serializable接口,该接口标识该类的对象是可序列化的

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

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
}

使用ObjectOutputStream将一个NbaPlayer对象写入磁盘文件

import java.io.*;

public class WriteObject
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("object.txt")))
        {
            NbaPlayer player = new NbaPlayer("维斯布鲁克", 0);
            // 将player对象写入输出流
            oos.writeObject(player);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

反序列化

从二进制流中恢复Java对象,则需要使用反序列化,程序可以通过如下两个步骤来序列化该对象:

1.创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上

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

2.调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,可进行强制类型转换成其真实的类型

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

从object.txt文件中读取NbaPlayer对象的步骤

import java.io.*;
public class ReadObject
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            // 从输入流中读取一个Java对象,并将其强制类型转换为NbaPlayer类
            NbaPlayer player = (NbaPlayer)ois.readObject();
            System.out.println("名字为:" + player.getName()
                + "\n号码为:" + player.getNumber());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象所属的class文件,否则会引发ClassNotFoundException异常;反序列化机制无须通过构造器来初始化Java对象

如果使用序列化机制向文件中写入了多个Java对象,使用反序列化机制恢复对象必须按照实际写入的顺序读取。当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参的构造器,要么也是可序列化的—否则反序列化将抛出InvalidClassException异常。如果父类是不可序列化的,只是带有无参数的构造器,则该父类定义的Field值不会被序列化到二进制流中

对象引用的序列化

如果某个类的Field类型不是基本类型或者String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则有用该类型的Field的类也是不可序列化的

public class AllStar implements java.io.Serializable
{
    private String name;
    private NbaPlayer player;
    public AllStar(String name, NbaPlayer player)
    {
        this.name = name;
        this.player = player;
    }

    // name的setter和getter方法
    public String getName()
    {
        return this.name;
    }

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

    // player的setter和getter方法
    public NbaPlayer getPlayer() 
    {
        return player;
    }
    
    public void setPlayer(NbaPlayer player) 
    {
        this.player = player;
    }
}

Java特殊的序列化算法

  • 所有保存到磁盘中的对象都有一个序列化编号

  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟中机)被序列化过,系统才会将该对象转换成字节序列并输出

  • 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象

import java.io.*;
public class WriteAllStar
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("allStar.txt")))
        {
            NbaPlayer player = new NbaPlayer("詹姆斯哈登", 13);
            AllStar allStar1 = new AllStar("西部全明星", player);
            AllStar allStar2 = new AllStar("首发后卫", player);
            // 依次将四个对象写入输出流
            oos.writeObject(allStar1);
            oos.writeObject(allStar2);
            oos.writeObject(player);
            oos.writeObject(allStar2);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

4个写入输出流的对象,实际上只序列化了3个,而且序列的两个AllStar对象的player引用实际是同一个NbaPlayer对象。以下程序读取序列化文件中的对象

import java.io.*;
public class ReadAllStar
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectInputStream输出流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("allStar.txt")))
        {
            // 依次读取ObjectInputStream输入流中的四个对象
            AllStar star1 = (AllStar)ois.readObject();
            AllStar star2 = (AllStar)ois.readObject();
            NbaPlayer player = (NbaPlayer)ois.readObject();
            AllStar star3 = (AllStar)ois.readObject();
            // 输出true
            System.out.println("star1的player引用和player是否相同:"
                + (star1.getPlayer() == player));
            // 输出true
            System.out.println("star2的player引用和player是否相同:"
                + (star2.getPlayer() == player));
            // 输出true
            System.out.println("star2和star3是否是同一个对象:"
                + (star2 == star3));
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

如果多次序列化同一个可变Java对象时,只有第一次序列化时才会把该Java对象转换成字节序列并输出

当使用Java序列化机制序列化可变对象时,只有第一次调用WriteObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream;即使在后面程序中,该对象的实例变量发生了改变,再次调用WriteObject()方法输出该对象时,改变后的实例变量也不会被输出

import java.io.*;

public class SerializeMutable
{
    public static void main(String[] args)
    {

        try(
            // 创建一个ObjectOutputStream输入流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("mutable.txt"));
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("mutable.txt")))
        {
            NbaPlayer player = new NbaPlayer("斯蒂芬库里", 30);
            // 系统会player对象转换字节序列并输出
            oos.writeObject(player);
            // 改变per对象的name实例变量
            player.setName("塞斯库里");
            // 系统只是输出序列化编号,所以改变后的name不会被序列化
            oos.writeObject(player);
            NbaPlayer player1 = (NbaPlayer)ois.readObject();    //①
            NbaPlayer player2 = (NbaPlayer)ois.readObject();    //②
            // 下面输出true,即反序列化后player1等于player2
            System.out.println(player1 == player2);
            // 下面依然看到输出"斯蒂芬库里",即改变后的实例变量没有被序列化
            System.out.println(player2.getName());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

自定义序列化

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

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

在实例变量前面使用transient关键字修饰,可以指定java序列化时无须理会该实例变量

public class NbaPlayer implements java.io.Serializable
{
    private String name;
    private transient int number;
    // 注意此处没有提供无参数的构造器!
    public NbaPlayer(String name, int number)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.number = number;
    }
    
    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
}

transient关键字只能用于修饰实例变量,不可修饰Java程序中的其他成分

import java.io.*;

public class TransientTest
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("transient.txt"));
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("transient.txt")))
        {
            NbaPlayer per = new NbaPlayer("克莱汤普森", 11);
            // 系统会per对象转换字节序列并输出
            oos.writeObject(per);
            NbaPlayer p = (NbaPlayer)ois.readObject();
            System.out.println(p.getNumber());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException

  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

  • private void readObjectNoData() throws ObjectStreamException

writeObject()方法负责写入特定类的实例的状态,以便相应的readObject()方法可以恢复它。通过重写该方法,可以完全获得对序列化机制的控制,自主决定哪些实例变量需要序列化,怎样序列化。在默认情况下,该方法会调用out.defaultWriteObject来保存Java对象的各实例变量,从而可以实现序列化Java对象状态的目的

readObject()方法负责从流中读取并恢复对象实例变量,通过重写该方法,可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,怎样反序列化。在默认情况下,该方法会调用in.defaultReadObject来恢复Java对象的非瞬态实例变量

通常情况下readObject()方法与writeObject()方法对应,如果writeObject()方法中对Java对象的实例变量进行了一些处理,则应该在readObject()方法中对该实例变量进行相应的反处理,以便正确恢复该对象

当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象

import java.io.IOException;

public class NbaPlayer implements java.io.Serializable
{
    private String name;
    private int number;
    // 注意此处没有提供无参数的构造器!
    public NbaPlayer(String name, int number)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.number = number;
    }
    
    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
    
    private void writeObject(java.io.ObjectOutputStream out) throws IOException
    {
        // 将name实例变量值反转后写入二进制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(number);
    }
    
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
    {
        // 将读取的字符串反转后赋给name实例变量
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.number = in.readInt();
    }
}

writeObject()方法存储实例变量的顺序应该和readObject()方法中恢复实例变量的顺序一致,否则将不能正常恢复该Java对象

ANY-ACCESS-MODIFIER Object writeReplace() 实现序列化某个对象时替换该对象

此writeReplace()方法将由序列化机制调用,只要该方法存在。因为该方法可以拥有私有(private),受保护的(protected)和包私有(package-private)等访问权限,所以其子类有可能获得该方法

下面程序的writeReplace()方法,这样可以在写入NbaPlayer对象时将该对象替换成ArrayList

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

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

// 系统将player对象转换字节序列并输出
oos.writeObject(player);
// 反序列化读取得到的是ArrayList
ArrayList list = (ArrayList)ois.readObject();
System.out.println(list);

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

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException实现保护性复制整个对象,紧挨着readObject()之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象将会立即丢弃

Java的序列化机制:首先调用writeReplace(),其次调用writeObject(),最后调用writeResolve()

readObject()方法在序列化单例类,枚举类时尤其有用

反序列化机制在恢复java对象时无须调用构造器来初始化java对象。从这个意义上来看,序列化机制可以用来"克隆"对象;所有单例类,枚举类在实现序列化时都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常;readResolve()方法建议使用final修饰

另一种自定义序列化机制

这种序列化方式完全由程序员决定存储和恢复对象数据。要实现该目标,必须实现Externalizable接口,该接口里定义了如下两个方法:

  • void readExternal(ObjectInput in):需要序列化的类实现readExternal()方法来实现反序列化。该方法调用DataInput(它是ObjectInput的父接口)的方法来恢复基本类型的实例变量值,调用ObjectInput的readObject()方法来恢复引用类型的实例变量值

  • void writeExternal(Object out):需要序列化的类实现该方法来保存对象的状态。该方法调用DataOutput(它是ObjectOutput的父接口)的方法来保存基本类型的实例变量值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值

import java.io.*;

public class Player
    implements java.io.Externalizable
{
    private String name;
    private int number;
    // 注意此处没有提供无参数的构造器!
    public Player(String name, int number)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.number = number;
    }
    // 省略name与number的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }

    public void writeExternal(java.io.ObjectOutput out)
        throws IOException
    {
        // 将name实例变量的值反转后写入二进制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(number);
    }
    public void readExternal(java.io.ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        // 将读取的字符串反转后赋给name实例变量
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.number = in.readInt();
    }
}

两种序列化机制的对比

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

对象序列化的注意事项:

  • 对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量(即static修饰的成员变量)、transient实例变量(也被称为瞬态实例变量)都不会被序列化

  • 反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象所属的class文件,否则会引发ClassNotFoundException异常

  • 实现Serializable接口的类如果需要让某个实例变量不被序列化,则可以在该实例变量前加transient修饰符,而不是static关键字,虽然static关键字也可以达到这个效果,但static关键字不能这样用

  • 保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient修饰该变量

  • 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取

版本

随着项目的设计,系统的class文件也会升级,Java如何保证两个class文件的兼容性?为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的类中加入private static final long serialVersionUID这个属性,具体数值自定义。这样,即使某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确反序列化

如不显式定义该变量值,这个变量值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败

导致该类实例的反序列化失败的类修改操作:

  • 如果修改类时仅仅修改了方法,则反序列化完全不受任何影响,类定义无需修改serizlVersionUID属性值

  • 如果修饰类时仅仅修改了静态属性或瞬态(transient)属性,则反序列化不受任何影响,类定义无需修改serialVersionUID属性值

  • 如果修改类时修饰了非静态、非瞬态属性,则可能导致序列化版本不兼容,如果对象流中的对象和新类中包含同名的属性,而属性类型不同,则反序列化失败 ,类定义应该更新serialVersionUID属性值。如果新类比对象流中对象包含更多的 属性,序列化版本也可以兼容,类定义可以不更新serialVersionUID属性值;但反序列化得到的新对象中多出的属性值都是null(引用类型属性)或0(基本类型属性)

阅读 2.7k

推荐阅读
BigData
用户专栏

Java Hive

10 人关注
52 篇文章
专栏主页