Java 序列化

本文最后更新于:2024年3月18日 凌晨

Java 序列化

  • Java序列化是将一个对象编码成一个字节流,反序列化将字节流编码转换成一个对象。
  • 对象输入流ObjectInputStream和对象输出流ObjectOutputStream将Java流系统扩充到能输入/输出对象,他们提供的writeObject()readObject()方法实现了对象的序列化(Serialized)和反序列化(Deserialized)

Serializable 实现序列化

  • 为了实现用户自定义的序列化,相应的类必须实现Serializable接口,Serializable接口中没有定义任何方法在,Java 中实现了 Serializable 接口后, JVM 会在底层帮我们实现序列化和反序列。
  • 被 transient 关键字修饰的属性不会被序列化, static 属性也不会被序列化。

serialVersionUID

  • Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
  • 如果不显示指定 serialVersionUID, JVM 在序列化时会根据属性自动生成一个 serialVersionUID,然后与属性一起序列化,再进行持久化或网络传输。在反序列化时, JVM 会再根据属性自动生成一个新版 serialVersionUID,然后将这个新版 serialVersionUID 与序列化时生成的旧版 serialVersionUID 进行比较,如果相同则反序列化成功,否则报错。
  • 如果显示指定了 serialVersionUID, JVM 在序列化和反序列化时仍然都会生成一个 serialVersionUID,但值为我们显示指定的值,这样在反序列化时新旧版本的 serialVersionUID 就一致了。
  • 当序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出异常。

使用ObjectStream输入输出对象流

[例13-9]:系统对象的序列化处理。

将系统对象写入文件

1
2
3
4
5
6
7
8
9
10
11
public class 将系统对象写入文件 {
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("src/Study/流式输入输出与文件处理/对象序列化/Study/流式输入输出与文件处理/系统对象的序列化处理/storedata.dat"));
// 用对象输出流的writeObject()方法分别写入了日期和字符串对象到文件中。
out.writeObject(new Date());// 写入日期对象。
out.writeObject("hello world");// 写入字符串对象。
System.out.println("写入完毕");
}catch (IOException e){}
}
}

读取文件中的对象并显示出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class 读取文件中的对象并显示出来 {
public static void main(String[] args) {
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("src/Study/流式输入输出与文件处理/对象序列化/Study/流式输入输出与文件处理/系统对象的序列化处理/storedata.dat"));
// 调用对象输入流的readObject()方法读文件汇总存储的对象,如果输入源数据不符合对象规范将产生ClassNotFoundException异常。
Date current =(Date)in.readObject();
System.out.println("日期:"+current);
String str = (String)in.readObject();
System.out.println("字符串:"+str);
}catch (IOException e){
}catch (ClassNotFoundException e){

}
}
}

[例13-10]:利用对象序列化将各种图形元素以对象形式存储,从而实现图形的保存。

  • 为简单起见,这里以直线和圆为例,创建Line和Circle两个类分别表示直线和圆,为了能方便地访问各种图形元素,定义一个抽象父类Graph,其中提供了一个draw()方法用来绘制相应的图形。

图形对象的序列化设计

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
abstract class Graph implements Serializable {// 抽象类。

public abstract void draw(Graphics g);// 定义draw()方法。
}

class Line extends Graph {
int x1, y1;
int x2, y2;

public void draw(Graphics g) {// 实现直线绘制的draw()方法。
g.drawLine(x1, y1, x2, y2);
}

public Line(int x1, int y1, int x2, int y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
}

class Circle extends Graph {
int x, y;
int r;

public void draw(Graphics g) {// 实现圆绘制draw()
g.drawOval(x, y, r, r);
}

public Circle(int x, int y, int r) {
this.x = x;
this.y = y;
this.r = r;
}
}

测试将图形对象序列化写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class 测试将图形对象序列化写入文件 {
public static void main(String[] args) {
/*以下程序分别创建一条直线和一个圆写入文件中*/
Line K1= new Line(60,90,140,90);
Line K3= new Line(100,50,100,130);
Circle K2 = new Circle(60,50,80);
try {
FileOutputStream fout = new FileOutputStream("src/Study/流式输入输出与文件处理/对象序列化/Study/流式输入输出与文件处理/图形的序列化处理/storeshape.dat");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(new Integer(3));// 保存要写入图形对象的数量。
out.writeObject(K1);// 写入直线。
out.writeObject(K2);// 写入圆。
out.writeObject(K3);// 写入直线。
}catch (IOException e){
System.out.println(e);
}
}
}

从文件读取序列化对象并绘图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DisplayGraph extends Frame{
public static void main(String[] args) {
new DisplayGraph();
}

public DisplayGraph(){
super("读对象文件显示图形");
setSize(300,300);
setVisible(true);
Graphics g = getGraphics();// 得到窗体的Graphics对象。
try{
FileInputStream fin = new FileInputStream("src/Study/流式输入输出与文件处理/对象序列化/Study/流式输入输出与文件处理/图形的序列化处理/storeshape.dat");
ObjectInputStream in = new ObjectInputStream(fin);
// 读出图形对象的数量。
int n=((Integer)in.readObject()).intValue();
for (int i=1;i<=n;i++){
Graph me = (Graph)in.readObject();// 读取对象。
// 根据图形对象的个数循环读出每个图形对象并调用其draw()方法进行绘图。
me.draw(g);
}
}catch (IOException e){}
catch (ClassNotFoundException e){}
}
}
  • 说明:为了增加程序的通用性,程序中将要写入文件中的图形对象的数量首先写入到文件的开始处。

Externalizable 实现序列化

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
public class ExternalizableTest implements Externalizable {

private transient String content = "是的,我将会被序列化,不管我是否被transient关键字修饰";

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}

@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
content = (String) in.readObject();
}

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

ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et);

ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content);

out.close();
in.close();
}
}
  • content变量会被序列化吗?好吧,我把答案都输出来了,是的,运行结果就是:
1
是的,我将会被序列化,不管我是否被transient关键字修饰。
  • 这是为什么呢,不是说类的变量被transient关键字修饰以后将不能序列化了吗?
  • 我们知道在Java中,对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,则所有的序列化将会自动进行,若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关,因此第二个例子输出的是变量content初始化的内容,而不是null