L
L
LearnJava
Search…
Java序列化

1. 什么是序列化?

序列化是将一个对象的状态,各属性的值序列化保存起来,然后在合适的时候通过反序列化获得。
Java的序列化是将一个对象表示成字节序列,该字节序列包括了对象的数据,有关对象的类型信息和存储在对象中的数据类型。
说白了,就是将对象保存起来,就跟保存字符串数据一样,用到的时候再取出来。任何实现了Serializable接口的类都可以被序列化。

2. 实现Serializable接口进行序列化

1
package com.wangjun.othersOfJava;
2
3
import java.io.FileInputStream;
4
import java.io.FileOutputStream;
5
import java.io.IOException;
6
import java.io.ObjectInputStream;
7
import java.io.ObjectOutputStream;
8
import java.io.Serializable;
9
10
public class SerializeDemo {
11
12
public static void main(String[] args) {
13
Employee em = new Employee();
14
em.name = "wangjun";
15
em.age = 24;
16
em.ssh = 123456;
17
// 将对象序列化后保存到文件
18
try (
19
FileOutputStream fo = new FileOutputStream("tem.ser");
20
ObjectOutputStream oo = new ObjectOutputStream(fo))
21
{
22
oo.writeObject(em);
23
} catch (IOException e) {
24
e.printStackTrace();
25
}
26
// 反序列化取出对象
27
try(
28
FileInputStream fi = new FileInputStream("tem.ser");
29
ObjectInputStream oi = new ObjectInputStream(fi))
30
{
31
Employee e2 = (Employee) oi.readObject();
32
System.out.println(e2.name);
33
System.out.println(e2.age);
34
System.out.println(e2.ssh);
35
System.out.println(Employee.local);
36
e2.test();
37
} catch (Exception e) {
38
e.printStackTrace();
39
}
40
}
41
42
static class Employee implements Serializable {
43
String name;
44
int age;
45
static String local = "earth";
46
transient int ssh;
47
48
public void test() {
49
System.out.println("this is test method!");
50
}
51
}
52
53
}
Copied!
程序的运行结果:
1
wangjun
2
24
3
0
4
earth
5
this is test method!
Copied!
如果有一些字段不想被序列化怎么办呢?这时候就可以用transient关键字修饰,就像上面代码的ssh字段,关于transient关键字有以下几个特点:
  • 一旦被transient关键字修饰,那变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问;
  • transient只能修饰变量,不能修饰方法和类,本地变量(局部变量)也不能被transient修饰;
  • 一个静态变量不管是否被transient修饰,都不能被序列化。
从上面的例子看到好像与第三条不符,其实反序列化取出的local是JVM里面的值,而不是反序列化出来的。可以加一行代码验证一下,在反序列化之前更改一下local的值:
1
// 反序列化取出对象
2
Employee.local = "earth2";
3
try(
4
...
Copied!
看一下打印结果
1
wangjun
2
24
3
0
4
earth2
5
this is test method!
Copied!
这说明打印出来的是JVM中对应的local的值earth2,而不是序列化的时候的值earth。

3. 实现Externalizable接口进行序列化

transient只有对实现了Serializable接口方式的序列化有效,还有一种序列化的方式是实现Externalizable接口,这种实现方式不像实现Serializable接口一样可以帮你自动序列化,它需要在writeExternal方法中手动指定需要序列化的变量并且在readExternal手动取出来,这与是否被transient修饰无关,下面更改一下上面的例子,将Employee类改成:
1
static class Employee implements Externalizable {
2
String name;
3
int age;
4
static String local = "earth";
5
transient int ssh;
6
7
//实现Externalizable接口进行序列化必须显式声明无参构造器
8
public Employee() {
9
}
10
11
public void test() {
12
System.out.println("this is test method!");
13
}
14
15
@Override
16
public void writeExternal(ObjectOutput out) throws IOException {
17
out.writeObject(name);
18
//out.writeObject(age);
19
out.writeObject(ssh);
20
out.writeObject(local);
21
}
22
23
@Override
24
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
25
name = (String) in.readObject();
26
//age = (int) in.readObject();
27
ssh = (int) in.readObject();
28
local = (String) in.readObject();
29
}
30
}
Copied!
重新运行,结果(注意:上述主函数中还存在对local重新赋值的代码Employee.local = "earth2";):
1
wangjun
2
0
3
123456
4
earth
5
this is test method!
Copied!
可以看到能否被序列化跟transient和static修饰都没有关系,只跟writeExternal和readExternal有关系。

4. Serializable和Externalizable的区别

  • 对Serializable对象反序列化时,由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable实现类无需默认构造函数,但是当Serializable实现类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
  • 对Externalizable对象反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
  • 如果不是特别坚持实现Externalizable接口,那么还有另一种方法。我们可以实现Serializable接口,并添加writeObject()readObject()的方法。一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。

5. SerialVersionUID的作用

上述实现Serializable接口的Employee类中,会有一个警告:
1
The serializable class Employee does not declare a static final serialVersionUID field of type long
Copied!
意思是Employee没有声明一个静态final的常量serialVersionUID,那这个serialVersionUID的作用是什么呢?
serialVersionUID是对类进行版本控制的,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
serialVersionUID有两种生成方式:
  • 一是默认的1L,比如:private static final long serialVersionUID = 1L;
  • 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。
如果程序没有显式的声明serialVersionUID,那么程序将用第二种实现。我们可以做一个实现,还是用上述实现Serializable接口的例子。
我们先运行一下程序,生成序列化文件tem.ser,在把“将对象序列化后保存到文件”这一段逻辑注释掉,对Employee类增加一个test字段:
1
static class Employee implements Serializable {
2
String name;
3
int age;
4
static String local = "earth";
5
transient int ssh;
6
String test;
7
8
public void test() {
9
System.out.println("this is test method!");
10
}
11
}
Copied!
这时候运行的时候会报错:
1
java.io.InvalidClassException: com.wangjun.othersOfJava.SerializeDemo$Employee; local class incompatible: stream classdesc serialVersionUID = 4506166831890198488, local class serialVersionUID = 785960679919880606
2
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
3
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
4
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
5
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
6
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
7
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
8
at com.wangjun.othersOfJava.SerializeDemo.main(SerializeDemo.java:32)
Copied!
因为程序发现取到的序列化文件的serialVersionUID和当前的serialVersionUID不一样。这个serialVersionUID是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,因为增加了test字段,因此生成的serialVersionUID不一样了。
接着,我们显式的声明serialVersionUID
1
static class Employee implements Serializable {
2
private static final long serialVersionUID = 1L;
3
String name;
4
int age;
5
static String local = "earth";
6
transient int ssh;
7
8
public void test() {
9
System.out.println("this is test method!");
10
}
11
}
Copied!
将刚才注释的代码取消注释,运行一遍再注释掉,并且新增字段test:
1
static class Employee implements Serializable {
2
private static final long serialVersionUID = 1L;
3
String name;
4
int age;
5
static String local = "earth";
6
transient int ssh;
7
String test;
8
9
public void test() {
10
System.out.println("this is test method!");
11
}
12
}
Copied!
再次运行发现没有报错,运行OK。这是因为你显式声明了serialVersionUID,序列化的serialVersionUID和目前的serialVersionUID一样,因此会认为是同一个版本的类。
你也可以将serialVersionUID改成2L,这个时候又会报错了。
Last modified 1yr ago