前言
记录学习过程
学的越多越感觉渺小,序列化是一个深坑
目录
- 概念
- Java的序列化机制
2.1 Serializable接口
2.2 Externalizable接口
2.3 区别 - 序列化与构造方法
- serialVersionUID的作用
- 静态变量的序列化问题
- transient 关键字的使用
- 自定义序列化
- 更多操作
- 总结
序列化
概念
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,
这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,
就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
Java 对象序列化就能够帮助我们实现该功能
Java序列化是指把Java对象转换为字节序列的过程,即是把对象写入IO流中
Java序列化的常见运用场景:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;
很明显,序列化的好处就在与永久的保存数据和实现远程通信
TCP/IP 远程传送数据:
Java的序列化机制
Java中实现序列化有两个接口:Serializable、Externalizable
Serializable接口
可以看到Serializable接口是属于IO包,即IO输入输出流
Serializable接口是一个标记接口,不用实现任何方法,实现该接口就可以完成序列化
import java.io.Serializable;
可以看看jdk文档的Serializable注释
使用Serializable实现序列化
创建一个User类:
package com.company.NewEntity;
import java.io.Serializable;
public class User implements Serializable {
private int id;
private String name;
//序列化对象版本控制
private static final long serialVersionUID = 1L;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
测试类TestUser实现序列化、反序列化(创建一个文件提供写入读出)
序列化步骤:
- 创建对象
- 创建文件字节输出流
- 对象的序列化输出流
- 写入文件
- 关闭序列化输出流
反序列化步骤与序列化相似,将输出改为输入
package com.company.NewEntity;
import java.io.*;
public class TestUser {
public static void main(String[] args) throws Exception, IOException {
SerializeUser();
DeSerializeUser();
}
//序列化方法
private static void SerializeUser() throws IOException {
User user=new User();
user.setId(1);
user.setName("zhangsan");
//序列化对象到文档
try {
//文件字节输出流
FileOutputStream fileOutputStream=new FileOutputStream("UserFile.txt");
//对象的序列化流:把对象转成字节数据的输出到文件中保存
ObjectOutputStream outputStream=new ObjectOutputStream(fileOutputStream);
//写入
outputStream.writeObject(user);
//关闭对象的序列化流
outputStream.close();
System.out.println("写入成功");
}
catch (FileNotFoundException e){
System.out.println("文件找不到");
}
catch (IOException io){
System.out.println("输出流失败");
}
}
//反序列化方法
private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
//创建文件对象,得到对应地址的文件
File file=new File("UserFile.txt");
//文件字节输入流
FileInputStream fileInputStream=new FileInputStream(file);
//对象的反序列化输入流
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
//读出对象
User user= (User) objectInputStream.readObject();
System.out.println(user.toString());
objectInputStream.close();
}
}
完成简单的序列化和反序列化
使用Externalizable实现序列化
Externalizable的特点
- Externalizable是Serializable的子类
- 继承Externalizable实现序列化需要重写readExternal和writeExternal方法
- 实现Externalizable接口的类必须要提供一个public的无参的构造器
反序列化时会默认调用无参构造实例化对象,如果没有此无参构造,则运行时将会出现异常
改变类User
package com.company.NewEntity;
import java.io.*;
public class User implements Externalizable {
private int id;
private String name;
//序列化对象版本控制
private static final long serialVersionUID = 1L;
public User() {
}
//省略set、get方法
//省略toString方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id);
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
id= in.readInt();
name= (String) in.readObject();
}
}
在User对象中重写方法,如果不重写方法得不到对象
重写完后继续运行上面的测试类:
需要注意:反序列化获得的对象不是通过构造方法实例的,而是JVM自己生成的对象
可以试着去验证一下序列化与构造方法
Externalizable接口、Serializable接口的区别
实现Serializable接口
- 系统自动存储必要的信息
- Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持
- 性能略差
实现Externalizable接口
- 程序员决定存储哪些信息
- 必须实现接口内的两个方法
- 性能略好
序列化与构造方法
验证一下序列化与构造法的关系:
修改一下User类
运行一下测试类:
可以看出,序列化和反序列化都与带参构造方法无关
序列化会调用无参构造方法
而反序列不会调用构造方法,是JVM生成的对象
关于父类、子类Java序列化与空参构造方法的关系详解
serialVersionUID的作用
随着项目的升级,class文件肯定会发生改变,而反序列必须拥有class文件,就必须讨论版本兼容问题
serialVersionUID的作用:序列化对象版本控制,有关各版本反序列化时是否兼容
兼容问题就像其他东西的版本兼容:
- 新版本修改了属性,旧版本肯定不兼容,如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量
例:前面已经序列化输出对象了,接下来我们把User中id的类型改为String,再反序列化得到对象
就会发现已经不兼容了
这个时候就需要手动修改serialVersionUID版本号,然后运行序列化、反序列化,将旧版本的对象更新
- 修改serialVersionUID版本号,会造成反序列失败
例:前面已经序列化输出对象,把版本号从1改为12,再反序列
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号
例:前面序列化了对象,关闭输出流后修改getId方法
public int getId() {
System.out.println("getId");
return id;
}
然后在反序列化方法中运行getId方法
确实,修改方法不会影响反序列化
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号(因为这两个变量根本不会进入序列化)
serialVersionUID有两种显示的生成方式:
一是默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;
不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化
静态变量的序列化问题
静态变量不会被序列化
为什么?
静态变量在全局区,本来流里面就没有写入静态变量,打印静态变量当然会去全局区查找,当write read 同时使用时,内存中的静态变量变了,所以打印出来的也变了
package com.company.NewEntity;
import java.io.*;
public class TestUser {
public static void main(String[] args) throws Exception, IOException {
SerializeUser();
DeSerializeUser();
}
//序列化方法
private static void SerializeUser() throws IOException {
User user=new User(1,"zhangsan");
//序列化对象到文档
try {
//文件字节输出流
FileOutputStream fileOutputStream=new FileOutputStream("UserFile.txt");
//对象的序列化流:把对象转成字节数据的输出到文件中保存
ObjectOutputStream outputStream=new ObjectOutputStream(fileOutputStream);
//写入
outputStream.writeObject(user);
//关闭对象的序列化流
outputStream.close();
user.setId(3);
System.out.println("写入成功");
}
catch (FileNotFoundException e){
System.out.println("文件找不到");
}
catch (IOException io){
System.out.println("输出流失败");
}
}
//反序列化方法
private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
//创建文件对象,得到对应地址的文件
File file=new File("UserFile.txt");
//文件字节输入流
FileInputStream fileInputStream=new FileInputStream(file);
//对象的反序列化输入流
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
//读出对象
User user= (User) objectInputStream.readObject();
System.out.println(user.toString());
objectInputStream.close();
}
}
对于序列化的方法,在输出流关闭后,通过setId()方法把user的id改为3
如果id序列化成功了,id应该是不变的,以为反序列化只是将文件里的对象取出
事实是id改变了,也就是说静态变量不能序列化,静态变量是在全局区里改变、查找的
transient 关键字的使用
transient 关键字的作用是控制变量的序列化
在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null
private transient int id;
注意:不是import java.beans.Transient;
添加了transient关键字后,id被阻止序列化到文件了
transient是控制变量的序列化,将属性完全隔离在了序列化外
自定义序列化
Java提供了自定义序列化,可以进行控制序列化的方式,或者对序列化数据进行编码加密
- Serializable通过重写writeObject、readObject方法实现自定义序列化
package com.company.NewEntity;
import java.beans.Transient;
import java.io.*;
public class User implements Serializable{
private int id;
private String name;
//序列化对象版本控制
private static final long serialVersionUID = 12L;
public User() {
}
public User(int id, String name) {
this.id = id;
this.name = name;
}
//省略toString方法
//省略set、get方法
//在序列化时调用
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(name);
System.out.println("writeObject");
}
//在反序列化时调用
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
name= (String) in.readObject();
System.out.println("readObject");
}
}
注意:writeObject、readObject是包私有private 方法
注意:readObject()的顺序要和writeObject()的顺序一致,即在readObject中属性顺序为 id ,name ,writeObject也要是 id ,name
如果没有重写,在序列化、反序列化时使用默认的方法,重写了就会使用我们重写的方法
上面重写的是将name写入序列化,而id没写入
- 编码加密
可以再属性上加密:name上小小的加密(还有很多加密方式)
//在序列化时调用
private void writeObject(ObjectOutputStream out) throws IOException {
//加密
out.writeObject(new StringBuffer(name).reverse());
System.out.println("writeObject");
}
如果不解密的话
再在readObject解密:
//在反序列化时调用
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
//readObject返回的是object,要类型转换
//反序列化解码
StringBuffer nameSB= (StringBuffer) in.readObject();
name=nameSB.reverse().toString();
System.out.println("readObject");
}
- 偷梁换柱writeReplace()方法 -将输入的对象替换
将上面的writeObject、readObject方法注释,写上writeReplace()方法
private Object writeReplace(){
return new String("ok");
}
将User对象换成String对象“ok”
当然反序列化要改一下得到的对象类型
//反序列化方法
private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
//创建文件对象,得到对应地址的文件
File file=new File("UserFile.txt");
//文件字节输入流
FileInputStream fileInputStream=new FileInputStream(file);
//对象的反序列化输入流
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
//读出对象
/* User user= (User) objectInputStream.readObject();*/
String str=(String)objectInputStream.readObject();
System.out.println(str.toString());
objectInputStream.close();
}
User:明明先来的是我!!
- 偷梁换柱readResolve()方法 - 将输出的对象替换
反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用
private Object readResolve() throws ObjectStreamException{
return new Integer(3);
}
再改一下readObject改一下:
//反序列化方法
private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
//创建文件对象,得到对应地址的文件
File file=new File("UserFile.txt");
//文件字节输入流
FileInputStream fileInputStream=new FileInputStream(file);
//对象的反序列化输入流
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
//读出对象
Integer integer= (Integer) objectInputStream.readObject();
System.out.println(integer.toString());
objectInputStream.close();
}
读出Integer对象 3
readResolve常用来反序列单例类,保证单例类的唯一性
- Externalizable自定义序列化
Externalizable本来就是自定义的序列化,前面已经知道继承Externalizable接口就必须重新两个方法writeExternal,readExternal
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
System.out.println("writeExternal");
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name= (String) in.readObject();
System.out.println("readExternal");
}
操作和Serializable一样
更多操作
- 使用序列化实现深度克隆
- readObjectNoData()方法可以用来正确地初始化反序列化的对象
涉及序列化的技术
总结
- 序列化是为了保存对象或者远程传输对象
- Java有两个序列化接口,各有特点
- 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量、静态变量都不会被序列化
- 序列化、反序列化与带参构造方法无关,序列化会调用无参构造方法,而反序列不会调用构造方法,是JVM生成的对象
- 序列化时,如果第一个非可序列化超类非Object类,那么一定要预留一个空参构造方法,否则就会序列化失败(关于父子类的序列化问题)
- transient修饰的变量会被阻止进入序列化
- 可以通过自定义序列化进行控制序列化或者编码加密
- 偷梁换柱writeReplace、readResolve方法
- 序列化是个需要深入的知识