我以前确实对序列化,乃至现在也是不是很熟悉,有时候查找资料,但依旧懵懵懂懂,不过还好遇到一个博主,确定写的挺好的,链接会放再底部
废话不多说,先看官网定义:
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
序列化主要有两个用途
- 把对象的字节序列永久保存到硬盘上,通常存放在一个文件中(序列化对象)
- 在网络上传送对象的字节序列(网络传输对象)
实际上就是将数据持久化,防止一直存储在内存当中,消耗内存资源。而且序列化后也能更好的便于网络运输何传播
- 序列化:将java对象转换为字节序列
- 反序列化:把字节序列回复为原先的java对象
对象如何序列化?
java目前没有一个关键字是直接定义一个所谓的“可持久化”对象
对象的持久化和反持久化需要靠程序员在代码里手动显式地进行序列化和反序列化还原的动作。
举个例子,假如我们要对Student
类对象序列化到一个名为student.txt
的文本文件中,然后再通过文本文件反序列化成Student
类对象:
按我的理解就是序列化:将一个对象转化为一种格式,能够更好的传输和电脑理解。
反序列化是转换过来,便于人们观看的。
先写一个实体类
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
//getter、setter省略
}
然后填写一个测试类
public class test01 {
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("linko");
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 );
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
serialize();
deserialize();
}
}
运行结果:
Serializable
接口的作用
这时候我们来看下Serializable接口,这时候我们点进去,会发现这个接口是个空接口,并没有包含任何方法
我们会感觉这个接口没什么用,那这时我们不继承Serializable接口,运行一下试试
这时候我们会看到这个错误,java.io.NotSerializableException
报出了没有序列化异常
然后我们按照错误提示,由源码一直跟到ObjectOutputStream
的writeObject0()
方法底层(虽然我看不懂
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable
接口的话,在序列化时就会抛出NotSerializableException
异常!
说的太好了哭,强烈推荐底部链接的那个博主
他一说我就大概知道懂什么意思了。
先说说instanceof
,它是用来测试一个对象是否为一个类的实例
// remaining cases
//判断这个obj对象是否是String类
if (obj instanceof String) {
writeString((String) obj, unshared);
//判断这个obj对象是否是数组
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
//判断这个obj对象是否是枚举
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
//判断这个obj对象是否是实现序列化
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
//否则就报错
} else {
//报错是否由有扩展信息,详细信息可以观看源码
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
所以Serializable
接口只是一个做标记用的
它告诉代码只要是实现了Serializable
接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。
serialVersionUID
号有何用?
在我们写项目的时候,我们会经常看到这么这么一个语句。
定义一个名为serialVersionUID
的字段
private static final long serialVersionUID = -4392658638228508589L;
为什么有这句话呢,为什么要搞一个名为**serialVersionUID
**的序列号
继续做一个简单实验,依旧是上面的Student
类,然后我们先不写序列号。然后在Student
中添加一个字段,并添加序列号
private static final long serialVersionUID = -4392658638228508589L;
private String name;
private Integer age;
private Integer score;
private Long studentId;
然后再次序列化和反序列化一下
再然后我们拿刚才已经序列化好的student.txt
文件,然后试着反序列化一下。
在test01
测试类中,我们将主函数的序列化调用的函数给删掉,再把序列号给删掉
public static void main(String[] args) throws IOException, ClassNotFoundException {
//serialize();
deserialize();
}
我们会发现报错了。太长了,没能截屏出来
它说序列号不一致
Exception in thread "main" java.io.InvalidClassException: demo01.Student; local class incompatible: stream classdesc serialVersionUID = -4392658638228508589, local class serialVersionUID = -2825960062149872719
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at demo01.test01.deserialize(test01.java:32)
at demo01.test01.main(test01.java:41)
从这几个地方我们可以看出几个重要的信息
- serialVersionUID是序列化前后的唯一标识符
- 默认如果没有人为显式定义过
serialVersionUID
,那编译器会为它自动声明一个!
第1个问题: serialVersionUID
序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。
所以,为了serialVersionUID
的确定性,写代码时还是建议,凡是implements Serializable
的类,都最好人为显式地为它声明一个serialVersionUID
明确值!
当然,如果不想手动赋值,你也可以借助IDE的自动添加功能,比如我使用的IntelliJ IDEA
,按alt + enter
就可以为类自动生成和添加serialVersionUID
字段,十分方便;
两种特殊情况
- 凡是被
static
修饰的字段是不会被序列化的 凡是被更正:经过@Programming_artist同学的修正和参考这位作者Java中关键字transient解析,这里不能一概而论。只有实现transient
修饰符修饰的字段也是不会被序列化的Serializable
接口,被transient
修饰符修饰的字段也是不会被序列化的 。后面详细解释一下
对于第一个,因为序列化保存的是对象的状态而非类的状态,所以会忽略static
静态域。
有同学@青铜大神 有个疑问说它测试说经过static修饰后还是能够序列化,这其实是不严谨的。可能是我当时举的例子还不够严谨(历经一年的断更,最近在实习,就忘了更新博客,我的罪过,这里就小更一下吧)
我还是以上面的代码为例吧,就不作太大改动了,现在将年龄age
加一个static
修饰
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private static Integer age;
private Integer score;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
//getter、setter省略
}
然后我们再写一个测试类
public class test01 {
//打印日志
private static Logger logger = LoggerFactory.getLogger(test01.class);
public static void serialize(Student student,String fileName) throws IOException {
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File(fileName) ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
logger.info("序列化成功!已经生成{}文件",fileName);
logger.info("==============================================");
//如果嫌麻烦,也可以用下面语句直接打印,经过一年的沉淀,笔者也是有所成长hhh
//System.out.println("序列化成功!已经生成student.txt文件");
//System.out.println("==============================================");
}
public static void deserialize(String fileName) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File(fileName) ) );
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
logger.info("反序列化结果:{}",student);
//下面日志同样注释了
//System.out.println("反序列化结果为:");
//System.out.println( student );
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
//生成第一个文件
Student studentOne = new Student();
studentOne.setName("linko");
studentOne.setAge(18);
studentOne.setScore(100);
String fileNameOne = "studentOne.txt";
//模拟序列化的过程
serialize(studentOne,fileNameOne);
//生成第二个文件
Student studentTwo = new Student();
studentTwo.setName("实习两年半");//这个词!!!警觉
studentTwo.setAge(24);
studentTwo.setScore(250);
String fileNameTwo = "studentTwo.txt";
//模拟序列化的过程
serialize(studentTwo,fileNameTwo);
//反序列化第一个文件
deserialize(fileNameOne);
//反序列化第二个文件
deserialize(fileNameTwo);
}
}
之前第一个写的测试就不更新了,刚好可以看出我这一年小细节上的进步hhh。
这里打印路径可能跟之前不一样了,为什么呢(因为我也忘记把之前写的测试放哪去了)
现在我们重点关注下年龄Age,因为static
修饰的就是age
。
我们可以看到两个反序列化的结果中,年龄age
的值是一样的。但是我们从代码中可以看出,我们两次赋值是不一样的。
第一个Student
赋值是18
,第二个Student
赋值是24
,但是第一个Student的值在反序列化之后的结果不一样了。
这是为什么呢?
这里需要了解一点jvm
中的知识,可以稍微了解一下。
对象的序列化是操作堆内存
当中的数据,而静态的变量又称作类变量,其数据存放在方法区
(jdk版本不同可能叫法不同)里,类一加载,就初始化了。
而因为静态变量age
没有被序列化,根本就没写入文件流中,所以我们打印的值其实一直都是当前Student类的静态变量age
的值,而静态变量又是所有的对象共享的一个变量,所以就都是24
,也就是直接读取类变量。
对于第二个,就需要了解transient
修饰符的作用了(在实现Serializable
接口的情况下
如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码
等),那这时就可以用transient
修饰符来修饰该字段。
比如在之前定义的Student
类中,加入一个密码字段,但是不希望序列化到txt
文本,则可以:
jvm是通过Serializable
这个标识来实现序列化的,那么我们是否可以自己自定义是否决定序列化呢?答案是可以的,java给我们提供了Externalizable
接口,让我们自己实现。
从实现的接口上看,它是继承了Serializable
接口,但使用这个接口,需要实现writeExternal
以及readExternal
这两个方法,来自己实现序列化和反序列化。
实现的过程中,需要自己指定需要序列化的成员变量,此时,static和transient关键词都是不生效的,因为你重写了序列化中的方法。感觉这个static验证也可以再多写点(有时间再更呜
约束性加持(后面两个有时间再更)
从上面的过程可以看出,序列化和反序列化是有漏洞的,因为序列化和反序列化是有中间过程的,如果别人拿到中间字节流,然后加以伪造或篡改,那反序列化出来的对象就有一定风险了。