Java序列化、反序列化机制
序列化、反序列化
文章目录
- 序列化、反序列化
- 一、概念
- 二、如何实现序列化
- 三、Serializable的作用
- 四、serialVersionUID号的作用
- 五、不会被序列化的情况
- 六、序列化的受控和加强
- 七、继承关系序列化问题
- 八、反序列化创建对象的方式
一、概念
序列化
- 把Java对象转换为与平台无关的二进制流,更便于跨平台存储和网络传播
反序列化
- 把二进制流恢复为原先的Java对象
二、如何实现序列化
将待序列化的类实现Serializable接口
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
@Override
public String toString() {
return "Student:" + 'n' +
"name = " + this.name + 'n' +
"age = " + this.age + 'n' +
"score = " + this.score + 'n'
;
}
// ... 其他省略 ...
}
序列化方法
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("周杰伦");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
}
反序列化方法
public static void deserialize( ) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列化结果为:" + student);
}
运行结果
反序列化结果为:
Student:
name = 周杰伦
age = 18
score = 1000
三、Serializable的作用
如果上述的
Student
类没有实现Serializable
接口,程序运行会报错根据错误提示,进入源码
ObjectOutputStream
的writeObject0()
方法,部分内容如下根据源码内容,可以得到如下结论:如果一个对象既不是字符串、数组、枚举,而且也没有实现
Serializable
接口的话,在序列化时就会抛出NotSerializableException
异常Serializable
接口也仅仅只是做一个标记用,它告诉代码只要是实现了Serializable
接口的类都是可以被序列化的,然而真正的序列化动作不需要靠它完成
四、serialVersionUID号的作用
在某些 JDK 自带的类中,经常会看到一个名为 serialVersionUID
的字段:
private static final long serialVersionUID = -4392658638228508589L;
对于上述 Student
类,并没有显示的声明一个 serialVersionUID
,发现序列化 / 反序列化一切正常。如果将对象序列化到本地后,在反序列化之前对 Student
类做任意的修改(比如添加属性或者修改属性名等操作),在反序列化时会报错,如下所示:
错误内容表示序列化前后的 serialVersionUID
号码不兼容
因此可以得到如下三个结论:
serialVersionUID是序列化前后的唯一标识符
serialVersionUID
序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程
如果没有人为显式定义过
serialVersionUID
,那编译器会为它自动声明一个- 如果在定义一个可序列化的类时,没有人为显式地给它定义一个
serialVersionUID
的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID
,但是一旦更改了类的结构或者信息,则类的serialVersionUID
也会跟着变化,这也是上述报错的原因 - 所以,为了
serialVersionUID
的确定性,写代码时还是建议凡是implements Serializable
的类,都最好人为显式地为它声明一个serialVersionUID
明确值
- 如果在定义一个可序列化的类时,没有人为显式地给它定义一个
- 只要序列化版本号一致,修改类结构并不会影响反序列化对象
五、不会被序列化的情况
凡是被
static
修饰的字段是不会被序列化的- 序列化保存的是对象的状态而非类的状态,所以会忽略
static
静态域也是理所应当的 - 反序列化得到的值读取的是虚拟机里面的此字段的值,所以 String 型不一定为 null 值
- 序列化保存的是对象的状态而非类的状态,所以会忽略
凡是被
transient
修饰的字段是不会被序列化的- 如果在序列化某个类的对象时,不希望某个字段被序列化(比如这个字段存放的是隐私值,如密码等),那这时就可以用
transient
修饰符来修饰该字段 - 反序列化得到的字段值采用默认值,比如 String 型为 null 值
- 如果在序列化某个类的对象时,不希望某个字段被序列化(比如这个字段存放的是隐私值,如密码等),那这时就可以用
六、序列化的受控和加强
- 从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了
解决方式是自行编写
readObject()
函数,用于对象的反序列化构造,从而提供约束性,既然自行编写readObject()
函数,那就可以做很多可控的事情:比如各种判断工作,如下://一般来说学生的成绩应该在0 ~ 100之间,我们为了防止学生的考试成绩在反序列化时被别人篡改成不符合常规的值,可以自行编写readObject()函数用于反序列化的控制
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
// 调用默认的反序列化函数
objectInputStream.defaultReadObject();
// 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作!
if( 0 > score || 100 < score ) {
throw new IllegalArgumentException("学生分数只能在0到100之间!");
}
}
此时故意将学生的分数改为101,此时反序列化操作立马终止并且报错:
自定义的
private
的readObject()
方法可以被自动调用的原因:进入
ObjectStreamClass
类的最底层源码:可以发现,通过反射调用了类中定义的
private
私有方法
七、继承关系序列化问题
序列化是以正向递归的形式进行的,如果父类实现了序列化那么其子类都将被序列化;子类实现了序列化而父类没实现序列化,那么只有子类的属性会进行序列化,而父类的属性是不会进行序列化的
八、反序列化创建对象的方式
反序列化时获取对象需要调用对象输入流 ObjectInputStream
中的 readObject()
方法,进入此方法的源码,继续进入其调用的 readObject0
方法,再进入其调用的 readOrdinaryObject
方法:
观察上述源码发现,在反序列化过程中,会判断待序列化类是否定义了 readResolve
方法,如果定义了此方法,则反序列化返回的对象是此方法返回的对象,否则返回新new出来的对象(这点应用在了单例模式中)
还没有评论,来说两句吧...