您的位置:首页 > 编程语言 > Java开发

Java序列化与反序列化

2016-02-04 17:30 399 查看
目录

一、Java序列化简介

二、什么情况下需要序列化

三、JDK类库中的序列化API

四、简单案例

五、serialVersionUID的作用

六、serialVersionUID的取值

七、static变量序列化

八、transient关键字

九、writeObject()方法与readObject()方法

十、序列化存储规则

十一、父类没有序列化

十二、Externalizable接口

十三、readResolve()方法

一、Java序列化简介

从JDK1.1就存在序列化,序列化是将Java对象转换为字节数组的过程。

反序列化是字节数组转换为Java对象的过程。

二、什么情况下需要序列化

1,需要将java对象存储到磁盘上或数据库中

2,当你想用套接字在网络上传送对象的时候

3,当你想通过RMI传输对象的时候

三、JDK类库中的序列化API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)可将参数obj对象进行序列化,

把得到的字节数组写到一个输出流中。

java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节数组,

再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable或其子接口Externalizable的类的对象才能被序列化。

注意的是:实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。

  对象序列化包括如下步骤:

  1)创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流。

  2)通过对象输出流的writeObject()方法写对象。

  对象反序列化的步骤如下:

  1)创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流。

  2)通过对象输入流的readObject()方法读取对象。

四、简单案例

清单1 显示一个实现Serializable的Cat类。

package cn.rumor.serial;

import java.io.Serializable;

public class Cat implements Serializable {

private String name;

private String gender;

public Cat(String name, String gender) {

this.name = name;

this.gender = gender;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getGender() {

return gender;

}

public void setGender(String gender) {

this.gender = gender;

}

@Override

public String toString() {

return "Cat [name=" + name + ", gender=" + gender + "]";

}

}

清单2 使用Junit进行测试

package cn.rumor.serial;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import org.junit.Test;

public class CatSerTest {

//序列化

@Test

public void testSerial() {

try {

Cat cat = new Cat("rat", "m");

//创建输出流对象

FileOutputStream fos = new FileOutputStream("E:/cat.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(cat);

oos.close();

} catch (Exception e) {

e.printStackTrace();

}

}

//反序列化

@Test

public void testDeSerial() {

try {

//创建输入流对象

FileInputStream fis = new FileInputStream("E:/cat.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

Cat cat2 = (Cat)ois.readObject();

ois.close();

System.out.println(cat2);

} catch (Exception e) {

e.printStackTrace();

}

}

}

依次执行之后,在E盘生成一个cat.ser文件

控制台打印输入:Cat [name=rat, gender=m]

五、serialVersionUID的作用

​字面意思为“序​列​化​的​版​本​号”,只要实现了Serializable接口都要有一个表示序列化版本号的静态变量。

private static final long serialVersionUID;

serialVersionUID有两种生成策略:

采用+Add default serial version ID这种方式生成的serialVersionUID是1L,例如:

private static final long serialVersionUID = 1L;

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

例如:

private static final long serialVersionUID = 4820613318225866253L;

如(四)所示,在Cat类里面并没有serialVersionUID,输出正常。

下面我们修改一下Cat类。如下:

清单3 添加color属性

public class Cat implements Serializable {

//无serialVersionUID

//...

private String color;

//...

}

执行反序列化,控制台报异常:

java.io.InvalidClassException: cn.rumor.serial.Cat;

local class incompatible:

stream classdesc serialVersionUID = 7495499194431377682,

local class serialVersionUID = -7456005194464997324

意思就是说,文件流中的class与classpath中的class不兼容了。

如果没有对Java类指定一个版本号,那么java编译器会自动给这个class进行一个摘要算法,生成一个默认的版本号。

在反序列化的时候,会拿这个版本号与本地的版本号进行比对,如果一致那就没问题,否则报出以上异常。

因此,只要我们指定了一个serialVersionUID,无论是添加或删除一个元素,都不会影响以后的反序列化操作。

下面我们修改一下Cat类,生成serialVersionUID。如下:

清单4 生成serialVersionUID

public class Cat implements Serializable {

//添加serialVersionUID

private static final long serialVersionUID = -7456005194464997324L;

//...

}

重新Junit序列化后,添加或删除某个元素,执行反序列化,No problem!

六、serialVersionUID的取值

serialVersionUID的取值是Java运行时环境根据类的内容细节自动生成的。如果对类作了修改,再重新编译,

新生成的类文件的serialVersionUID有可能也会有变化。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的编译器编译,

有可能会导致不同的serialVersionUID,也有可能相同。

为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。

显式地定义serialVersionUID有两种用途:

1、在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

2、在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

七、static变量序列化

清单5 声明static变量

public class Cat implements Serializable {

//...

//声明static变量

public static int legs = 4;

//...

}

清单6 Junit测试

public class CatSerTest {

//序列化

@Test

public void testSerial() {

try {

Cat cat = new Cat("rat", "m");

//创建输出流对象

FileOutputStream fos = new FileOutputStream("E:/cat.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(cat);

oos.close();

//修改static变量

cat.legs = 5;

//创建输入流对象

FileInputStream fis = new FileInputStream("E:/cat.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

Cat cat2 = (Cat)ois.readObject();

ois.close();

System.out.println(cat2.legs);

} catch (Exception e) {

e.printStackTrace();

}

}

}

清单6中的testSerial方法,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,

然后通过读取出来的对象获得静态变量的数值并打印出来。

依照清单6,这个System.out.println(cat2.legs)语句输出的是4还是5呢?

最后的输出是5,对于无法理解的读者认为,打印的legs是从读取的对象里获得的,应该是保存时的状态才对。之所以打印5的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。

八、transient关键字

当某个字段被声明为transient后,默认序列化机制就会忽略该字段。

清单7

public class Cat implements Serializable {

//...

private transient String hello;

//...

@Override

public String toString() {

return "Cat [name=" + name + ", gender=" + gender + ", hello = " + hello + "]";

}

}

控制台打印输入:Cat [name=rat, gender=m, hello=null]

可见,hello字段未被序列化。

九、 writeObject()方法与readObject()方法

1)对敏感数据加密

情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,清单8展示了这个过程。

清单8 字段加密

package cn.rumor.serial;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectInputStream.GetField;

import java.io.ObjectOutputStream;

import java.io.ObjectOutputStream.PutField;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 1L;

private transient String username;

private String password = "pass";

//set/get method.

private void writeObject(ObjectOutputStream out) {

try {

PutField putField = out.putFields();

System.out.println("原密码:" + password);

password = "encryption"; //模拟加密

putField.put("password", password);

System.out.println("加密后的密码:" + password);

out.writeFields();

} catch (IOException e) {

e.printStackTrace();

}

}

private void readObject(ObjectInputStream in) {

try {

GetField getField = in.readFields();

Object object = getField.get("password", "");

System.out.println("要解密的字符串:" + object.toString());

password = "pass"; //模拟解密,需要本地的密钥

} catch (IOException e) {

e.printStackTrace();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

}

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/user.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(new User());

oos.close();

FileInputStream fis = new FileInputStream("E:/user.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

User user = (User)ois.readObject();

ois.close();

System.out.println("解密后的字符串:"+user.getPassword());

}

}

执行main方法,控制台输出:

原密码:pass

加密后的密码:encryption

要解密的字符串:encryption

解密后的字符串:pass

特性使用案例

RMI 技术是完全基于 Java 序列化技术的,服务器端接口调用所需要的参数对象来至于客户端,它们通过网络相互传输。这就涉及 RMI 的安全传输的问题。一些敏感的字段,如用户名密码(用户登录时需要对密码进行传输),我们希望对其进行加密,这时,就可以采用本节介绍的方法在客户端对密码进行加密,服务器端进行解密,确保数据传输的安全性。

1)序列化transitive字段

package cn.rumor.serial;

import java.io.*;

public class User implements Serializable {

private static final long serialVersionUID = 1L;

private transient String username;

private String password;

public User(String username, String password) {

this.username = username;

this.password = password;

}

//set/get method.

private void writeObject(ObjectOutputStream out) throws IOException {

out.defaultWriteObject();

out.writeUTF(username);

}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

in.defaultReadObject();

username = in.readUTF();

}

@Override

public String toString() {

return "User [username=" + username + ", password=" + password + "]";

}

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/user.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(new User("u1", "p1"));

oos.close();

FileInputStream fis = new FileInputStream("E:/user.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

User user = (User)ois.readObject();

ois.close();

System.out.println(user);

}

}

在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉username字段。然后再调用writeUTF()方法显示地将username字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。

执行main方法,控制台输出:

User [username=u1, password=p1]

必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?毫无疑问,是使用反射。详情可见ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。

十、序列化存储规则

清单9 存储规则问题代码

package cn.rumor.serial;

import java.io.*;

public class User implements Serializable {

private static final long serialVersionUID = 1L;

private transient String username;

private String password;

//set/get method.

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/user.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();

//试图两次写入

oos.writeObject(user); //第一次写

oos.flush();

System.out.println(new File("E:/user.ser").length());

oos.writeObject(user); //第二次写

System.out.println(new File("E:/user.ser").length());

oos.close();

FileInputStream fis = new FileInputStream("E:/user.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

//从文件依次读出两个文件

User user1 = (User)ois.readObject();

User user2 = (User)ois.readObject();

ois.close();

//判断两个引用是否指向同一个对象

System.out.println(user1 == user2);

}

}

清单9中对同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,然后从文件中反序列化出两个对象,比较这两个对象是否为同一对象。一般的思维是,两次写入对象,文件大小会变为两倍的大小,反序列化时,由于从文件读取,生成了两个对象,判断相等时应该是输入 false 才对,但是最后结果输出:

#console

80

85

true

我们看到,第二次写入对象时文件只增加了 5 字节,并且两个对象是相等的,这是为什么呢?

解答:Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

特性案例分析

清单10

package cn.rumor.serial;

import java.io.*;

public class User implements Serializable {

private static final long serialVersionUID = 1L;

public int i;

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/user.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();

user.i = 1; //为变量赋值

oos.writeObject(user); //第一次写

oos.flush();

user.i = 2; //修改变量值

oos.writeObject(user); //第二次写

oos.close();

FileInputStream fis = new FileInputStream("E:/user.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

//从文件依次读出两个文件

User user1 = (User)ois.readObject();

User user2 = (User)ois.readObject();

ois.close();

//打印

System.out.println(user1.i); //1

System.out.println(user2.i); //1

}

}

清单10的目的是希望将 user 对象两次保存到 user.ser 文件中,写入一次以后修改对象属性值再次保存第二次,然后从 user.ser 中再依次读出两个对象,输出这两个对象的 i 属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。

结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。

十一、父类没有序列化

如果子类实现Serializable接口,而父类没有实现。那么序列化子类时,父类不会被序列化。

反序列化后访问父类中的变量时,则为默认值,int为0,对象为null。

如果希望改变父类中的变量值,刚通过父类中的无参构造函数为变量初赋值。

十二、Externalizable接口

无论是使用transient关键字,还是使用writeObject()和readObject()方法,都是基于Serializable接口的序列化。

JDK中提供了另一个序列化接口--Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。

此时将User类修改成如下

清单11 实现Externalizable接口

package cn.rumor.serial;

import java.io.*;

public class User implements Externalizable {

private static final long serialVersionUID = 1L;

private transient String username;

private String password;

public User() {

System.out.println("no-arg constructor");

}

public User(String username, String password) {

System.out.println("constructor");

this.username = username;

this.password = password;

}

//set/get

private void writeObject(ObjectOutputStream out) throws IOException {

out.defaultWriteObject();

out.writeObject(username);

}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

in.defaultReadObject();

username = (String)in.readObject();

}

public void writeExternal(ObjectOutput out) throws IOException {

}

public void readExternal(ObjectInput in) throws IOException,

ClassNotFoundException {

}

@Override

public String toString() {

return "User [username=" + username + ", password=" + password + "]";

}

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/user.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(new User("u1", "p1"));

oos.close();

FileInputStream fis = new FileInputStream("E:/user.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

User user = (User)ois.readObject();

ois.close();

System.out.println(user);

}

}

控制台输出:

constructor

no-arg constructor

User [username=null, password=null]

从该结果,一方面可以看出User对象中任何一个字段都没有被序列化。另一方面,如果细心的话,还可以发现这此次序列化过程调用了Person类的无参构造器。

Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。如上所示的代码,由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。

另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。

对上述User类作进一步的修改,使其能够对username与password字段进行序列化,但要忽略掉gender字段,

如下代码所示:

清单12 修改User类

package cn.rumor.serial;

import java.io.*;

public class User implements Externalizable {

private static final long serialVersionUID = 1L;

private transient String username;

private String password;

public User() {

System.out.println("no-arg constructor");

}

public User(String username, String password) {

System.out.println("constructor");

this.username = username;

this.password = password;

}

//set/get

private void writeObject(ObjectOutputStream out) throws IOException {

out.defaultWriteObject();

out.writeObject(username);

}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

in.defaultReadObject();

username = (String)in.readObject();

}

public void writeExternal(ObjectOutput out) throws IOException {

}

public void readExternal(ObjectInput in) throws IOException,

ClassNotFoundException {

}

@Override

public String toString() {

return "User [username=" + username + ", password=" + password + "]";

}

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/user.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(new User("u1", "p1"));

oos.close();

FileInputStream fis = new FileInputStream("E:/user.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

User user = (User)ois.readObject();

ois.close();

System.out.println(user);

}

}

控制台输出:

constructor

no-arg constructor

User [username=p1, password=u1]

十三、readResolve()方法

当我们使用Singlton模式时,期望某个类的实例为唯一的,当这个类可以序列化时,情况略有不同。

清单13 新建Person.java

package cn.rumor.serial;

import java.io.*;

public class Person implements Serializable {

private static class InstanceHolder {

private static final Person instance = new Person("victor", 18);

}

public static Person getInstance() {

return InstanceHolder.instance;

}

private static final long serialVersionUID = 1L;

private String name;

private Integer age;

public Person() {

System.out.println("no-arg constructor");

}

public Person(String name, Integer age) {

System.out.println("constructor");

this.name = name;

this.age = age;

}

@Override

public String toString() {

return "User [name=" + name + ", age=" + age + "]";

}

public static void main(String[] args) throws Exception {

FileOutputStream fos = new FileOutputStream("E:/person.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

//保存单例对象

oos.writeObject(Person.getInstance());

oos.close();

FileInputStream fis = new FileInputStream("E:/person.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

Person newPerson = (Person)ois.readObject();

ois.close();

System.out.println(newPerson);

//将获取的对象与Person类中的单例对象进行相等性比较

System.out.println(Person.getInstance() == newPerson);

}

}

控制台输出:

constructor

User [name=victor, age=18]

false

发现了没,从流中读取的实例与Person.getInstance实例不是同一个。

为了能序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法。

package cn.rumor.serial;

import java.io.*;

public class Person implements Serializable {

//...

private Object readResolve() throws ObjectStreamException {

return InstanceHolder.instance;

}

//...

}

控制台输出:

constructor

User [name=victor, age=18]

true

无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。

实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。

整理自网络,自测通过!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: