在这篇文章里,我们关注对象序列化。

  首先,我们来讨论一下什么是序列化以及序列化的原理;然后给出一个简单的示例来演示序列化和反序列化;有时有些信息是不应该被序列化的,我们应该如何控制;我们如何去自定义序列化内容;最后我们讨论一下在继承结构的场景中,序列化需要注意哪些内容。

  序列化概述

  序列化,简单来讲,就是以“流”的方式来保存对象,至于保存的目标地址,可以是文件,可以是数据库,也可以是网络,即通过网络将对象从一个节点传递到另一个节点。

  我们知道在Java的I/O结构中,有ObjectOutputStream和ObjectInputStream,它们可以实现将对象输出为 二进制流,并从二进制流中获取对象,那为什么还需要序列化呢?这需要从Java变量的存储结构谈起,我们知道对Java来说,基础类型存储在栈上,复杂类 型(引用类型)存储在堆中,对于基础类型来说,上述的操作时可行的,但对复杂类型来说,上述操作过程中,可能会产生重复的对象,造成错误。

  而序列化的工作流程如下:

  1)通过输出流保存的对象都有一个唯一的序列号。

  2)当一个对象需要保存时,先对其序列号进行检查。

  3)当保存的对象中已包含该序列号时,不需要再次保存,否则,进入正常保存的流程。

  正是通过序列号的机制,序列化才可以完整准确的保存对象的各个状态。

  序列化保存的是对象中的各个属性的值,而不是方法或者方法签名之类的信息。对于方法或者方法签名,只要JVM能够找到正确的ClassLoader,那么就可以invoke方法。

  序列化不会保存类的静态变量,因为静态变量是作用于类型,而序列化作用于对象。

  简单的序列化示例

  序列化的完整过程包括两部分:

  1)使用ObjectOutputStream将对象保存为二进制流,这一步叫做“序列化”。

  2)使用ObjectInputStream将二进制流转换成对象,这一步叫做“反序列化”。

  下面我们来演示一个简单的示例,首先定义一个Person对象,它包含name和age两个信息。

 
 class Person implements Serializable
{
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
} public String toString()
{
return "Name:" + name + "; Age:" + age;
}
}
 

  然后是两个公共方法,用来完成读、写对象的操作:

 
 private static void writeObject(Object obj, String filePath)
{
try
{
FileOutputStream fos = new FileOutputStream(filePath);
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.flush();
fos.flush();
os.close();
fos.close();
System.out.println("序列化成功。");
}
catch(Exception ex)
{
ex.printStackTrace();
}
} private static Object readObject(String filePath)
{
try
{
FileInputStream fis = new FileInputStream(filePath);
ObjectInputStream is = new ObjectInputStream(fis); Object temp = is.readObject(); fis.close();
is.close(); if (temp != null)
{
System.out.println("反序列化成功。");
return temp;
}
}
catch(Exception ex)
{
ex.printStackTrace();
} return null;
}
 

  这里,我们将对象保存的二进制流输出到磁盘文件中。

  接下来,我们首先来看“序列化”的方法:

 
 private static void serializeTest1()
{
Person person = new Person();
person.setName("Zhang San");
person.setAge(30);
System.out.println(person);
writeObject(person, "d:\\temp\\test\\person.obj");
}
 

  我们定义了一个Person实例,然后将其保存到d:\temp\test\person.obj中。

  最后,是“反序列化”的方法:

 
 private static void deserializeTest1()
{
Person temp = (Person)readObject("d:\\temp\\test\\person.obj"); if (temp != null)
{
System.out.println(temp);
}
}
 

  它从d:\temp\test\person.obj中读取对象,然后进行输出。

  上述两个方法的执行结果如下:

Name:Zhang San; Age:30
序列化成功。
反序列化成功。
Name:Zhang San; Age:30

  可以看出,读取的对象和保存的对象是完全一致的。

  隐藏非序列化信息

  有时,我们的业务对象中会包含很多属性,而有些属性是比较隐私的,例如年龄、银行卡号等,这些信息是不太适合进行序列化的,特别是在需要通过网络来传输对象信息时,这些敏感信息很容易被窃取。

  Java使用transient关键字来处理这种情况,针对那些敏感的属性,我们只需使用该关键字进行修饰,那么在序列化时,对应的属性值就不会被保存。

我们还是看一个实例,这次我们定义一个新的Person2,其中age信息是我们不希望序列化的:

 
 class Person2 implements Serializable
{
private String name;
private transient int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
} public String toString()
{
return "Name:" + name + "; Age:" + age;
}
}
 

  注意age的声明语句:

 private transient int age;

  下面是“序列化”和“反序列化”的方法:

 
 private static void serializeTest2()
{
Person2 person = new Person2();
person.setName("Zhang San");
person.setAge(30);
System.out.println(person);
writeObject(person, "d:\\temp\\test\\person2.obj");
} private static void deserializeTest2()
{
Person2 temp = (Person2)readObject("d:\\temp\\test\\person2.obj"); if (temp != null)
{
System.out.println(temp);
}
}
 

  它的输出结果如下:

Name:Zhang San; Age:30
序列化成功。
反序列化成功。
Name:Zhang San; Age:0

  可以看到经过反序列化的对象,age的信息变成了Integer的默认值0。

  自定义序列化过程

  我们可以对序列化的过程进行定制,进行更细粒度的控制。

  思路是在业务模型中添加readObject和writeObject方法。下面看一个实例,我们新建一个类型,叫Person3:

 
 class Person3 implements Serializable
{
private String name;
private transient int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
} public String toString()
{
return "Name:" + name + "; Age:" + age;
} private void writeObject(ObjectOutputStream os)
{
try
{
os.defaultWriteObject();
os.writeObject(this.age);
System.out.println(this);
System.out.println("序列化成功。");
}
catch(Exception ex)
{
ex.printStackTrace();
}
} private void readObject(ObjectInputStream is)
{
try
{
is.defaultReadObject();
this.setAge(((Integer)is.readObject()).intValue() - 1);
System.out.println("反序列化成功。");
System.out.println(this);
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
 

  请注意观察readObject和writeObject方法,它们都是private的,接受的参数是ObjectStream,然后在方法体内调用了defaultReadObject或者defaultWriteObject方法。

  这里age同样是transient的,但是在保存对象的过程中,我们单独对其进行了保存,在读取时,我们将age信息读取出来,并进行了减1处理。

  下面是测试方法:

 
 private static void serializeTest3()
{
Person3 person = new Person3();
person.setName("Zhang San");
person.setAge(30);
System.out.println(person);
try
{
FileOutputStream fos = new FileOutputStream("d:\\temp\\test\\person3.obj");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(person);
fos.close();
os.close();
}
catch(Exception ex)
{
ex.printStackTrace();
}
} private static void deserializeTest3()
{
try
{
FileInputStream fis = new FileInputStream("d:\\temp\\test\\person3.obj");
ObjectInputStream is = new ObjectInputStream(fis);
is.readObject();
fis.close();
is.close();
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
 

  输出结果如下:

Name:Zhang San; Age:30
序列化成功。
反序列化成功。
Name:Zhang San; Age:29

  可以看到,经过反序列化得到的对象,其age属性已经减1。

  探讨serialVersionUID

  在上文中,我们描述序列化原理时,曾经提及每个对象都会有一个唯一的序列号,这个序列号,就是serialVersionUID。

  当我们的对象实现Serializable接口时,该接口可以为我们生成serialVersionUID。

  有两种方式来生成serialVersionUID,一种是固定值:1L,一种是经过JVM计算,不同的JVM采取的计算算法可能不同。

  下面就是两个serialVersionUID的示例:

 private static final long serialVersionUID = 1L;
private static final long serialVersionUID = -2380764581294638541L;

  第一行是采用固定值生成的;第二行是JVM经过计算得出的。

  那么serialVersionUID还有其他用途吗?

  我们可以使用它来控制版本兼容。如果采用JVM生成的方式,我们可以看到,当我们业务对象的代码保持不变时,多次生成的 serialVersionUID也是不变的,当我们对属性进行修改时,重新生成的serialVersionUID会发生变化,当我们对方法进行修改 时,serialVersionUID不变。这也从另一个侧面说明,序列化是作用于对象属性上的。

  当我们先定义了业务对象,然后对其示例进行了“序列化”,这时根据业务需求,我们修改了业务对象,那么之前“序列化”后的内容还能经过“反序列 化”返回到系统中吗?这取决于业务对象是否定义了serialVersionUID,如果定义了,那么是可以返回的,如果没有定义,会抛出异常。

  来看下面的示例,定义新的类型Person4:

 
 class Person4 implements Serializable
{
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
private void xxx(){} public String toString()
{
return "Name:" + name + "; Age:" + age;
}
}
 

  然后运行下面的方法:

 
 private static void serializeTest4()
{
Person4 person = new Person4();
person.setName("Zhang San");
person.setAge(30); writeObject(person, "d:\\temp\\test\\person4.obj");
}
 

  接下来修改Person4,追加address属性:

 
 class Person4 implements Serializable
{
private String name;
private int age;
private String address;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
private void xxx(){} public String toString()
{
return "Name:" + name + "; Age:" + age;
}
public void setAddress(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
}
 

  然后运行“反序列化”方法:

 
 private static void deserializeTest4()
{
Person4 temp = (Person4)readObject("d:\\temp\\test\\person4.obj"); if (temp != null)
{
System.out.println(temp);
}
}
 

  可以看到,运行结果如下:

 
java.io.InvalidClassException: sample.serialization.Person4; local class incompatible: stream classdesc serialVersionUID = -2380764581294638541, local class serialVersionUID = -473458100724786987
at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
at java.io.ObjectInputStream.readClassDesc(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at sample.serialization.Sample.readObject(Sample.java:158)
at sample.serialization.Sample.deserializeTest4(Sample.java:105)
at sample.serialization.Sample.main(Sample.java:16)
 

  但是当我们在Person4中添加serialVersionUID后,再次执行上述各步骤,得出的运行结果如下:

反序列化成功。
Name:Zhang San; Age:30

  有继承结构的序列化

  业务对象会产生继承,这在管理系统中是经常看到的,如果我们有下面的业务对象:

 
 class Person5
{
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
} public String toString()
{
return "Name:" + name + "; Age:" + age;
} public Person5(String name, int age)
{
this.name = name;
this.age = age;
}
} class Employee extends Person5 implements Serializable
{
public Employee(String name, int age) {
super(name, age);
} private String companyName; public void setCompanyName(String companyName) {
this.companyName = companyName;
} public String getCompanyName() {
return companyName;
} public String toString()
{
return "Name:" + super.getName() + "; Age:" + super.getAge() + "; Company:" + this.companyName;
}
}
 

  Employee继承在Person5,Employee实现了Serializable接口,Person5没有实现,那么运行下面的方法:

 
 private static void serializeTest5()
{
Employee emp = new Employee("Zhang San", 30);
emp.setCompanyName("XXX"); writeObject(emp, "d:\\temp\\test\\employee.obj");
} private static void deserializeTest5()
{
Employee temp = (Employee)readObject("d:\\temp\\test\\employee.obj"); if (temp != null)
{
System.out.println(temp);
}
}
 

  会正常运行吗?事实上不会,它会抛出如下异常:

 
java.io.InvalidClassException: sample.serialization.Employee; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at sample.serialization.Sample.readObject(Sample.java:158)
at sample.serialization.Sample.deserializeTest5(Sample.java:123)
at sample.serialization.Sample.main(Sample.java:18)
 

  原因:在有继承层次的业务对象,进行序列化时,如果父类没有实现Serializable接口,那么父类必须提供默认构造函数

  我们为Person5添加如下默认构造函数:

 public Person5()
{
this.name = "Test";
this.age = 1;
}

  再次运行上述代码,结果如下:

Name:Zhang San; Age:30; Company:XXX
序列化成功。
反序列化成功。
Name:Test; Age:1; Company:XXX

  可以看到,反序列化后的结果,父类中的属性,已经被父类构造函数中的赋值代替了!

  因此,我们推荐在有继承层次的业务对象进行序列化时,父类也应该实现Serializable接口。我们对Person5进行修改,使其实现Serializable接口,执行结果如下:

Name:Zhang San; Age:30; Company:XXX
序列化成功。
反序列化成功。
Name:Zhang San; Age:30; Company:XXX

  这正是我们期望的结果。

Java回顾之序列化的更多相关文章

  1. Java回顾之Spring基础

    第一篇:Java回顾之I/O 第二篇:Java回顾之网络通信 第三篇:Java回顾之多线程 第四篇:Java回顾之多线程同步 第五篇:Java回顾之集合 第六篇:Java回顾之序列化 第七篇:Java ...

  2. 【Java基础】序列化与反序列化深入分析

    一.前言 复习Java基础知识点的序列化与反序列化过程,整理了如下学习笔记. 二.为什么需要序列化与反序列化 程序运行时,只要需要,对象可以一直存在,并且我们可以随时访问对象的一些状态信息,如果程序终 ...

  3. 对Java Serializable(序列化)的理解和总结

    我对Java Serializable(序列化)的理解和总结 博客分类: Java技术 JavaOSSocketCC++  1.序列化是干什么的?       简单说就是为了保存在内存中的各种对象的状 ...

  4. java.io.Serializable 序列化问题

    java.io.Serializable 序列化问题 Person.java package a.b.c; public class Person implements java.io.Seriali ...

  5. java对象的序列化与反序列化使用

    1.Java序列化与反序列化  Java序列化是指把Java对象转换为字节序列的过程:而Java反序列化是指把字节序列恢复为Java对象的过程. 2.为什么需要序列化与反序列化 我们知道,当两个进程进 ...

  6. Java transient关键字序列化时使用小记

    1. transient的作用及使用方法 我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过 ...

  7. 【Java】Java原生的序列化和反序列化

    写一个Java原生的序列化和反序列化的DEMO. 需序列化的类: package com.nicchagil.nativeserialize; import java.io.Serializable; ...

  8. Java对象的序列化与反序列化

    序列化与反序列化 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程.一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等.在网络传输过程中,可以是字节或是 ...

  9. Java对象的序列化和反序列化[转]

    Java基础学习总结--Java对象的序列化和反序列化 一.序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化.把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用 ...

随机推荐

  1. etc/fstab

    etc/fstab 就是在开机引导的时候自动挂载到linux的文件系统 设备名称 挂载点 分区的类型 挂载选项 dump选项 fsck选项UUID=ce25cdc7-434f-420b-b3 / ex ...

  2. 三维凸包求凸包表面的个数(HDU3662)

    3D Convex Hull Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) T ...

  3. Linux 搭建Nginx并添加配置 SSL 证书

    1. 安装准备   1.1 gcc安装 安装 nginx 需要先将官网下载的源码进行编译,编译依赖 gcc 环境,如果没有 gcc 环境,则需要安装: [root@nginx ~]# yum -y i ...

  4. #pragma once含义及用法

    #pragma once是一个比较常用的C/C++杂注,只要在头文件的最开始加入这条杂注,就能够保证头文件只被编译一次. #pragma once是编译器相关的,有的编译器支持,有的编译器不支持,具体 ...

  5. C++中的.和::和:和->的区别

    在学习C++的过程中我们经常会用到.和::和:和->,在此整理一下这些常用符号的区别. 1.A.B则A为对象或者结构体: 2.A->B则A为指针,->是成员提取,A->B是提取 ...

  6. Python量化教程 常用函数

    # -*- coding: utf-8 -*- # @Author: fangbei # @Date: 2017-08-26 # @Original: price_str = '30.14, 29.5 ...

  7. Django - 常用配置

    一.logging配置 Django项目常用的logging配置 settings.py LOGGING = { 'version': 1, 'disable_existing_loggers': F ...

  8. python模拟websocket握手过程中计算sec-websocket-accept

    背景 以前,很多网站使用轮询实现推送技术.轮询是在特定的的时间间隔(比如1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给浏览器.轮询的缺点很明显,浏览器需要不断的向服 ...

  9. rest_framework知识总汇

    RESTful规范 rest_framework基础 rest_framework基本组件(权限.认证.频率) rest_framework渲染器 rest_framework版本控制 解析器.路由控 ...

  10. Disruptor的伪共享解决方案

    1.术语 术语 英文单词 描述 内存屏障 Memory Barriers 是一组处理器指令,用于实现对内存操作的顺序限制. In the Java Memory Model a volatile fi ...