Java 之 序列化与反序列化 一、序列化与反序列化 二、java.io.Serializable 接口 三、java.io.Externalizable 接口
1、概述
序列化:简单理解就是把程序里面生成的对象以文件的形式保存到本地硬盘中,序列化写入文件的IO是ObjectOutputStream流。
反序列化:就是把序列化的对象文件导入到程序中,并生成为一个对象,供程序使用。反序列化的读取对象文件的IO流是 ObjectInputStream。
Java 中,经典的方式实现对象序列化,可以实现序列化接口。
2、哪些属性不序列化
static 属性不会被序列化,反序列化时恢复为当前类变量的值。
因为 static 代表“一个类一个”,而不是“一个对象一个”,因此它不是对象的状态。
如果某个实例变量不能或不应该被序列化,就把它标记为 transient(瞬时)的。
当某个属性的类型没有实现 Serializable 接口,而又不能修改该类,那么该实例变量不能被实例化。
那么只能把该属性标记为 transient 。或者动态数据只可以在执行时求出而不能或不必存储。
被标记为 transient 的数学,在反序列化时,被恢复为默认值。
3、序列化版本 ID
如果你将对象序列化,则必须要有该类才能还原和使用该对象。但若同时又修改了类会发生什么事?
有些修改会严重违反Java的类型安全性和兼容性,例如:删除了某个实例变量、改变实例变量的类型、将类从可序列化修改为不可序列化,将实例变量修改静态的等等。
但是有些修改通常不会影响,例如:加入新的实例变量(还原时可以按默认值处理),将实例变量从瞬时修改为非瞬时的(可以使用默认值)等。
但是,现在所有对类的修改都导致原来的数据在反序列化时失败java.io.InvalidClassException。
解决这个问题的方法,就是在实现 java.io.Serializable 接口时,增加一个long类型的静态常量 serialVersionUID。如果类没有显示定义这个静态变量,它的值是Java运行时环境根据类的内部细节自动生成的,若类的源代码作了修改,serialVersionUID 就会发生变化,从而导致“旧”数据反序列化失败。
如果对类做了会影响数据兼容性的操作时,要么修改serialVersionUID的值,使得原来的反序列化数据失败,要么你要对“旧”对象反序列化后引起的问题负责
二、java.io.Serializable 接口
Java 类通过实现 java.io.Serializable 接口和定义 serialVersionUID常量 以启用其序列化功能,未实现此接口的类型将无法使其任何状态序列化或反序列化。
Serializable接口:只是一个标记,JVM在序列化的时候,会去判断要序列化的对象是否有实现Serializable接口,如果没有实现就会报错,不允许系列化。
serialVersionUID常量:是指JVM在序列化对象的时候,会把这个常量表示序列化对象所属的类的类ID。在反序列化时,反序列化对象的serialVersionUID能匹配上程序里面的类的serialVersionUID时,就判断这个反序列化的对象就是这个类生成的,因此允许反序列化。
可序列化类的所有子类型本身都是可序列化的。
序列化接口没有方法或字段,仅用于标识可序列化的语义。
如果实现 Serializable 接口,对象如何序列化,各个属性序列化的顺序是什么,都是默认的,程序员本身无法指定,也不用关心。
如果属性前面有 static 和 transient 修饰,该属性不参与序列化。
如果需要序列化对象本身的属性,那么该属性的类也要实现Serializable接口,否则不能序列化。
Demo:
1 public interface Emp extends Serializable{
2 public void work();
3 }
4
5 public class Employee implements Emp{
6 /* 序列化编号 */
7 private static final long serialVersionUID = 1L;
8
9 private String empNo;
10 //透明化处理,不能持久化 瞬时态属性
11 private transient String dept;
12 private String name;
13
14 public Employee(String empNo, String dept, String name) {
15 super();
16 this.empNo = empNo;
17 this.dept = dept;
18 this.name = name;
19 }
20
21 public Employee() {
22 super();
23 }
24
25 public void work() {
26 System.out.println("name:" + this.name + ",empNo:" + this.empNo + ",dept:" + this.dept + " is working.");
27 }
28
29
30 @Override
31 public String toString() {
32 return "Employee [empNo=" + empNo + ", dept=" + dept + ", name=" + name
33 + "]";
34 }
35 }
序列化的过程:
1 public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
2 String file = "D:emp.dat";
3 Emp e1 = new Employee("001", "技术部", "张三");
4
5 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
6 out.writeObject(e1);
7 out.close();
8 System.out.println(e1 + "序列化成功");
9
10 ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
11 Emp e2 = (Emp)in.readObject();;
12 in.close();
13 System.out.println(e2 + "反序列化成功");
14 }
但是看控制台输出的日志信息:
这里就有一个问题,为什么 dept 这个属性没有被序列化呢?
由于 Employee 类中定义的 dept 属性,使用了 transient 关键字修饰,使用了 transient 修饰的属性是不能被序列化的。
解决办法:
这个属性已经存在这个类中,删除这个属性是不可行的,SUN还是提供了一些后门给我们处理这种问题的,就是实现了Serializable接口的类。
如果需要序列化被 transient 修饰的属性,可以通过定义:writeObject(java.io.ObjectOutputStream s)序列化对象和 readObject(java.io.ObjectInputStream s)反序列化对象。这两个方法来实现序列化和反序列化transient的属性。
上面的案例进行修改:
1 public class Employee implements Emp{
2 /* 序列化编号 */
3 private static final long serialVersionUID = 3694902274397865665L;
4
5 private String empNo;
6 //透明化处理,不能持久化
7 private transient String dept;
8 private String name;
9
10 public Employee(String empNo, String dept, String name) {
11 super();
12 this.empNo = empNo;
13 this.dept = dept;
14 this.name = name;
15 }
16
17 public Employee() {
18 super();
19 }
20
21 public void work() {
22 System.out.println("name:" + this.name + ",empNo:" + this.empNo + ",dept:" + this.dept + " is working.");
23 }
24
25
26 @Override
27 public String toString() {
28 return "Employee [empNo=" + empNo + ", dept=" + dept + ", name=" + name
29 + "]";
30 }
31
32 // 强行序列化对象属性的方法
33 private void writeObject(java.io.ObjectOutputStream s)
34 throws java.io.IOException{
35 s.defaultWriteObject();
36 s.writeObject(this.dept);
37 }
38 // 强行反序列化对象属性的方法
39 private void readObject(java.io.ObjectInputStream s)
40 throws java.io.IOException, ClassNotFoundException {
41 s.defaultReadObject();
42 this.dept = (String)s.readObject();
43 }
44 }
这时候就可以把 transient 修饰的属性进行序列化和反序列化了。
总结:
1. 通过实现 Serializable 接口的,默认是不会序列化 transient 修饰的属性,除非重写 writeObject 和 readObject 两个方法。其实这两个方法就是 JVM 在执行 Serializable 接口序列化对象时执行的方法。
2. 如果对象有多个属性都被transient修饰了,例如是empNo和dept,那么在重写writeObject和readObject这两个方法时,一定要注意这两个属性的写入顺序和读取顺序必须保持一致,否则会导致反序列化失败。但是在这里,由于都是字符串格式的,不会报错,只是出现顺序颠倒。
三、java.io.Externalizable 接口
若某个要完全控制某一对象及其超类型的流格式和内容,则它要实现 Externalizable 接口中的 writeExternal 和 readExternal 方法。
程序员要在 writerExternal 方法中,自己定制哪些属性要序列化,顺序是什么样的。
程序员要在 readExternal 方法中,自己定制哪些属性要反序列化,顺序与 writerExternal 方法中的一致。
Demo:JavaBean 类
1 import java.io.Externalizable;
2 import java.io.IOException;
3 import java.io.ObjectInput;
4 import java.io.ObjectOutput;
5
6 public class Goods implements Externalizable{
7 private static String brand = "Made In China";
8 private String name;
9 private double price;
10 private transient int sale;
11 public Goods(String name, double price, int sale) {
12 super();
13 this.name = name;
14 this.price = price;
15 this.sale = sale;
16 }
17 public Goods() {
18 super();
19 }
20 public static String getBrand() {
21 return brand;
22 }
23 public static void setBrand(String brand) {
24 Goods.brand = brand;
25 }
26 public String getName() {
27 return name;
28 }
29 public void setName(String name) {
30 this.name = name;
31 }
32 public double getPrice() {
33 return price;
34 }
35 public void setPrice(double price) {
36 this.price = price;
37 }
38 public int getSale() {
39 return sale;
40 }
41 public void setSale(int sale) {
42 this.sale = sale;
43 }
44 @Override
45 public String toString() {
46 return "Goods [brand = " + brand +",name=" + name + ", price=" + price + ",sale = " + sale +"]";
47 }
48 @Override
49 public void writeExternal(ObjectOutput out) throws IOException {
50 //程序员自己定制要序列化的内容,顺序等
51 //这两个方法是在对象被序列化和反序列化的过程中,JVM帮我们调用
52 out.writeUTF(brand);//静态的也序列化
53 out.writeUTF(name);
54 out.writeDouble(price);
55 out.writeInt(sale);//有transient也序列化
56 }
57 @Override
58 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
59 //程序员自己定制要反序列化的内容,顺序等,建议与序列化的顺序一致
60 brand = in.readUTF();
61 name = in.readUTF();
62 price = in.readDouble();
63 sale = in.readInt();
64 }
65
66 }
序列化与反序列
1 import java.io.FileInputStream;
2 import java.io.FileOutputStream;
3 import java.io.IOException;
4 import java.io.ObjectInputStream;
5 import java.io.ObjectOutputStream;
6 import org.junit.Test;
7
8 public class TestObject {
9 @Test
10 public void test01() throws IOException{
11 Goods goods = new Goods("《Java从入门到放弃》", 20.45, 100);
12 Goods.setBrand("China Beijing");
13
14 FileOutputStream fos = new FileOutputStream("goods.dat");
15 ObjectOutputStream oos = new ObjectOutputStream(fos);
16
17 oos.writeObject(goods);
18
19 oos.close();
20 fos.close();
21 }
22
23 @Test
24 public void test02()throws IOException, ClassNotFoundException{
25 FileInputStream fis = new FileInputStream("goods.dat");
26 ObjectInputStream ois = new ObjectInputStream(fis);
27
28 Object obj = ois.readObject();
29 System.out.println(obj);
30
31 ois.close();
32 fis.close();
33 }
34 }
总结:
对于实现序列化,有两种方式,一种是经典的 Serializable 接口,另外一种是实现 Externalizable接口。
1. Serializable 是自动序列化,因此直接编写序列化编号即可,如果需要序列化transient属性,就需要重写writeObject(ObjectOutputStream s)、readObject(ObjectInputStream s),原则与下面一致。
2. Externalizable 不是自动序列化,需要重写 writeExternal(ObjectOutput out)、readExternal(ObjectInput in)方法,除此外还需要对象有空的构造函数。
(1)writeExternal、readExternal可以指定具体的序列化的属性。写一个序列化一个属性。
(2)序列化属性和反序列化,都是按照顺序的。
3. 其实实现这两个接口是一样效果的,只是Serializable可以自动序列化非transient的变量,而Externalizable不会序列化任何变量。两者需要序列化transient都需要重写一些特定方法。 而Serializable的方法是JVM默认的方法,Externalizable则是接口定义的方法。
4. 序列化数组,反序列化也要使用集合对接,否则会报类转义异常。
5. 更改了类名以后,无法进行反序列化。