Java 对象序列化

对象序列化

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

序列化的含义和意义

序列化

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

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

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

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

  • Externalizable性能

全部在网络上传输的对象都应该是可序列化的,不然将会出现异常;全部须要保存到磁盘里的对象的类都必须可序列化;程序建立的每一个JavaBean类都实现Serializable;this

使用对象流实现序列化

实现Serializable实现序列化的类,程序能够经过以下两个步骤来序列化该对象:设计

1.建立一个ObjectOutputStream,这个输出流是一个处理流,因此必须创建在其余节点流的基础之上code

// 建立个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(基本类型属性)

相关文章
相关标签/搜索