概述
在日常开发过程中,有些场景下可能需要将应用程序中的对象信息保存在文件中,此时就需要将对象转换为流文件进行保存。这个转化的过程就叫 序列化,Java 本身也是支持序列化的,本篇博客我就来简单介绍下 Java 序列化相关的知识。
Java 序列化与反序列化
首先我们来了解下序列化与反序列化的概念:
-
序列化:内存中保存的数据都是暂时的,如果某个对象需要长期保存,这时就需要将对象转化为流文件写入存储媒介中,这个转化的过程就是序列化
-
反序列化:仅仅写入存储媒介肯定是不够的,当后面我们需要使用时还需要读取流文件并转化为对象,这个反向转化的过程就是反序列化。
Java 代码中通过 Serializable 接口实现对象的序列化:如果某个对象想要序列化,就必须实现该标记接口,实现该接口的对象就可以执行序列化和反序列化操作。
Serializable
Serializable 是 Java 提供的一个空接口,具体示例如下:
public class SerializableTest implements Serializable {
private static final long serialVersionUID = -1364948644692894869L;
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
从上图可以看出,实现 Serializable 接口不需要实现任何方法,只需要实现 serialVersionUID 属性。需要注意的一点是:serialVersionUID 属性是否实现是可选的。即使没有实现该属性,对象仍然可以进行序列化以及反序列化操作。
serialVersionUID:该属性主要用来确保序列化和反序列化的顺利执行。序列化后,流文件中 serialVersionUID 属性和类 serialVersionUID 属性相等时才能正常进行反序列化。具体过程是这样的:
- 序列化过程中,系统将类的 serialVersionUID 属性写入流文件保存到文件中
- 反序列化过程中,根据 serialVersionUID 判断文件是否当前类型,如果 serialVersionUID 属性相等,则可以进行反序列化操作,否则抛出异常。
当系统判断 serialVersionUID 属性不同导致反序列失败时,会抛出如下异常:
java.io.InvalidClassException: com.qhd.worker.SerializableTest;
local class incompatible: stream classdesc serialVersionUID = -2776343930999941918,
local class serialVersionUID = -2776343930999941950
这里我建议在使用序列化时实现 serialVersionUID 属性,这样即使序列化后的流文件收到轻微损坏,也不会影响反序列化的过程。如果不实现该属性的话,哪怕序列化后的流文件多一个空格,也可能造成 serialVersionUID 属性改变,反序列化操作时报。
需要特别注意的一点是:serialVersionUID 属性只是判断的一环,不是说 serialVersionUID 属性相等就一定能反序列化,如果对象属性都不相同的话,反序列化一定也会报错。
序列化、反序列化测试
下面我们通过具体测试用例看看序列化和反序列化的过程:
@Test
public void test() throws IOException, ClassNotFoundException {
SerializableTest s = new SerializableTest();
s.setId(1);
s.setName("liming");
// 序列化过程
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("D://1.txt"));
oo.writeObject(s);
oo.close();
// 反序列化过程
ObjectInputStream oi = new ObjectInputStream(new FileInputStream("D://1.txt"));
SerializableTest ss = (SerializableTest) oi.readObject();
oi.close();
System.out.println(s);
System.out.println(ss);
System.out.println("判断是否相同对象:" + (s == ss ? "True" : "False"));
}
执行结果:
SerializableTest{id=1, name='liming'}
SerializableTest{id=1, name='liming'}
判断是否相同对象:False
其中序列化之后,写入文件的流文件格式如下图所示:
从执行结果可以看出,反序列化后创建了一个全新的对象,并且该对象属性和序列化前对象完全相同,也就是说该对象所有属性都通过了序列化和反序列化的过程。
需要注意的一点是,不是所有类型的属性都可以进行序列化操作,以下两种类型的属性会跳过序列化过程:
- 序列化只针对对象属性,static 修饰的类变量不会进行序列化
- transient 修饰的对象属性也不会进行序列化操作
下面我们看一组新的测试案例:
public class SerializableTest implements Serializable {
private static final long serialVersionUID = -1364948644692894869L;
// 全局变量
private static int staticParam;
private int id;
private String name;
// transient 修饰成员变量
private transient int transientParam;
// 其他方法省略
...
}
@Test
public void test2() throws IOException, ClassNotFoundException {
SerializableTest s = new SerializableTest();
s.setId(1);
s.setName("liming");
s.transientParam = 1;
// 序列化过程
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("D://1.txt"));
oo.writeObject(s);
oo.close();
// 反序列化过程
ObjectInputStream oi = new ObjectInputStream(new FileInputStream("D://1.txt"));
SerializableTest ss = (SerializableTest) oi.readObject();
oi.close();
System.out.println(s);
System.out.println(ss);
}
执行结果:
SerializableTest{transientParam=1, id=1, name='liming'}
SerializableTest{transientParam=0, id=1, name='liming'}
从输出结果可以看出,序列化前后 transientParam 属性值发生变化,也就是说该属性并没有参与到序列化的过程中去,因此反序列化新对象该属性默认设为0。
关于类属性很好理解,因为序列化和反序列化都是针对对象而言,而类属性是所有对象共享的内存模块,因此序列化前后不需要关注 static 类属性变量。
序列化、反序列化与引用属性
上面示例中的属性基本都是常量类型,下面我们来看一个引用类型的示例:
class Node implements Serializable {
NodeParam nodeParam;
public Node(NodeParam nodeParam) {
this.nodeParam = nodeParam;
}
@Override
public String toString() {
return "Node{" +
"nodeParam=" + nodeParam +
'}';
}
}
class NodeParam {
int id;
String name;
public NodeParam(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "NodeParam{" +
"id=" + id +
", name=" + name +
'}';
}
}
@Test
public void test3() throws IOException, ClassNotFoundException {
NodeParam nodeParam = new NodeParam(1, "xiaoming");
Node node = new Node(nodeParam);
// 序列化过程
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("D://1.txt"));
oo.writeObject(node);
oo.close();
// 反序列化过程
ObjectInputStream oi = new ObjectInputStream(new FileInputStream("D://1.txt"));
Node node2 = (Node) oi.readObject();
oi.close();
System.out.println(node);
System.out.println(node2);
System.out.println(node.nodeParam == node2.nodeParam);
}
执行结果:
java.io.NotSerializableException: com.qhd.worker.SerializableTest$NodeParam
通过异常可以看出,nodeParam 属性不能序列化,因此程序报错。解决办法也很简单,只需要让 NodeParam 类实现 Serializable 接口,把该类转换为可序列化的即可:
class NodeParam implements Serializable {
// 省略
}
执行结果:
Node{nodeParam=NodeParam{id=1, name=xiaoming}}
Node{nodeParam=NodeParam{id=1, name=xiaoming}}
false
从执行结果可以看出,对于引用类型变量只需类实现 serializable 接口即可同时序列化。并且需要特别注意的一点是,序列化前后引用类型属性是不相同的,也就是说序列化创建的是对象本身,而不是引用。
重写序列化方法
在 Java 代码中,可以通过实现以下方法重写序列化过程,具体我们看代码:
class NodeA implements Serializable {
private static final long serialVersionUID = -4767927628280154118L;
private int id;
private String name;
public NodeA(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "NodeA{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
/**
* 序列化时,系统首先会先调用 writeReplace方法
* 通过该方法获取需要序列化的对象
*
* @return
* @throws ObjectStreamException
*/
private Object writeReplace() throws ObjectStreamException {
System.out.println("获取需要序列化的对象");
return this;
}
/**
* 读取到序列化对象后,调用 writeObject方法
* 通过该方法序列化对象属性,这里我们只序列化 name 属性
*
* @param out
* @throws IOException
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("序列化对象属性");
out.writeObject(this.name == null ? "qhd" : this.name);
}
/**
* 反序列化时,系统首先会先调用 readObject方法
* 通过该方法反序列化我们之前序列化的属相
*
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
System.out.println("反序列化属性");
this.name = (String) in.readObject();
}
/**
* 反序列化读取属性完毕后,调用 readResolve方法
* 通过该方法将反序列化对象返回
*
* @return
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
System.out.println("返回反序列化创建的新对象");
return this;
}
}
执行结果:
获取需要序列化的对象
序列化对象属性
反序列化属性
返回反序列化创建的新对象
NodeA{id=1, name='liming'}
NodeA{id=0, name='liming'}
从执行结果可以看出,序列化过程中按次序调用了如上方法,并且只序列化了 name 属性。
序列化使用的场景
一般情况下,Java 序列化主要在以下几个场景中使用:
- 内存中的对象写入内存
- 使用套接字通过网络传输对象
- 通过远程方法调用传输对象
不过通常情况下,通过数据库来保存对象数据。序列化主要用于网络传输对象过程中。