目录
序列化与反序列化
什么是序列化与反序列化?
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
序列化:对象序列化是指将Java对象(动态的状态,如变量、函数)转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序。
反序列化:从字节流重构出Java对象的过程。
序列化得到的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。
什么情况需要序列化?
- 当准备把一个内存中的对象状态保存到文件或者数据库中的时候
- 当你想用套接字在网络上传送对象的时候
- 当你想通过RMI传输对象的时候(Java RMI(Remote Method Invocation)--Java的远程⽅法调⽤是Java所特有的分布式计算技术,它允许运⾏在⼀个Java虚拟机上的对象调⽤运⾏在另⼀个Java虚拟机上的对象的⽅法,从⽽使Java编程⼈员可以⽅便地在⽹络环境中作分布式计算)
- 一台机器调用另一台机器实现远程调用
Java如何进行序列化?
实现Serializable接口即可
对象DEMO1.java代码如下:实现了序列化接口
package com.tu.Serializable;
import java.io.Serializable;
public class DEMO1 implements Serializable { //这里实现Serializable接口
private static String AGE = "269";
private String name;
private String color;
transient private String car;
public static String getAGE() {
return AGE;
}
public static void setAGE(String AGE) {
DEMO1.AGE = AGE;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getCar() {
return car;
}
public void setCar(String car) {
this.car = car;
}
public String toString() {
return "DEMO1{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", car='" + car + '\'' +
'}';
}
}
main方法进行测试
package com.tu.Serializable;
import java.io.*;
public class DEMO1SerializableTest {
public static void main(String[] args) throws Exception {
serialize();
DEMO1 demo1 = deserialize();
System.out.println(demo1.toString());
}
/**
* 序列化
* ObjectOutputStream代表对象输出流:
* 它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,
* 把得到的字节序列写到一个目标输出流中
*/
private static void serialize() throws IOException {
DEMO1 car = new DEMO1();
car.setColor("black");
car.setName("奥迪");
car.setCar("0000");
// ObjectOutputStream 对象输出流,将 DEMO1 对象存储到D盘的 TUDEEN.txt 文件中,
// 完成对 DEMO1 对象的序列化操作
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/TUDEEN.txt")));
oos.writeObject(car);
System.out.println("DEMO1 对象序列化成功!");
oos.close();
}
/**
* 反序列化
* ObjectInputStream代表对象输入流:
* 它的readObject()方法从一个源输入流中读取字节序列,
* 再把它们反序列化为一个对象,并将其返回。
*/
private static DEMO1 deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/TUDEEN.txt")));
DEMO1 car = (DEMO1) ois.readObject();
System.out.println("DEMO1 对象反序列化成功!");
return car;
}
}
实际运行结果,他会在 d:/TUDEEN.txt 生成个文件。
从运行结果上看:
先修改DEMO1类中的静态变量static的值如下
单独进行序列化操作
由此可见,序列化成功,然后再次修改 静态变量的单独进行反序列化操作,修改静态变量值为"现在是准备反序列化",按照序列化与反序列化结果来说,反序列化的静态变量值应该为"现在是准备序列化"
实际结果为"现在准备反序列化"
-
他实现了对象的序列化和反序列化。
-
transient 修饰的属性,是不会被序列化的。我设置的car值为0000,成了null。
-
这个静态static的属性,他不序列化
static修饰的属性不能被序列化的原因是:
被static修饰的属性是所有类共享的,如果可以序列化,就会出现下面的情况,当我们序列化某个类的一个对象到某个文件后,这个文件中的对象的那个被static修饰的属性值会固定下来,当另外一个普通的的对象修改了该static属性后,我们再去反序列化那个文件中的对象,就会得到和后面的对象不同的static属性值,这显然违背了static关键字诞生的初衷。
transient 修饰的属性不能被序列化的原因是:
transient 修饰的属性,是不会被序列化的。transient是序列化和反序列化时用于标记属性是否可序列化反序列化的“标记”,带有transient修饰的成员变量,在类中相当于一个(没有static修饰的)成员变量,对于该类的使用没有任何影响,只是在序列化的时候,带有transient标记的变量,无法被序列化和反序列化。
serialVersionUID 的作用和用法?
现在有一个场景,在原来的类DEMO1中增加一个变量,进行Setter、Getter与toString方法,然后注释掉新增加的变量以及一系列方法,用原来的的方法单独进行序列化操作,再把注释取消查看反序列化操作。(// private String SerializableIDTest; 这里序列化会注释调改字段,在反序列化的时候取消注释查询对应情况
)
package com.tu.Serializable;
import java.io.Serializable;
public class DEMO1 implements Serializable {
private static String AGE = "现在准备反序列化";
private String name;
private String color;
transient private String car;
// private String SerializableIDTest;
public static String getAGE() {
return AGE;
}
public static void setAGE(String AGE) {
DEMO1.AGE = AGE;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getCar() {
return car;
}
public void setCar(String car) {
this.car = car;
}
// public String getSerializableIDTest() {
// return SerializableIDTest;
// }
//
// public void setSerializableIDTest(String serializableIDTest) {
// SerializableIDTest = serializableIDTest;
// }
@Override
public String toString() {
return "DEMO1{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", car='" + car + '\'' +
", AGE='" + AGE + '\'' +
// ", SerializableIDTest='" + SerializableIDTest + '\'' +
'}';
}
}
运行结果如下:
抛异常:InvalidClassException 以及serialVersionUID等。
这个serialVersionUID到底是啥呢?
serialVersionUID
serialVersionUID是序列化版本号:
如果更改了源代,在进行反序列会报错。
原因:
因为我在DEMO1类中里面是没有明确的给这个 serialVersionUID 赋值,但是,Java会自动的给我赋值的,这个值跟这个DEMO1的属性相关计算出来的。
那为什么报错呢?
我保存写入磁盘文件TUDEEN.txt的时候,也就是我序列化的时候,那时候还没有这个SerializableIDTest属性呢,所以,自动生成的serialVersionUID 这个值,但是反序列化的时候Java自动生成的这个serialVersionUID值与序列化生成的serialVersionUID是不同的,他就抛异常了。
在DEMO1类下增加把 private static final long serialVersionUID = 1L; 然后在进行上述得一系列操作,查看结果如下:
这个时候,代码执行一切正常。序列化的时候,是没的那个属性的,在反序列化的时候,对应的DEMO1类多了个属性,但是,反序列化执行OK,没出异常。
(加上private static final long serialVersionUID = 1L 序列化id就不会报错)
Java语言中是采用什么机制来区分类的?
第一:首先通过类名进行对比,如果类名不一样肯定不是同一个类
第二:如果类名一样,靠序列化号版本进行区分,你定义了相同的序列化号就不会报异常了
序列化版本号是用来区分类的
不同的人编写了同一个类,但是这两个类不是同一个人编写的,这时候序列化就起作用了
对于Java虚拟机来说,Java虚拟机是可以区分开这两个类的,因为这两个类都实现了Serialaizeble接口
都有默认的序列化版本号,他们的序列化版本号不一样,所以区分开了(这是自动生成序列化版本号的好处)
自动生成的序列化版本号的缺陷:这种自动生成的序列化版本号缺点是:一旦代码确定之后,不能进行后续的修改,因为只要修改,必然要重新编译,
此时会生成全新的序列化版本号,这时候Java虚拟机会认为这是一个全新的类,这样是不好的
凡是一个类实现了Serializable接口,建议给该类提供一个固定的序列化版本号,这样即使这个类的代码被修改了,但是版本号不变,java虚拟机会认为是同一个类
serialVersionUID 的作用
serialVersionUID 的作用是验证版本一致性。如果serialVersionUID 一致,说明他们的版本是一样的。反之,就说明版本不同,就无法运行或使用相关功能。
(你还可以反过来,带ID去序列化,然后,没ID去反序列化。也是同样的问题。)
该怎么赋值?
eclipse可能会自动给你赋值个一长串数字。这个是没必要的。可以简单的赋值个 1L,这就可以啦。。这样可以确保代码一致时反序列化成功不同的serialVersionUID的值,会影响到反序列化,也就是数据的读取。
serialVersionUID的翻译:
序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段必须是静态 (static)、最终 (final) 的 (Long)型字段)显式声明其自己的 serialVersionUID:
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。例如之前操作的例子就会导致序列化与反序列化的serialVersionUID不同。
因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
ArrayList怎么序列化的知道吗? 为什么⽤transient修饰数组?
ArrayList的序列化不太⼀样,它使⽤ transient 修饰存储元素的 elementData 的数组, transient 关 键字的作⽤是让被修饰的成员属性不被序列化的标志。
为什么最ArrayList不直接序列化元素数组呢?
出于效率的考虑,数组可能长度100,但实际只⽤了5 0,剩下的50不⽤其实不⽤序列化,这样可以提⾼序列化和反序列化的效率,还可以节省内存空间。
那ArrayList怎么序列化呢?
ArrayList通过两个⽅法readObject、writeObject⾃定义序列化和反序列化策略,实际直接使⽤两个 流 ObjectOutputStream 和 ObjectInputStream 来进⾏序列化和反序列化。