序列化反序列化详解"/>
Java序列化反序列化详解
本文参考自java 序列化,看这篇就够了
文章目录
- 一、序列化的定义
- 二、实现序列化的方式
- 1. Serializable
- 父类与子类的序列化关系
- 成员是引用的序列化机制
- 同一对象多次序列化机制
- 潜在问题
- 自定义序列化
- 2. Externalizable
- 3. 两种序列化对比
- 序列化版本号
- 总结
一、序列化的定义
- 序列化:将对象写入到 IO 流中
- 反序列化:从 IO 流中恢复对象
- 使用场景:将对象存入数据库或文件时,在网络通信时传输序列化后的对象时
二、实现序列化的方式
如果一个类想要实现序列化,那它就要实现Serializable或者Externalizable两个接口中的一个。这里放个 User 类 用来序列化:
User 类
package com.grh;
import java.io.Serializable;
public class User implements Serializable {private String name;private String password;//get/set省略public User() {System.out.println("一个对象被创建了");}public User(String name, String password) {System.out.println("一个对象被创建了");this.name = name;this.password = password;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", password='" + password + '\'' +'}';}
}
1. Serializable
相信对序列化好奇的人都点开过他的源码,然而他的源码是空的,只是一个没有任何函数和属性的接口:
public interface Serializable {
}
那么实现这个接口有什么用呢?
它只是个标记接口,用来提醒 JVM 这个类可以序列化,也就是说, Java 将类是否实现序列化的选择交给了程序员。
我们看个例子,它将 User 类进行序列化,并写入了 User.txt 文件中,然后再从中读出来进行输出。
Test 测试类
package com.grh;
import org.junit.Test;
import java.io.*;
public class serialize {@Testpublic void serializeBySerializable() {try {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));User user = new User("张三","123");oos.writeObject(user);oos.close();ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));User user1 = (User)ois.readObject();ois.close();System.out.println(user1);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
输出结果:
一个对象被创建了
User{name='张三', password='123'}
我们打开项目所在位置,会发现多了一个 User.txt 文件,里面的内容如下
这就是通过字节流序列化后的 User 对象,我们可以看到他的类名:com.grh.User ,也就是说,序列化会将该对象的类的信息也存进去。
还有一个地方值得注意,输出结果中显示只调用了一次构造函数,也就是通过 new 创建对象时创建的,也就是说,JVM 有自己的方式反序列化来创建对象,而不需要通过构造函数。
父类与子类的序列化关系
-
如果一个类的父类实现了序列化,那么这个类也可以被实例化
-
如果一个子类实现了序列化,但父类没有实现,那么将子类反序列化时,会调用父类的无参构造函数构建父类对象,而且父类中的数据都是默认值,如 int 为 0 ,对象为 null。
我们定义一个 Person 类,并用 User 类继承它。
package com.grh;
import java.io.Serializable;
public class Person {Person(){System.out.println("构建一个person");}
}
再次调用会得到一下结果:
构建一个person
一个对象被创建了
构建一个person
User{name='张三', password='123'}
我们可以看到父类 Person 的构造函数调用了两次,一次是 new 的时候,一次是反序列化 user 的时候。
成员是引用的序列化机制
如果一个可序列化的类的成员不是基本类型,也不是 String 类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
同一对象多次序列化机制
我们去掉 User 和 Person 的继承关系,将 User 作为 Person 中的一个成员,再加个年龄的成员,Person 类如下
package com.grh;
import java.io.Serializable;
public class Person implements Serializable{private int age;private User user;public Person(){ }public Person(int age, User user) {this.age = age;this.user = user;}public int getAge() { return age; }public void setAge(int age) { this.age = age; }public User getUser() { return user; }public void setUser(User user) { this.user = user; }
}
再写一个测试类,注意:反序列化的顺序与序列化时的顺序一致:
@Testpublic void serializeBySerializable() {try {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));User setUser = new User("张三","123");Person setPerson = new Person(18,setUser);Person setPerson1 = new Person(25,setUser);oos.writeObject(setUser);oos.writeObject(setPerson);oos.writeObject(setPerson1);oos.writeObject(setPerson1);oos.close();ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));User getUser = (User)ois.readObject();Person getPerson = (Person)ois.readObject();Person getPerson1 = (Person)ois.readObject();Person getPerson2 = (Person)ois.readObject();ois.close();System.out.println(getPerson==getPerson2); //falseSystem.out.println(getPerson.getUser()==getPerson1.getUser()); //trueSystem.out.println(getUser==getPerson1.getUser()); //trueSystem.out.println(getUser==getPerson2.getUser()); //true} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
可以看到,不管怎么存 user ,读出来时也都是同一个,而年龄不同的 person 是不同的对象,读出来时也是不同的。
Java 序列化同一对象,并不会将此对象序列化多次得到多个对象。
潜在问题
每个序列化的对象都有一个编号,如果多次序列化,只会重复这个编号,并不会重新序列化。
因此,如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,只是保存序列化编号,会导致更改无效化。如:
@Testpublic void serializeBySerializable() {try {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));User setUser = new User("张三","123");oos.writeObject(setUser);setUser.setName("王五");oos.writeObject(setUser);oos.close();ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));User getUser = (User)ois.readObject();ois.close();System.out.println(getUser);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
输出结果:
User{name='张三', password='123'}
可以看到修改名字为王五并没有实现。
自定义序列化
- static 不会被序列化,因为序列化存的是对象,static 所代表的是类
- 在成员前加上 transient 关键字,就不会对这个成员进行序列化,反序列化时还会有这个成员,不过会赋值默认值。
- 重写 writeObject 与 readObject 方法,就是修改序列化的规则,比 transient 更灵活,还可以对数据进行加密,提高安全性。注意,writeObject 与 readObject 方法需要对应,怎么进行序列化,就应该怎样反序列化。如:
public class Person implements Serializable {private String name;private int age;//省略构造方法,get及set方法private void writeObject(ObjectOutputStream out) throws IOException {//将名字反转写入二进制流out.writeObject(new StringBuffer(this.name).reverse());out.writeInt(age);}private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {//将读出的字符串反转恢复回来this.name = ((StringBuffer) ins.readObject()).reverse().toString();this.age = ins.readInt();}
}
- 重写 writeReplace 和 readResolve 方法,这两个方法会在写入之前调用,一般用于替换掉要写入或读取的对象。
package com.grh;
import java.io.*;
import java.util.ArrayList;
public class Person implements Serializable {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}private Object writeReplace() throws ObjectStreamException {ArrayList<Object> list = new ArrayList<Object>(2);list.add(this.name);list.add(this.age);return list;}public static void main(String[] args) {try {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));Person person = new Person("张三", 23);oos.writeObject(person);ArrayList list = (ArrayList) ios.readObject();System.out.println(list);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
//输出结果:
//[张三, 23]
在上面的例子中,本来写入的是 Person,但换成了 list,读取出来的时候也可以看到已经是 list 类型而不是 Person 类型了。
package com.grh;
import java.io.*;
import java.util.HashMap;public class Person implements Serializable {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}private Object readResolve() throws ObjectStreamException {return new Person("李四", 23);}public static void main(String[] args){try {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));Person person1 = new Person("张三", 23);Person person2 = new Person("王五", 23);oos.writeObject(person1);oos.writeObject(person2);Person person3 = (Person) ios.readObject();Person person4 = (Person) ios.readObject();System.out.println(person3);System.out.println(person4);System.out.println(person3==person4);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
在这个类中,无论序列化了多少个对象进入文件中,当他读出的时候,始终会调用 readResolve() 方法 new 一个李四的对象出来,序列化中的数据已经不重要了。
一般来说,重写 readResolve() 主要用于防止破坏单例模式的规则,因为反序列化后会重新构建一个一样的对象,如果在 readResolve() 方法中直接返回已经存在的单例对象,反序列化后就不会生成对象了。
注:writeReplace() 在writeObject前调用,readResolve() 在 readObject() 后调用。
2. Externalizable
Externalizable接口中,有两个方法,writeExternal() 和 readExternal() ,因此要想实现这个接口来序列化,必须实现这两个函数
package com.grh;
import java.io.*;
public class ExPerson implements Externalizable {private String name;private int age;//注意,必须加上pulic 无参构造器public ExPerson() { System.out.println("无参构造函数构建对象");}public ExPerson(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "ExPerson{" +"name='" + name + '\'' +", age=" + age +'}';}public void writeExternal(ObjectOutput out) throws IOException {//将name反转后写入二进制流StringBuffer reverse = new StringBuffer(name).reverse();System.out.println(reverse.toString());out.writeObject(reverse);out.writeInt(age);}public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {//将读取的字符串反转后赋值给name实例变量this.name = ((StringBuffer) in.readObject()).reverse().toString();System.out.println(name);this.age = in.readInt();}public static void main(String[] args) {try {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"));oos.writeObject(new ExPerson("brady", 23));ExPerson ep = (ExPerson) ois.readObject();System.out.println(ep);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
输出结果:
ydarb
无参构造函数构建对象
brady
ExPerson{name='brady', age=23}
这个例子跟 Serializable 接口中的自定义序列化基本一致,将用户名反转再序列化和反序列化。
注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。
3. 两种序列化对比
Serializable | Externalizable |
---|---|
系统自动存储必要的信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
序列化版本号
java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况:
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
- 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
总结
- 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
- 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
- 如果想让某个变量不被序列化,使用transient修饰。
- 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
- 反序列化时必须有序列化对象的class文件。
- 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
- 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
- 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
- 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
更多推荐
Java序列化反序列化详解
发布评论