一、对象的串行化(Serialization)
串行化的概念和目的
1.什么是串行化
对象的寿命通常随着生成该对象的程序的终止而终止。有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。我们把对象的这种能记录自己的状态以便将来再生的能力。叫作对象的持续性(persistence)。对象通过写出描述自己状态的数值来记录自己 ,这个过程叫对象的串行化(Serialization) 。串行化的主要任务是写出对象实例变量的数值。如果交量是另一对象的引用,则引用的对象也要串行化。这个过程是递归的,串行化可能要涉及一个复杂树结构的单行化,包括原有对象、对象的对象、对象的对象的对象等等。对象所有权的层次结构称为图表(graph)。
2.串行化的目的
Java对象的单行化的目标是为Java的运行环境提供一组特性,如下所示:
1) 尽量保持对象串行化的简单扼要 ,但要提供一种途径使其可根据开发者的要求进行扩展或定制。
2) 串行化机制应严格遵守Java的对象模型 。对象的串行化状态中应该存有所有的关于种类的安全特性的信息。
3) 对象的串行化机制应支持Java的对象持续性。
4) 对象的串行化机制应有足够的 可扩展能力以支持对象的远程方法调用(RMI)。
5) 对象串行化应允许对象定义自身 的格式即其自身的数据流表示形式,可外部化接口来完成这项功能。
二、串行化方法
从JDK1.1开始,Java语言提供了对象串行化机制 ,在java.io包中,接口Serialization用来作为实现对象串行化的工具 ,只有实现了Serialization的类的对象才可以被串行化。
Serializable接口中没有任何的方法。当一个类声明要实现Serializable接口时,只是表明该类参加串行化协议,而不需要实现任何特殊的方法。下面我们通过实例介绍如何对对象进行串行化。
1.定义一个可串行化对象
一个类,如果要使其对象可以被串行化,必须实现Serializable接口。我们定义一个类Student如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import
java.io.Serializable;
public
class
Student
implements
Serializable {
int
id;
// 学号
String name;
// 姓名
int
age;
// 年龄
String department;
// 系别
public
Student(
int
id, String name,
int
age, String department) {
this
.id = id;
this
.name = name;
this
.age = age;
this
.department = department;
}
}
|
2.构造对象的输入/输出流
要串行化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。
java.io包中,提供了ObjectInputStream和ObjectOutputStream将数据流功能扩展至可读写对象 。将对象读出或者写入流的主要类有两个: ObjectOutputStream与ObjectInputStream 。ObjectOutputStream 提供用来将对象写入输出流的writeObject方法, ObjectInputStream提供从输入流中读出对象的readObject方法。使用这些方法的对象必须已经被序列化的。也就是说,必须已经实现 Serializable接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
import
java.io.FileInputStream;
import
java.io.FileOutputStream;
import
java.io.IOException;
import
java.io.ObjectInputStream;
import
java.io.ObjectOutputStream;
public
class
ObjectSer {
public
static
void
main(String args[])
throws
IOException,
ClassNotFoundException {
Student stu =
new
Student(
981036
,
"LiuMing"
,
18
,
"CSD"
);
FileOutputStream fo =
new
FileOutputStream(
"data.ser"
);
ObjectOutputStream so =
new
ObjectOutputStream(fo);
try
{
so.writeObject(stu);
so.close();
}
catch
(IOException e) {
System.out.println(e);
}
stu =
null
;
FileInputStream fi =
new
FileInputStream(
"data.ser"
);
ObjectInputStream si =
new
ObjectInputStream(fi);
try
{
stu = (Student) si.readObject();
si.close();
}
catch
(IOException e)
{
System.out.println(e);
}
System.out.println(
"Student Info:"
);
System.out.println(
"ID:"
+ stu.id);
System.out.println(
"Name:"
+ stu.name);
System.out.println(
"Age:"
+ stu.age);
System.out.println(
"Dep:"
+ stu.department);
}
}
|
运行结果如下:
Student Info:
ID:981036
Name:LiuMing
Age:18
Dep:CSD
在这个例子中,我们首先定义了一个类Student,实现了Serializable接口 ,然后通过对象输出流的writeObject()方法将Student对象保存到文件 data.ser中 。之后,通过对家输入流的readObjcet()方法从文件data.ser中读出保存下来的Student对象 。从运行结果可以看到,通过串行化机制,可以正确地保存和恢复对象的状态。
Java的"对象序列化"能让你将一个实现了Serializable接口的对象转换成byte流,这样日后要用这个对象时候,你就能把这些byte数据恢复出来,并据此重新构建那个对象了。
要想序列化对象,你必须先创建一个OutputStream,然后把它嵌进ObjectOutputStream。这时,你就能用writeObject()方法把对象写入OutputStream了。writeObject()方法负责写入特定类的对象的状态,以便相应的 readObject()方法可以还原它。通过调用 out.defaultWriteObject 可以调用保存 Object 的字段的默认机制。该方法本身不需要涉及属于其超类或子类的状态。状态是通过使用 writeObject 方法或使用 DataOutput 支持的用于基本数据类型的方法将各个字段写入 ObjectOutputStream 来保存的。
读的时候,你得把InputStream嵌到ObjectInputStream里面,然后再调用readObject()方法。不过这样读出来的,只是一个Object的reference,因此在用之前,还得先下传。readObject() 方法负责从流中读取并还原类字段。它可以调用 in.defaultReadObject 来调用默认机制,以还原对象的非静态和非瞬态字段。 defaultReadObject()方法使用流中的信息来分配流中通过当前对象中相应命名字段保存的对象的字段。这用于处理类发展后需要添加新字段的情形。该方法本身不需要涉及属于其超类或子类的状态。状态是通过使用 writeObject 方法或使用 DataOutput 支持的用于基本数据类型的方法将各个字段写入 ObjectOutputStream 来保存的。
在序列化时,有几点要注意的:
1:当一个对象被序列化时,只保存对象的非静态成员变量(包括声明为private的变量),不能保存任何的成员方法和静态的成员变量。
2:如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被序列化。
3:如果一个可序列化的对象包含对某个不可序列化的对象的引用,那么整个序列化操作将会失败,并且会抛出一个NotSerializableException。我们可以将这个引用标记为transient,那么对象仍然可以序列化。
三、串行化的注意事项
1.串行化能保存的元素
串行化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符都不能保存。
2.transient关键字 (不是很明白)
对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的。例如一个Thread对象或一个FileInputStream对象 ,对于这些字段,我们必须用transient关键字标明,否则编译器将报措。
另外 ,串行化可能涉及将对象存放到 磁盘上或在网络上发达数据,这时候就会产生安全问题。因为数据位于Java运行环境之外,不在Java安全机制的控制之中。对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。应该在这些字段前加上transient关键字。
下面是java规范中对transient关键字的解释:
The transient marker is not fully specified by The Java Language Specification but is used in object serialization to mark member variables that should not be serialized.
以下是transient的一个应用举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
//LoggingInfo.java
import
java.io.FileInputStream;
import
java.io.FileOutputStream;
import
java.io.ObjectInputStream;
import
java.io.ObjectOutputStream;
import
java.util.Date;
public
class
LoggingInfo
implements
java.io.Serializable {
private
static
final
long
serialVersionUID = 1L;
private
Date loggingDate =
new
Date();
private
String uid;
private
transient
String pwd;
LoggingInfo(String user, String password) {
uid = user;
pwd = password;
}
public
String toString() {
String password =
null
;
if
(pwd ==
null
) {
password =
"NOT SET"
;
}
else
{
password = pwd;
}
return
"logon info: \n "
+
"user: "
+ uid +
"\n logging date : "
+ loggingDate.toString() +
"\n password: "
+ password;
}
public
static
void
main(String[] args) {
LoggingInfo logInfo =
new
LoggingInfo(
"MIKE"
,
"MECHANICS"
);
System.out.println(logInfo.toString());
try
{
ObjectOutputStream o =
new
ObjectOutputStream(
new
FileOutputStream(
"logInfo.out"
));
o.writeObject(logInfo);
o.close();
}
catch
(Exception e) {
// deal with exception
}
// To read the object back, we can write
try
{
ObjectInputStream in =
new
ObjectInputStream(
new
FileInputStream(
"logInfo.out"
));
LoggingInfo logInfo1 = (LoggingInfo) in.readObject();
System.out.println(logInfo1.toString());
}
catch
(Exception e) {
// deal with exception
}
}
}
|
四、其他资料的定义
1、序列化是干什么的?
简单说就是为了保存在内存中的各种对象的状态,并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存Object States,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。
2、什么情况下需要序列化
a)当你想把的内存中的对象保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
3、当对一个对象实现序列化时,究竟发生了什么?
在没有序列化前,每个保存在堆(Heap)中的对象都有相应的状态(state),即实例变量(instance ariable)比如:
Foo myFoo = new Foo();
myFoo .setWidth(37);
myFoo.setHeight(70);
当通过下面的代码序列化之后,MyFoo对象中的width和Height实例变量的值(37,70)都被保存到foo.ser文件中,这样以后又可以把它从文件中读出来,重新在堆中创建原来的对象。当然保存时候不仅仅是保存对象的实例变量的值,JVM还要保存一些小量信息,比如类的类型等以便恢复原来的对象。
FileOutputStream fs = new FileOutputStream("foo.ser");
ObjectOutputStream os = new ObjectOutputStream(fs);
os.writeObject(myFoo);
4、实现序列化(保存到一个文件)的步骤
a)Make a FileOutputStream
java 代码
FileOutputStream fs = new FileOutputStream("foo.ser");
b)Make a ObjectOutputStream
java 代码
ObjectOutputStream os = new ObjectOutputStream(fs);
c)write the object
java 代码
os.writeObject(myObject1);
os.writeObject(myObject2);
os.writeObject(myObject3);
d) close the ObjectOutputStream
java 代码
os.close();
6、相关注意事项
a)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
b)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
c)并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:
1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现。
serialVersionUID
序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。