Java序列化与反序列化

目录
一、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()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。


整理自网络,自测通过!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值