序列化与反序列化
什么时序列化与反序列化:
(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;
(2)序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
(3)反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。
为什么需要序列化与反序列化:
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。
那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!
换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
总的来说可以归结为以下几点:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;
序列化算法一般会按步骤做如下事情:
(1)将对象实例相关的类元数据输出。
(2)递归地输出类的超类描述直到不再有超类。
(3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
(4)从上至下递归输出实例的数据
Serializable
序列化步骤:
- 步骤一:创建一个 ObjectOutputStream 输出流
- 步骤二:调用 ObjectOutputStream 对象的 writeObject 输出可序列化对象
代码实现:
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
| class Person implements Serializable { private String name; private int age; private String weight;
public Person(String name, int age, String weight) { this.name = name; this.age = age; this.weight = weight; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", weight=" + weight + '}'; } }
public static void writeObject() { try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://test//per.txt"))) { Person per = new Person("wx", 21, "60"); oos.writeObject(per); }catch (IOException e) { e.printStackTrace(); } }
|
反序列化步骤:
- 步骤一:创建一个 ObjectInputStream 输出流
- 步骤二:调用 ObjectInputStream 对象的 readObject 得到序列化的对象
代码实现:
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
| class Person implements Serializable { private String name; private int age; private String weight;
public Person(String name, int age, String weight) { this.name = name; this.age = age; this.weight = weight; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", weight=" + weight + '}'; } }
public static void readObject() { try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://test//per.txt"))) { Person tmp = (Person) ois.readObject(); System.out.println(tmp); }catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
|
输出:
1
| Person{name='wx', age=21, weight=60kg}
|
可选的自定义序列化
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
通过重写writeObject与readObject方法,可以自己选择哪些属性需要序列化, 哪些属性不需要。如果writeObject使用某种规则序列化,则相应的readObject需要相反的规则反序列化,以便能正确反序列化出对象。这里展示对名字进行反转加密。
代码实现:
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
| class Person implements Serializable { private String name; private int age; private String weight;
private static final long serialVersionUID = 10086L;
public Person(String name, int age, String weight) { this.name = name; this.age = age; this.weight = weight; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", weight=" + weight + '}'; }
private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(new StringBuffer(this.name).reverse()); out.writeInt(age); out.writeObject(new StringBuffer(this.weight).append("kg")); }
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { this.name = ((StringBuffer) in.readObject()).reverse().toString(); this.age = in.readInt(); this.weight = ((StringBuffer) in.readObject()).toString(); } }
|
Externalizable
强制自定义序列化
通过实现Externalizable接口,必须实现writeExternal、readExternal方法。
注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。
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
| public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
public class ExternalizableDemo { public static void main(String[] args) { try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://test//exp.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://test//exp.txt"))) { oos.writeObject(new Experson("wangxin",21)); Experson ep = (Experson) ois.readObject(); System.out.println(ep); }catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
class Experson implements Externalizable { public String name; public int age; public Experson(){ }
public Experson(String name, int age) { this.name = name; this.age = age; }
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(new StringBuffer(this.name).reverse()); out.writeInt(age); }
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.name = ((StringBuffer) in.readObject()).reverse().toString(); this.age = in.readInt(); }
@Override public String toString() { return "Experson{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
|
两种序列化对比
虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
序列化版本号serialVersionUID
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况。
* 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
* 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
* 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
总结
1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
3. 如果想让某个变量不被序列化,使用transient修饰。
4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
5. 反序列化时必须有序列化对象的class文件。
6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
9. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级