本文 Github开源项目:github.com/hansonwang9… 中已收录,有详细自学编程学习路线、面试题和面经、编程资料及系列技术文章等,资源持续更新中...java
曾几什么时候,对于Java的序列化的认知一直停留在:「实现个Serializbale
接口」不就行了的状态,直到 ...git
因此此次抽时间再次从新捧起了尘封已久的《Java编程思想》,就像以前梳理《枚举部分知识》同样,把「序列化和反序列化」这块的知识点又从新审视了一遍。程序员
序列化的本来意图是但愿对一个Java对象做一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,因此概念上很好理解:github
并且序列化机制从某种意义上来讲也弥补了平台化的一些差别,毕竟转换后的字节流能够在其余平台上进行反序列化来恢复对象。面试
事情就是那么个事情,看起来很简单,不事后面的东西还很多,请往下看。编程
然而Java目前并无一个关键字能够直接去定义一个所谓的“可持久化”对象。数组
对象的持久化和反持久化须要靠程序员在代码里手动显式地进行序列化和反序列化还原的动做。网络
举个例子,假如咱们要对Student
类对象序列化到一个名为student.txt
的文本文件中,而后再经过文本文件反序列化成Student
类对象:ide
一、Student类定义函数
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("CodeSheep");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
System.out.println("序列化成功!已经生成student.txt文件");
System.out.println("==============================================");
}
复制代码
三、反序列化
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("反序列化结果为:");
System.out.println( student );
}
复制代码
四、运行结果
控制台打印:
序列化成功!已经生成student.txt文件
==============================================
反序列化结果为:
Student:
name = CodeSheep
age = 18
score = 1000
复制代码
上面在定义Student
类时,实现了一个Serializable
接口,然而当咱们点进Serializable
接口内部查看,发现它居然是一个空接口,并无包含任何方法!
试想,若是上面在定义Student
类时忘了加implements Serializable
时会发生什么呢?
实验结果是:此时的程序运行会报错,并抛出NotSerializableException
异常:
咱们按照错误提示,由源码一直跟到ObjectOutputStream
的writeObject0()
方法底层一看,才恍然大悟:
若是一个对象既不是字符串、数组、枚举,并且也没有实现Serializable
接口的话,在序列化时就会抛出NotSerializableException
异常!
哦,我明白了!
原来Serializable
接口也仅仅只是作一个标记用!!!
它告诉代码只要是实现了Serializable
接口的类都是能够被序列化的!然而真正的序列化动做不须要靠它完成。
serialVersionUID
号有何用?相信你必定常常看到有些类中定义了以下代码行,即定义了一个名为serialVersionUID
的字段:
private static final long serialVersionUID = -4392658638228508589L;
复制代码
你知道这句声明的含义吗?为何要搞一个名为serialVersionUID
的序列号?
继续来作一个简单实验,还拿上面的Student
类为例,咱们并无人为在里面显式地声明一个serialVersionUID
字段。
咱们首先仍是调用上面的serialize()
方法,将一个Student
对象序列化到本地磁盘上的student.txt
文件:
public static void serialize() throws IOException {
Student student = new Student();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 100 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
}
复制代码
接下来咱们在Student
类里面动点手脚,好比在里面再增长一个名为studentID
的字段,表示学生学号:
这时候,咱们拿刚才已经序列化到本地的student.txt
文件,还用以下代码进行反序列化,试图还原出刚才那个Student
对象:
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("反序列化结果为:");
System.out.println( student );
}
复制代码
运行发现报错了,而且抛出了InvalidClassException
异常:
这地方提示的信息很是明确了:序列化先后的serialVersionUID
号码不兼容!
从这地方最起码能够得出两个重要信息:
serialVersionUID
,那编译器会为它自动声明一个!第1个问题: serialVersionUID
序列化ID,能够当作是序列化和反序列化过程当中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID作比对,只有二者一致,才能从新反序列化,不然就会报异常来终止反序列化的过程。
第2个问题: 若是在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID
的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID
,一旦像上面同样更改了类的结构或者信息,则类的serialVersionUID
也会跟着变化!
因此,为了serialVersionUID
的肯定性,写代码时仍是建议,凡是implements Serializable
的类,都最好人为显式地为它声明一个serialVersionUID
明确值!
固然,若是不想手动赋值,你也能够借助IDE的自动添加功能,好比我使用的IntelliJ IDEA
,按alt + enter
就能够为类自动生成和添加serialVersionUID
字段,十分方便:
static
修饰的字段是不会被序列化的transient
修饰符修饰的字段也是不会被序列化的对于第一点,由于序列化保存的是对象的状态而非类的状态,因此会忽略static
静态域也是理所应当的。
对于第二点,就须要了解一下transient
修饰符的做用了。
若是在序列化某个类的对象时,就是不但愿某个字段被序列化(好比这个字段存放的是隐私值,如:密码
等),那这时就能够用transient
修饰符来修饰该字段。
好比在以前定义的Student
类中,加入一个密码字段,可是不但愿序列化到txt
文本,则能够:
这样在序列化Student
类对象时,password
字段会设置为默认值null
,这一点能够从反序列化所获得的结果来看出:
从上面的过程能够看出,序列化和反序列化的过程实际上是有漏洞的,由于从序列化到反序列化是有中间过程的,若是被别人拿到了中间字节流,而后加以伪造或者篡改,那反序列化出来的对象就会有必定风险了。
毕竟反序列化也至关于一种 “隐式的”对象构造 ,所以咱们但愿在反序列化时,进行受控的对象反序列化动做。
那怎么个受控法呢?
答案就是: 自行编写readObject()
函数,用于对象的反序列化构造,从而提供约束性。
既然自行编写readObject()
函数,那就能够作不少可控的事情:好比各类判断工做。
还以上面的Student
类为例,通常来讲学生的成绩应该在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
类的最底层,看到这里我相信你必定恍然大悟:
又是反射机制在起做用!是的,在Java里,果真万物皆可“反射”(滑稽),即便是类中定义的private
私有方法,也能被抠出来执行了,简直引发温馨了。
一个容易被忽略的问题是:可序列化的单例类有可能并不单例!
举个代码小例子就清楚了。
好比这里咱们先用java
写一个常见的「静态内部类」方式的单例模式实现:
public class Singleton implements Serializable {
private static final long serialVersionUID = -1576643344804979563L;
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
public static synchronized Singleton getSingleton() {
return SingletonHolder.singleton;
}
}
复制代码
而后写一个验证主函数:
public class Test2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(
new FileOutputStream( new File("singleton.txt") )
);
// 将单例对象先序列化到文本文件singleton.txt中
objectOutputStream.writeObject( Singleton.getSingleton() );
objectOutputStream.close();
ObjectInputStream objectInputStream =
new ObjectInputStream(
new FileInputStream( new File("singleton.txt") )
);
// 将文本文件singleton.txt中的对象反序列化为singleton1
Singleton singleton1 = (Singleton) objectInputStream.readObject();
objectInputStream.close();
Singleton singleton2 = Singleton.getSingleton();
// 运行结果竟打印 false !
System.out.println( singleton1 == singleton2 );
}
}
复制代码
运行后咱们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到咱们的目标。
解决办法是:在单例类中手写readResolve()
函数,直接返回单例对象,来规避之:
private Object readResolve() {
return SingletonHolder.singleton;
}
复制代码
这样一来,当反序列化从流中读取对象时,readResolve()
会被调用,用其中返回的对象替代反序列化新建的对象。
本觉得这篇会很快写完,结果又扯出了这么多东西,不过这样一梳理、一串联,感受仍是清晰了很多。
就这样吧,下篇见。
本文 Github开源项目:github.com/hansonwang9… 中已收录,有详细自学编程学习路线、面试题和面经、编程资料及系列技术文章等,资源持续更新中...
慢一点,才能更快