概念
说到Java,万物皆对象。对象,是一个比较抽象的概念,他就是类存活在内存中的一个实例,有状态和行为,一旦JVM停止运行,对象的状态也会随之丢失。那么如何将这个对象当前状态进行一个记录,使其可以进行存储和传输呢?这就要用到序列化了。
-
序列化(Serialization)
把对象转换为字节序列的过程称为对象的序列化,把对象的状态保持下来,写入到磁盘或者其他介质中。在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。 -
反序列化(Deserialize)
把字节序列恢复为对象的过程称为对象的反序列化,把已存在在磁盘或者其他介质中的对象,读取到内存中,以便后续操作。
Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。
实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream 和 ObjectOutputStream 类、完全保真的元数据以及程序员愿意用 Serializable 标识接口标记他们的类,从而 “参与” 这个过程。
Android序列化
Android 序列化有两种方式,实现 Serializable 或 Parcelable。今天先搞懂 Java 中自带的序列化方式 Serializable。
Serializable 是 java.io 包中定义的、用于实现 Java 类的序列化操作而提供的一个语义级别的空接口,代码里面除了注释几乎啥也没有,仅仅是个接口:
package java.io;
// Android-added: Notes about serialVersionUID, using serialization judiciously, JSON.
/**
* // 此处省略150行注释...
*
* @author unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Externalizable
* @since JDK1.1
*/
public interface Serializable {
}
既然啥都没有,实现序列化和反序列化为什么要实现 Serializable 接口?
在Java中实现了 Serializable 接口后,JVM 会在底层帮我们实现序列化和反序列化,如果我们不实现Serializable接口,那自己去写一套序列化和反序列化代码也行,至于具体怎么写,Google一下你就知道了。
只要我们实现 Serializable 接口,那么这个类就可以被 ObjectOutputStream 转换为字节流,也就是进行了序列化。那么就来走一把,创建个简单的学生对象,有年龄、姓名和地址等属性。
public class Student implements Serializable {
//private static final long serialVersionUID = 1L;
private static String FLAG = "9527";// 静态属性
private int age;
private String name;
private String address;
transient private String car;// 忽略序列化
//private String addTip;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCar() {
return car;
}
public void setCar(String car) {
this.car = car;
}
/*public String getAddTip() {
return addTip;
}
public void setAddTip(String addTip) {
this.addTip = addTip;
}*/
@Override
public String toString() {
return "[Student:" +
"age='" + age + '\'' +
",name='" + name + '\'' +
",address='" + address + '\'' +
",car='" + car + '\'' +
",FLAG='" + FLAG + '\'' +
']';
}
}
再来一个测试类:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializableTest {
public static void main(String[] args) throws Exception {
// 初始化一个对象实例
Student student = new Student();
student.setAge(45);
student.setAddress("suzhoujie");
student.setName("xiaoming");
// 序列化
serializeStudent(student);
// 反序列化
Student dStudent = deserializeStudent();
System.out.println(dStudent.toString());
}
/**
* 序列化
*
* @param student 对象
* @throws Exception IO异常
*/
private static void serializeStudent(Student student) throws Exception {
// ObjectOutputStream 对象输出流,将 student 对象存储到D盘的 student.txt 文件中,完成对 student 对象的序列化操作
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/student.txt")));
oos.writeObject(student);// 对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
System.out.println("Student 对象序列化成功!");
oos.close();
}
/**
* 反序列化
*
* @return 对象
* @throws Exception IO异常
*/
private static Student deserializeStudent() throws Exception {
// ObjectInputStream 代表对象输入流,
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/student.txt")));
Student student = (Student) ois.readObject();// 从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
System.out.println("Student 对象反序列化成功!");
return student;
}
}
运行 main() 方法后在控制台能看到序列化和发序列化,都没啥问题:
Student 对象序列化成功!
Student 对象反序列化成功!
[Student:age='45',name='xiaoming',address='suzhoujie',car='null',FLAG='9527']
Process finished with exit code 0
在D盘生成了一个文件student.txt,虽然后缀定义为txt,其实这只是个字节流文件,记事本打开都是乱码格式的

可能也能看到一些能识别的东西,但是已经是乱掉了。
注意到日志 transient 修饰的 car字段被抹掉了,避免了学生的攀比风气, transient 这个修饰符就可以抹去一些不需要或者不必要参与序列化的字段。
这个静态变量 FLAG 也被序列化啦?no no no……
为了验证静态变量是否参与,我们修改一下测试文件
public static void main(String[] args) throws Exception {
// 初始化一个对象实例
Student student = new Student();
student.setAge(45);
student.setAddress("suzhoujie");
student.setName("xiaoming");
// 序列化
serializeStudent(student);
// // 反序列化
// Student dStudent = deserializeStudent();
// System.out.println(dStudent.toString());
}
先屏蔽掉反序列化,运行:
Student 对象序列化成功!
Process finished with exit code 0
序列化没问题,修改 FLAG 的值为666,再屏蔽掉序列化,只执行反序列化。
public static void main(String[] args) throws Exception {
// 初始化一个对象实例
// Student student = new Student();
// student.setAge(45);
// student.setAddress("suzhoujie");
// student.setName("xiaoming");
//
// // 序列化
// serializeStudent(student);
// 反序列化
Student dStudent = deserializeStudent();
System.out.println(dStudent.toString());
}
Student 对象反序列化成功!
[Student:age='45',name='xiaoming',address='suzhoujie',car='null',FLAG='666']
Process finished with exit code 0
FLAG 的值变成了修改后的666,刚刚序列化的9527没有读出来。而是刚刚修改的666,如果可以的话应该是覆盖这个666,是9527才对。序列化是针对对象而言的,静态成员变量属于类不属于对象,而static属性优先于对象存在,随着类的加载而加载,所以,这个静态 static 的属性不参与序列化。
实现 Serializable 接口就算了, 为什么还要显示指定 serialVersionUID 的值?
Student 类中还有个 serialVersionUID 属性被注释掉了,接下来聊聊这个。
先看效果,还是这个类对象,只执行序列化,把对象存到本地去;再打开注释掉的 addTip 属性,只执行反序列化方法,抛锚了:
Exception in thread "main" java.io.InvalidClassException: com.stock.messenger.entity.Student; local class incompatible: stream classdesc serialVersionUID = 5110753772711222189, local class serialVersionUID = -6906862297427533202
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.stock.messenger.entity.SerializableTest.deserializeStudent(SerializableTest.java:49)
at com.stock.messenger.entity.SerializableTest.main(SerializableTest.java:22)
返回一个 InvalidClassException 的异常,并告知了 serialVersionUID 不匹配导致。原来坑在这里!
在实际开发中,不显式指定 serialVersionUID 的情况会导致什么问题? 如果我们的类写完后不再修改,那当然不会有问题,但这在实际开发中是不可能的,我们的类会不断迭代,新增、修改或删减属性、方法等,一旦类被修改了,那旧对象反序列化就会报错。也不难理解,因为开始的对象里面是没有明确的给这个字段赋值,但是,Java 会自动的给赋值的,这个值跟这个对象的属性相关计算出来的。保存的时候,也就是序列化的时候,那时候还没有这个 addTip 属性呢。序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。所以,如果可序列化类未显式声明,则序列化运行时将基于该类的各个方面计算该类的默认值,然后与属性一起序列化,再进行持久化或网络传输。
不过,强烈建议所有可序列化类都显式声明该值,原因是计算默认的该值对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,自动生成这个值,在反序列化时,JVM会再根据属性自动生成一个新值,自动生成的这个值是不同的,然后将这个新值与序列化时生成的旧值进行比较,如果相同则反序列化成功;如果接收者加载的该对象的类的值与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException,就抛异常啦。因此,为保证该值跨不同 Java 编译器实现的一致性,序列化类必须声明一个明确的值,这样在反序列化时新旧值就一致了。还强烈建议使用 private 修饰符显式声明(该字段必须是静态static、不可修改final 的 long 型字段),原因是这种声明仅应用于直接声明类该字段作为继承成员没有用处。所以在实际开发中,我们都会显式指定一个serialVersionUID,值是多少无所谓,只要不变就行。
如果开始时候把 serialVersionUID = 1L;赋值,那再执行这个流程就不会有问题了,只是给这个 addTip 字段赋值为 null。
Student 对象反序列化成功!
[Student:age='45',name='xiaoming',address='suzhoujie',car='null',addTip='null',car='null',FLAG='666']
Process finished with exit code 0
通俗来说,这个就是为了对象升级做判断用的功能,在实现这个 Serializable 接口的时候,一定要记得给这个 serialVersionUID 赋值。你可以不用自己去赋值,Java 会给你赋值一长串数字,但是,这个就会出现上面的 bug,很不安全,所以,还得自己手动的来,可以简单的赋值个 1L,这就可以。
是不是有人会问,serialVersionUID 也被 static 修饰,为什么也会被序列化?其实这个属性并没有被序列化,JVM 在序列化对象时会自动生成一个默认值,然后将我们显式指定的该属性值赋给自动生成的 serialVersionUID。
当属性是对象的时候,如果这个对象,没实现序列化接口也会出问题,给学生新加一个工具对象 Tool
public class Tool {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// Student 新增Tool对象
private Tool tool;
public Tool getTool() {
return tool;
}
public void setTool(Tool tool) {
this.tool = tool;
}
//...
执行序列化就会看到 NotSerializableException 错误:
Exception in thread "main" java.io.NotSerializableException: com.stock.messenger.entity.Tool
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:441)
at com.stock.messenger.entity.Student.writeObject(Student.java:83)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1140)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
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.stock.messenger.entity.SerializableTest.serializeStudent(SerializableTest.java:38)
at com.stock.messenger.entity.SerializableTest.main(SerializableTest.java:22)
将 Tool 对象实现 Serializable,又可正常使用
public class Tool implements Serializable {
//...
下面是摘自 JDK API 文档里面关于接口 Serializable 的描述
类通过实现
java.io.Serializable接口以启用其序列化功能。
未实现此接口的类将无法使其任何状态序列化或反序列化。
可序列化类的所有子类型本身都是可序列化的,因为实现接口也是间接的等同于继承。
序列化接口没有方法或字段,仅用于标识可序列化的语义。
模糊化序列化数据
对象某些属性是敏感的信息,我们可以考虑在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)
让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。
这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。
幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject() 方法来做到这一点。
为了 “hook” 序列化过程,我们将在 Student 上实现一个 writeObject() 方法;为了 “hook” 反序列化过程,我们将在同一个类上实现一个 readObject() 方法。重要的是这两个方法的细节要正确。
Student 中复写下面两个方法:
private void writeObject(ObjectOutputStream stream) throws IOException {
// 加密,"Encrypt"/obscure the sensitive data
age = age << 2;
stream.defaultWriteObject();
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
// 解密,"Decrypt"/de-obscure the sensitive data
age = age >> 2;
}
运行结果完全没有问题,也能正确的读取出数据。
如果需要查看被模糊化的数据,总是可以查看序列化数据流/文件。而且,由于该格式被完全文档化,即使不能访问类本身,也仍可以读取序列化流中的内容。
更多Tips可以查看:关于 Java 对象序列化您不知道的 5 件事

本文介绍了Java序列化的基本概念,包括序列化和反序列化的定义,以及它们在对象状态保存和传输中的作用。文章重点讲解了实现Serializable接口进行序列化的原理,提到通过实现该接口,JVM会自动处理序列化和反序列化过程。同时,文章讨论了transient关键字的作用,即忽略某些字段的序列化。此外,还提到了序列化版本ID(serialVersionUID)的重要性,防止反序列化时因类结构改变导致的问题。最后,文章提到了序列化数据的安全性问题,以及如何通过重写writeObject和readObject方法来保护或模糊化敏感数据。
954

被折叠的 条评论
为什么被折叠?



