Java序列化和反序列化

发布于 2017-10-27 | 作者: BenjaminWu | 来源: benjaminwhx.com | 转载于: benjaminwhx.com

序列化和反序列化的概念

把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

JDK类库中的序列化API


java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。

对象序列化和反序列范例:

定义一个Person类:

package map;

import java.io.Serializable;

/**
 * Created by benjamin on 12/5/15.
 */
public class Person implements Serializable{


    private static final long serialVersionUID = -3751291995104363537L;
    private int age;
    private String name;
    private String sex;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}
package map;

import java.io.*;

/**
 * Created by piqiu on 12/8/15.
 */
public class SerializableTest {

    private static final String DISK_PATH = "/Users/piqiu1/Person";

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person p = new Person();
        p.setName("benjamin");
        p.setAge(24);
        p.setSex("man");
        serializePerson(p);

        Person p2 = deserializePerson();
        System.out.println("name: " + p2.getName() + ", age: " + p2.getAge() + ", sex: " + p2.getSex());
    }

    private static void serializePerson(Person p) throws IOException {
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File(DISK_PATH)));
        oo.writeObject(p);
        System.out.println("Person序列化成功");
        oo.close();
    }

    private static Person deserializePerson() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(DISK_PATH)));
        Person p = (Person)ois.readObject();
        System.out.println("Person反序列化成功");
        return p;
    }
}

执行结果:

Person序列化成功
Person反序列化成功
name: benjamin, age: 24, sex: man

我们设定的路径下面也生成了序列化的文件。

serialVersionUID的作用


s​e​r​i​a​l​V​e​r​s​i​o​n​U​I​D​:​ ​字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量

private static final long serialVersionUID

一般如果在eclipse或者myeclipse中不加版本号会出现警告。
采用 +Add default serial version ID这种方式生成的serialVersionUID是1L,例如:

private static final long serialVersionUID = 1L;

采用+Add generated serial version ID这种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如:

private static final long serialVersionUID = 4603642343377807741L;

添加了之后就不会出现警告了。

在IDEA编辑器中可能会有的人不加版本号也不会警告,这是因为配置的问题。

打开Preferences -> 搜索serialVersionUID -> 勾选serializable class without serialVersionUID -> Apply

这样就会出现提示了。

那么serialVersionUID(序列化版本号)到底有什么用呢?
如果不加serialVersionUID,也是可以正常进行序列化,但是如果以后项目中需要对实体类进行增减字段的话,再进行反序列化就会报错,错误为:

Exception in thread "main" java.io.InvalidClassException: Customer; 
  local class incompatible: 
  stream classdesc serialVersionUID = -88175599799432325, 
  local class serialVersionUID = -5182532647273106745


serialVersionUID的取值

serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
  类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定serialVersionUID,为它赋予明确的值。

显式地定义serialVersionUID有两种用途:
1、 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
  
参考链接:http://www.cnblogs.com/xdp-gacl/p/3777987.html

readObjectNoData的使用


实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。
如果你实现了一个带有实例域的类,它是可序列化和可扩展的,你就应该担心这样一条告诫。如果类有一些约束条件,当类的实例域被初始化成它们的默认值(整数类型为0,boolean为false,对象引用类型为null)时,就会违背这些约束条件,这时候你就必须给这个类添加这个readObjectNoData方法:

private void readObjectNoData() throws InvalidObjectException {
    throw new InvalidObjectException("Stream data required");
}

下面举出一个例子来更好的理解何时和如何使用:

public class Person implements Serializable {
    private static final long serialVersionUID = -1046907702282365423L;

    private int age;

    public Person(){}

    public int getAge() {
        return age;
    }

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

    private static final String fileUrl = "/Users/piqiu1/Desktop/seralize.txt";

    public static void main(String[] args) {
        write();
//        read();
    }

    private static void write() {
        Person p = new Person();
        p.setAge(10);
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(new File(fileUrl)));
            oos.writeObject(p);
            oos.flush();
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void read() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(new File(fileUrl)));
            Person p = (Person)ois.readObject();
            System.out.println(p.getAge());
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上面是一个简单的序列化和反序列化的例子,现在我们在原来Person类的基础上,继承一个类Animals:

public class Animals implements Serializable {
    private static final long serialVersionUID = 2768202150914525915L;

    private String name;

    public Animals(){}

    public String getName() {
        return name;
    }

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

同时让Person继承它

public class Person extends Animals implements Serializable {
    private static final long serialVersionUID = -1046907702282365423L;

    private int age;

    public Person(){}

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
private static void read() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(new File(fileUrl)));
            Person p = (Person)ois.readObject();
            System.out.println(p.getAge() + " " + p.getName());
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

上面read的时候输出的p.getName()为空。这里我们就要使用readObjectNoData方法了,在Animals类中加入下面的方法:

private void readObjectNoData() {
	this.name = "benjamin";
}

这样再执行,就能够输出我们给设置的默认值了。

考虑使用自定义序列化形式


考虑以一个对象为根的对象图,相对于它的物理表示法而言,该对象的默认序列化形式是一种比较有效的编码形式。换句话说,默认的序列化形式描述了该对象内部所包含的数据,以及没一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被连接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象锁表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。

public class Name implements Serializable {

    private final String lastName;
    
    private final String firstName;
    
    private final String middleName;
    
    ...//
}

从逻辑的角度而言,一个名字包含三个字符串,分别代表姓、名和中间名。Name中的实例域精确地反映了它的逻辑内容。
下面的例子与Name不同,它是另一个极端,该类表示了一个字符串列表:

public class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    
    ... //
}

当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
1、它使这个类的导出API永远地束缚在该类的内部表示法上。
2、它会消耗过多的空间
3、它会消耗过多的时间:序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程。在上面的例子中,沿着next引用进行遍历是非常简单的。
4、它会引起栈溢出:默认的序列化过程要对对象图执行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能会引起栈溢出。在我的机器上,如果StringList实例包含1258个元素,对它进行序列化就会导致栈溢出。到底多少个元素就会引发栈溢出,这要取决于JVM的具体实现以及Java启动时的命令行参数,(比如Heap Size的-Xms与-Xmx的值)有些实现可能根本不存在这样的问题

下面我们使用了writeObject和readObject方法和transient修饰符来改变了这个方法的实现(transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉)

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }

    public final void add(String s) {...}

    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);

        //以正确的顺序写出所有元素
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt();

        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }
}

我们还可以利用writeObject和readObject来进行模糊化序列化数据的操作。

假设我们有一个Person类要进行序列化操作,但是里面有一个age字段是敏感数据,毕竟女士忌谈年龄。我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(你可以用更安全的算法代替)

package Effective;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Created by piqiu on 2/5/16.
 */
public class Person extends Animals implements Serializable {
    private static final long serialVersionUID = -1046907702282365423L;

    private String firstName;
    private String lastName;
    private int age;
    private Person spouse;

    public Person(String fn, String ln, int a) {
        this.firstName = fn;
        this.lastName = ln;
        this.age = a;
    }

    private void writeObject(ObjectOutputStream stream) throws IOException {
        // 加密数据
        age = age << 2;
        stream.defaultWriteObject();
    }

    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        // 解密数据
        age = age << 2;
    }

    @Override
    public String toString() {
        return "[Person: firstName=" + firstName + " lastName=" + lastName + " age=" + age +
                " spouse=" + (spouse != null ? spouse.getFirstName() : "[null]") + "]";
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

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

    public Person getSpouse() {
        return spouse;
    }

    public void setSpouse(Person spouse) {
        this.spouse = spouse;
    }
}

writeReplace和readResolve的使用


序列化非常强大,我们可以通过流把对象保存在磁盘上,再从磁盘上读取转为对象,但是如果单例的对象这么转换过后可就不是单例了,为了防止这种情况发生,我们可以在单例中加入readResolve这个方法来保证单例的可靠性。

package Effective;

import java.io.Serializable;

/**
 * Created by piqiu on 2/5/16.
 */
public class MySingletion implements Serializable {
    private static final long serialVersionUID = -2786296717146940199L;

    private MySingletion(){}

    private static final MySingletion instance = new MySingletion();

    public static MySingletion getInstance() {
        return instance;
    }

    private Object readResolve() {
        return instance;
    }
}

很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。
如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。
writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

class PersonProxy implements java.io.Serializable
{
    public PersonProxy(Person orig)
    {
        data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge();
        if (orig.getSpouse() != null)
        {
            Person spouse = orig.getSpouse();
            data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + ","  
              + spouse.getAge();
        }
    }

    public String data;
    private Object readResolve()
        throws java.io.ObjectStreamException
    {
        String[] pieces = data.split(",");
        Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2]));
        if (pieces.length > 3)
        {
            result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt
              (pieces[5])));
            result.getSpouse().setSpouse(result);
        }
        return result;
    }
}
public class Person implements java.io.Serializable
{
    public Person(String fn, String ln, int a)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
    }

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public Person getSpouse() { return spouse; }

    private Object writeReplace()
        throws java.io.ObjectStreamException
    {
        return new PersonProxy(this);
    }
    
    public void setFirstName(String value) { firstName = value; }
    public void setLastName(String value) { lastName = value; }
    public void setAge(int value) { age = value; }
    public void setSpouse(Person value) { spouse = value; }   

    public String toString()
    {
        return "[Person: firstName=" + firstName + 
            " lastName=" + lastName +
            " age=" + age +
            " spouse=" + spouse.getFirstName() +
            "]";
    }    
    
    private String firstName;
    private String lastName;
    private int age;
    private Person spouse;
}

注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。
这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。