认识对象序列化机制
何为对象序列化机制?
对象序列化机制
,允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其他程序获取到了这种二进制流,就可以恢复成原来的Java对象。
- 序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该
对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存
了一个对象的信息。 - 反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行
反序列化
。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。
序列化机制的重要性
序列化是RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值都必须实现的机制,而RMI是JavaEE的基础。因此序列化机制是JavaEE平台的基础。
序列化的好处,在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。
实现原理
- 序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。方法为:
public final void writeObject(Object obj)
:将指定的对象写出。
- 反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。方法为:
public final Object readObject()
:读取一个对象。
案例:演示序列化与反序列过程
序列化:
/**
* 序列化过程,使用ObjectOutputStream流实现,将内存中的Java对象保存在文件中或通过网络传播出去
*/
@Test
public void test(){
File file = new File("object.txt");
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeUTF("江山如此多娇,引无数英雄竞折腰");
oos.flush();
oos.writeObject("轻轻地我走了,正如我轻轻地来");
oos.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
反序列化:
/**
* 反序列化过程:将存储在文件中的编码读入到内存中转换成数据
*/
@Test
public void test1() {
File file = new File("object.txt");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
String s1 = ois.readUTF();
String s2 =(String) ois.readObject();
System.out.println(s1);
System.out.println(s2);
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
打印结果:
序列化之后使用文件保存,但是这个文件不是给我们看的,而是进行反序列化的,序列化后的文件中的数据肯定都是乱码,序列化保存在文件中或者通过网络传输后,让我们可以再使用反序列化读取到内存中。
以上序列化与反序列化的对象都是String类型的,对于自定义类型的对象进行序列化和反序列化是否有什么要求吗?
有要求,下面就介绍一下有哪些要求。
如何实现序列化机制
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现**java.io.Serializable
**接口。
Serializable
是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。
- 如果对象的某个属性也是引用属性类型,那么如果该属性也要序列化的话,也要实现
Serializable
接口 - 该类得到所有属性必须都是可序列化的。如果有一个属性不需要可序列化,则该属性必须注明是瞬态的,使用
transient
关键字修饰。 静态
(static)变量的值不会序列化。因为静态变量的值不属于某个对象。
Serializable
接口给需要序列化的类,提供一个序列版本号:serialVersionUID
。凡是实现Serializable接口的类都应该有一个表示序列化版本标识符的静态变量:
static final long serialVersionUID = 127398132789L;//它的值由程序员随意指定即可。
- serivalVersionUID用来表明类的不同版本间的兼容性。简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(
InvalidCastException
)。 - 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节
自动生成
的。若类的实例变量做了修改,serialVersionUID可能发生变化
。因此,需要显式声明。 - 如果声明了serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已。
测试:
定义一个类Person,然后直接去使用对象流将创建的Person类对象写出到文件中:
定义的Person类:
class Person{
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//getter、setter方法以及toString()方法省略
}
使用ObjectOutputStream进行输出测试:
@Test
public void test3(){
File file = new File("object1.dat");
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(file));
Person p1 = new Person("Tom", 22);
oos.writeObject(p1);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
执行结果:
原因就在于Person类未实现Serializable
接口,该类就不可进行序列化与反序列化操作。
当自定义类Person实现Serializable接口后,就执行成功了。
然后我们进行反序列化,将文件中的数据转换成类对象数据:
@Test
public void test4(){
File file = new File("object1.dat");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
Person p = (Person)ois.readObject();
System.out.println(p);
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
打印结果:
我们注意到,这个Person类中没有设置serialVersionUID
,但是序列化与反序列化过程依旧成功了,这是为什么呢?
实际上是因为Person类没有发生变化。
让我们思考一下serivalVersionUID这个全局常量的作用:
标识传输的对象是否属于同一个类
。
当我们进行序列化时,比如对Person类对象进行序列化,序列化完成后进行了存储、网络传输等等一些操作,然后我们想要对序列化的文件进行反序列化成内存中的Person类对象数据,那么肯定是需要转换成相同的Person类型对象,所以serivalVersionUID就可以去告诉我们转换前对象所属的类型与转换后的类型是否属于同一个类,是否该类发生了变化(因为类型改变以后,对象无法转变回来)。
案例:
当未设置serialVersionUID后,我们对Person类进行修改,比如增加一个id类型的属性:
class Person implements Serializable{
String name;
int age;
int id;
//省略其他构造器和方法
}
此时去测试输入流,打印结果为:
结果报错,实际上就是因为存储到文件中的对象,反序列化接收的类是修改过的,会报错。
此时,我们就可以使用serialVersionUID
全局常量,用来验证版本的一致性,
当给Person类添加serialVersionUID全局常量:
class Person implements Serializable{
@Serial
private static final long serialVersionUID = 12316387126L;
String name;
int age;
int id;
}
此时再去调用输入流进行测试:
此时就能够让我们得到传入的Person类型对象数据。
总结:
自定义类要想实现序列化机制,需要满足:
- 自定义类需要实现接口:
Serializable
- 要求自定义类声明一个全局常量:
static final long serialVersionUID
- 要求自定义类的各个属性也必须是可序列化的。
- 对于基本数据类型的属性,默认就是可序列化的。
- 对于引用数据类型的属性,要求实现
Serializable
接口
注意点:
-
如果不声明全局常量serialVersionUID,系统会自动声明生产一个针对于当前类的serialVersionUID。
如果修改此类的话,会导致serialVersionUID变化,进而导致反序列化时,出现
InvalidClassException
异常。 -
类中的属性如果声明为
transient
或static
,不会实现序列化。
在实际的开发中,一般在网络传输中使用最多的都是String类型的数据(JSON数据
),String类本身就去实现了Serializable接口,不需要我们另外进行操作就可以进行序列化了。
应用场景案例
对象序列化和反序列化在实际应用中有多种用途。以下是一些常见的应用场景案例:
- 数据持久化:将对象保存到磁盘或数据库中,以便后续使用或恢复。
- 远程调用:在分布式系统中,可以将对象进行序列化后通过网络传输,实现远程调用。
- 缓存机制:将对象序列化后保存在缓存中,提高系统性能。
- 消息传递:在消息队列中,可以将对象序列化后发送给其他系统进行处理。