一文看懂Java序列化

 

一文看懂Java序列化

简介

首先我们看一下wiki上面对于序列化的解释。

序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

以最简单的方式来说,序列化就是将内存中的对象变成网络或则磁盘中的文件。而反序列化就是将文件变成内存中的对象。(emm,序列化就是将脑海中的“老婆”变成纸片人?反序列化就是将纸片人变成脑海中的“老婆”?当我没说)如果说的代码中具体一点,序列化就是将对象变成字节,而反序列化就是将字节恢复成对象。

当然,你在一个平台进行序列化,在另外一个平台也可以进行反序列化。

对象的序列化主要有两种用途:
  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(比如说服务器上用户的session对象)
  2. 在网络上传送对象的字节序列。(比如说进行网络通信,消息(可以是文件)肯定要变成二进制序列才能在网络上面进行传输)

OK,既然我们已经了解到什么是(反)序列化了,那么多说无益,让我们来好好的看一看Java是怎么实现的吧。

Java实现

对于Java这把轻机枪来说,既然序列化是一个很重要的部分,那么它肯定自身提供了序列化的方案。

在Java中,只有实现了Serializable和Externalizable接口的类的对象才能够进行序列化。在下面将分别对两者进行介绍。

Serializable

最基本情况

Serializable可以说是最简单的序列化实现方案了。它就是一个接口,里面没有任何的属性和方法。一个类通过implements Serializable标示着这个类是可序列化的。下面将举一个简单的例子:

public class People implements Serializable {
private String name;
private int age; public People(String name, int age) {
this.name = name;
this.age = age;
} @Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

People类显而易见,是可序列化的。那么我们如何来实现可序列化呢?在序列化的过程中,有两个步骤:

  1. 序列化
  • 创建一个ObjectOutputStream输出流。
  • 调用ObjectOutputStream的writeObject函数输出可序列化的对象。
public class Main {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 18);
oos.writeObject(people);
}
}

ObjectOutputStream对象中需要一个输出流,这里使用的是文件输出流(也可以是用其他输出流,例如System.out,输出到控制台)。然后我们通过调用writeObject就可以讲people对象写入到“object.txt”了。

  1. 反序列化
    我们重新编辑People的构造方法,在里面添加一个输出来查看反序列化是否会进行调用构造函数。
public class People implements Serializable {
private String name;
private int age; public People(String name, int age) {
System.out.println("是否调用序列化?");
this.name = name;
this.age = age;
} @Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

反序列化和序列化一样,也分为2个步骤:

  • 创建一个ObjectInputStream输入流
  • 调用ObjectInputStream中的readObject函数得到序列化的对象
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people = (People) ois.readObject();
System.out.println(people);
}
}

下面是程序运行之后的控制台的图片。

一文看懂Java序列化

可以很明显的看见,反序列化的时候,并没有调用People的构造方法。反序列化的对象是由JVM自己生成的对象,而不是通过构造方法生成。

Ok,通过上面我们简单的学会了序列化的使用,那么,我们会有一个问题,一个对象在序列化的过程中,有哪一些属性是可是序列化的,哪一些是不可序列化的呢?

通过查看源代码,我们可以知道:

一文看懂Java序列化

对象的类,签名和非transient和非static变量会写入到类中。

类的成员为引用

看到很多博客都是这样说的:

如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。

其实这样说不是很准确,因为即使是String类型,里面也实现了Serializable这个接口。

一文看懂Java序列化

我们新建一个Man类,但是它并没有实现Serializable方法。

public class Man{
private String sex; public Man(String sex) {
this.sex = sex;
} @Override
public String toString() {
return "Man{" +
"sex='" + sex + '\'' +
'}';
}
}

然后在People类中进行引用。

public class People implements Serializable {
private String name;
private int age;
private Man man; @Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
", man=" + man +
'}';
} public People(String name, int age, Man man) {
this.name = name;
this.age = age;
this.man = man;
}
}

如果我们进行序列化,会发生以下错误:

java.io.NotSerializableException: People
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.main(Main.java:41)

因为Man是不可序列化的,也就导致了People类是不可序列化的。

同一对象多次序列化

大家看一下下面的这段代码:

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); People people = new People("name", 11);
oos.writeObject(people);
oos.writeObject(people); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1 == people2);
}
}

你们觉得会输出啥?

最后的结果会输出true

然后大家再看一段代码,与上面代码不同的是,People在第二次writeObject的时候,对name进行了重新赋值操作。

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); People people = new People("name", 11);
oos.writeObject(people);
people.setName("hello");
oos.writeObject(people); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1 == people2);
}
}

结果会输出啥?

结果还是:true,同时在people1和people2对象中,name都为“name”,而不是为“hello”。

一文看懂Java序列化
 

why??为什么会这样?

在默认情况下,对于一个实例的多个引用,为了节省空间,只会写入一次。而当写入多次时,只会在后面追加几个字节而已(代表某个实例的引用)。

但是我们如果向在后面追加实例而不是引用那么我们应该怎么做?使用rest或writeUnshared即可。

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); People people = new People("name", 11);
oos.writeObject(people);
people.setName("hello");
oos.reset();
oos.writeObject(people); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1);
System.out.println(people2);
System.out.println(people1 == people2);
}
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); People people = new People("name", 11);
oos.writeObject(people);
people.setName("hello");
oos.writeUnshared(people); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
People people2 = (People) ois.readObject();
System.out.println(people1);
System.out.println(people2);
System.out.println(people1 == people2);
}
}

子父类引用序列化

子类和父类有两种情况:

  • 子类没有序列化,父类进行了序列化
  • 子类进行序列化,父类没有进行序列化

emm,第一种情况不需要考虑,肯定不会出错。让我们来看一看第二种情况会怎么样!!

父类Man类

public class Man {
private String sex; public Man(String sex) {
this.sex = sex;
} @Override
public String toString() {
return "Man{" +
"sex='" + sex + '\'' +
'}';
}
}

子类People类:

public class People extends Man implements Serializable {

    private String name;
private int age; public People(String name, int age, String sex) {
super(sex);
this.name = name;
this.age = age;
} @Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
"} " + super.toString();
}
}

如果这个时候,我们对People进行序列化会怎么样呢?会报错!!

Exception in thread "main" java.io.InvalidClassException: People; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2098)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:465)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:423)
at Main.main(Main.java:38)

如何解决,我们可以在Man中,添加一个无参构造器即可。这是因为当父类不可序列化的时候,需要调用默认无参构造器初始化属性的值。

可自定义的可序列化

我们会有一个疑问,序列化可以将对象保存在磁盘或者网络中,but,我们如何能够保证这个序列化的文件的不会被被人查看到里面的内容。假如我们在进行序列化的时候就像这些属性进行加密不就Ok了吗?(这个仅仅是举一个例子)

可自定义的可序列化有两种情况:

  • 某些变量不进行序列化
  • 在序列化的时候改变某些变量

在上面我们知道transientstatic的变量不会进行序列化,因此我们可以使用transient来标记某一个变量来限制它的序列化。

在第二中情况我们可以通过重写writeObject与readObject方法来选择对属性的操作。(还有writeReplacereadResolve

在下面的代码中,通过transient来限制name写入,通过writeObject和readObject来对写入的age进行修改。

public class People implements Serializable {

    transient private String name;
private int age; public People(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(age + 1);
} private void readObject(ObjectInputStream in) throws IOException {
this.age = in.readInt() -1 ;
}
}

至于main函数怎么调用?还是正常的调用:

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
People people = new People("name", 11);
oos.writeObject(people);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
People people1 = (People) ois.readObject();
}
}

Externalizable:强制自定义序列化

这个,emm,“强制”两个字都懂吧。让我们来看一看这个接口的源代码:

public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

简单点来说,就是类通过implements这个接口,实现这两个方法来进行序列化的自定义。

public class People implements Externalizable {

    private String name;
private int age; public People(String name, int age) {
this.name = name;
this.age = age;
}
// 注意必须要一个默认的构造方法
public People() {
} public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(this.age+1);
} public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.age = in.readInt() - 1;
} }

两者之间的差异

方案 实现Serializable接口 实现Externalizable接口
方式 系统默认决定储存信息 程序员决定存储哪些信息
方法 使用简单,implements即可 必须实现接口内的两个方法
性能 性能略差 性能略好

序列化版本号serialVersionUID

我相信很多人都看到过serialVersionUID,随便打开一个类(这里是String类),我么可以看到:

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;

使用来自JDK 1.0.2 的serialVersionUID用来保持连贯性

这个serialVersionUID的作用很简单,就是代表一个版本。当进行反序列化的时候,如果class的版本号与序列化的时候不同,则会出现InvalidClassException异常。

版本好可以只有指定,但是有一个点要值得注意,JVM会根据类的信息自动算出一个版本号,如果你更改了类(比如说添加/修改了属性或者方法),则计算出来的版本号就发生了改变。这样也就代表这你无法反序列化你以前的东西。

什么情况下需要修改serialVersionUID呢?分三种情况。

  • 修改了方法,这个当然版本好不需要改变
  • 修改了静态变量或者transient关键之修饰的变量,同样不需要修改。
  • 新增了变量或者删除了变量也不需要修改。如果是新增了变量,则进行反序列化的时候会给新增的变量赋一个默认值。如果是修改了变量,则进行反序列化的时候无需理会被删除的值。

讲完了讲完了,序列化实际上还是挺简单。不过需要注意使用的时候遇到的坑。~~

上一篇:CSS 类也可以与伪元素配合特殊效果


下一篇:一文看懂Java序列化之serialVersionUID