相信你们平常开发中,常常看到Java对象“implements Serializable”。那么,它到底有什么用呢?本文从如下几个角度来解析序列这一块知识点~html
Java对象是运行在JVM的堆内存中的,若是JVM中止后,它的生命也就戛然而止。java
打个比喻,做为大城市漂泊的码农,搬家是常态。当咱们搬书桌时,桌子太大了就通不过比较小的门,所以咱们须要把它拆开再搬过去,这个拆桌子的过程就是序列化。 而咱们把书桌复原回来(安装)的过程就是反序列化啦。面试
序列化使得对象能够脱离程序运行而独立存在,它主要有两种用途:segmentfault
好比 Web服务器中的Session对象,当有 10+万用户并发访问的,就有可能出现10万个Session对象,内存可能消化不良,因而Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。数组
咱们在使用Dubbo远程调用服务框架时,须要把传输的Java对象实现Serializable接口,即让Java对象序列化,由于这样才能让对象在网络上传输。bash
java.io.ObjectOutputStream
java.io.ObjectInputStream
java.io.Serializable
java.io.Externalizable
复制代码
Serializable接口是一个标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。服务器
public interface Serializable {
}
复制代码
Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal(),若是开发人员使用Externalizable来实现序列化和反序列化,须要重写writeExternal()和readExternal()方法网络
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
复制代码
表示对象输出流,它的writeObject(Object obj)方法能够对指定obj对象参数进行序列化,再把获得的字节序列写到一个目标输出流中。并发
表示对象输入流, 它的readObject()方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。框架
序列化如何使用?来看一下,序列化的使用的几个关键点吧:
public class Student implements Serializable {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
复制代码
把Student对象设置值后,写入一个文件,即序列化,哈哈~
ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out"));
Student student = new Student();
student.setAge(25);
student.setName("jayWei");
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();
复制代码
看看序列化的可爱模样吧,test.out文件内容以下(使用UltraEdit打开):
再把test.out文件读取出来,反序列化为Student对象
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student = (Student) objectInputStream.readObject();
System.out.println("name="+student.getName());
复制代码
Serializable接口,只是一个空的接口,没有方法或字段,为何这么神奇,实现了它就可让对象序列化了?
public interface Serializable {
}
复制代码
为了验证Serializable的做用,把以上demo的Student对象,去掉实现Serializable接口,看序列化过程怎样吧~
序列化过程当中抛出异常啦,堆栈信息以下:
Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.example.demo.Test.main(Test.java:13)
复制代码
顺着堆栈信息看一下,原来有重大发现,以下~
序列化的方法就是writeObject,基于以上的demo,咱们来分析一波它的核心方法调用链吧~(建议你们也去debug看一下这个方法,感兴趣的话)
writeObject直接调用的就是writeObject0()方法,
public final void writeObject(Object obj) throws IOException {
......
writeObject0(obj, false);
......
}
复制代码
writeObject0 主要实现是对象的不一样类型,调用不一样的方法写入序列化数据,这里面若是对象实现了Serializable接口,就调用writeOrdinaryObject()方法~
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
......
//String类型
if (obj instanceof String) {
writeString((String) obj, unshared);
//数组类型
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
//枚举类型
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
//Serializable实现序列化接口
} 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());
}
}
......
复制代码
writeOrdinaryObject()会先调用writeClassDesc(desc),写入该类的生成信息,而后调用writeSerialData方法,写入序列化数据
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
......
//调用ObjectStreamClass的写入方法
writeClassDesc(desc, false);
// 判断是否实现了Externalizable接口
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//写入序列化数据
writeSerialData(obj, desc);
}
.....
}
复制代码
writeSerialData()实现的就是写入被序列化对象的字段数据
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
for (int i = 0; i < slots.length; i++) {
if (slotDesc.hasWriteObjectMethod()) {
//若是被序列化的对象自定义实现了writeObject()方法,则执行这个代码块
slotDesc.invokeWriteObject(obj, this);
} else {
// 调用默认的方法写入实例数据
defaultWriteFields(obj, slotDesc);
}
}
}
复制代码
defaultWriteFields()方法,获取类的基本数据类型数据,直接写入底层字节容器;获取类的obj类型数据,循环递归调用writeObject0()方法,写入数据~
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
// 获取类的基本数据类型数据,保存到primVals字节数组
desc.getPrimFieldValues(obj, primVals);
//primVals的基本类型数据写到底层字节容器
bout.write(primVals, 0, primDataSize, false);
// 获取对应类的全部字段对象
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
// 获取类的obj类型数据,保存到objVals字节数组
desc.getObjFieldValues(obj, objVals);
//对全部Object类型的字段,循环
for (int i = 0; i < objVals.length; i++) {
......
//递归调用writeObject0()方法,写入对应的数据
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
......
}
}
复制代码
static静态变量和transient 修饰的字段是不会被序列化的,咱们来看例子分析一波~ Student类加了一个类变量gender和一个transient修饰的字段specialty
public class Student implements Serializable {
private Integer age;
private String name;
public static String gender = "男";
transient String specialty = "计算机专业";
public String getSpecialty() {
return specialty;
}
public void setSpecialty(String specialty) {
this.specialty = specialty;
}
@Override
public String toString() {
return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' +
'}';
}
......
复制代码
打印学生对象,序列化到文件,接着修改静态变量的值,再反序列化,输出反序列化后的对象~
序列化前Student{age=25, name='jayWei', gender='男', specialty='计算机专业'}
序列化后Student{age=25, name='jayWei', gender='女', specialty='null'}
复制代码
对比结果能够发现:
serialVersionUID 表面意思就是序列化版本号ID,其实每个实现Serializable接口的类,都有一个表示序列化版本标识符的静态变量,或者默认等于1L,或者等于对象的哈希码。
private static final long serialVersionUID = -6384871967268653799L;
复制代码
serialVersionUID有什么用?
JAVA序列化的机制是经过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,若是相同,反序列化成功,若是不相同,就抛出InvalidClassException异常。
接下来,咱们来验证一下吧,修改一下Student类,再反序列化操做
Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student;
local class incompatible: stream classdesc serialVersionUID = 3096644667492403394,
local class serialVersionUID = 4429793331949928814
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
at com.example.demo.Test.main(Test.java:20)
复制代码
从日志堆栈异常信息能够看到,文件流中的class和当前类路径中的class不一样了,它们的serialVersionUID不相同,因此反序列化抛出InvalidClassException异常。那么,若是确实须要修改Student类,又想反序列化成功,怎么办呢?能够手动指定serialVersionUID的值,通常能够设置为1L或者,或者让咱们的编辑器IDE生成
private static final long serialVersionUID = -6564022808907262054L;
复制代码
实际上,阿里开发手册,强制要求序列化类新增属性时,不能修改serialVersionUID字段~
给Student类添加一个Teacher类型的成员变量,其中Teacher是没有实现序列化接口的
public class Student implements Serializable {
private Integer age;
private String name;
private Teacher teacher;
...
}
//Teacher 没有实现
public class Teacher {
......
}
复制代码
序列化运行,就报NotSerializableException异常啦
Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.example.demo.Test.main(Test.java:16)
复制代码
其实这个能够在上小节的底层源码分析找到答案,一个对象序列化过程,会循环调用它的Object类型字段,递归调用序列化的,也就是说,序列化Student类的时候,会对Teacher类进行序列化,可是对Teacher没有实现序列化接口,所以抛出NotSerializableException异常。因此若是某个实例化类的成员变量是对象类型,则该对象类型的类必须实现序列化
子类Student实现了Serializable接口,父类User没有实现Serializable接口
//父类实现了Serializable接口
public class Student extends User implements Serializable {
private Integer age;
private String name;
}
//父类没有实现Serializable接口
public class User {
String userId;
}
Student student = new Student();
student.setAge(25);
student.setName("jayWei");
student.setUserId("1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();
//反序列化结果
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student1 = (Student) objectInputStream.readObject();
System.out.println(student1.getUserId());
//output
/**
* null
*/
复制代码
从反序列化结果,能够发现,父类属性值丢失了。所以子类实现了Serializable接口,父类没有实现Serializable接口的话,父类不会被序列化。
本文第六小节能够回答这个问题,如回答Serializable关键字做用,序列化标志啦,源码中,它的做用啦~还有,能够回答writeObject几个核心方法,如直接写入基本类型,获取obj类型数据,循环递归写入,哈哈~
能够用transient关键字修饰,它能够阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,好比int型的值会被设置为 0,对象型初始值会被设置为null。
Externalizable继承了Serializable,给咱们提供 writeExternal() 和 readExternal() 方法, 让咱们能够控制 Java的序列化机制, 不依赖于Java的默认序列化。正确实现 Externalizable 接口能够显著提升应用程序的性能。
能够看回本文第七小节哈,JAVA序列化的机制是经过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,若是相同,反序列化成功,若是不相同,就抛出InvalidClassException异常。
能够的。咱们都知道,对于序列化一个对象需调用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。若是在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。同时,能够声明这些方法为私有方法,以免被继承、重写或重载。
static静态变量和transient 修饰的字段是不会被序列化的。静态(static)成员变量是属于类级别的,而序列化是针对对象的。transient关键字修字段饰,能够阻止该字段被序列化到文件中。