Java基础——对象的序列化
什么是对象的序列化(Serialization)
“序列化”是一种把对象的状态转化成字节流的机制,“反序列”是其相反的过程,把序列化成的字节流用来在内存中重新创建一个实际的Java对象。这个机制被用来“持久化”对象。通过对象序列化,可以方便的实现对象的持久化储存以及在网络上的传输。大致的过程如下图所示:
对象被转换成“字节流”后可以存入文件,内存,亦或者是数据库内进行持久化保存。然后通过“反序列化”可以把“字节流”转换成实际的Java对象。
- 对象的序列化是与平台无关的,因此在一个平台上被“序列化”的对象可以很容易的在另一个不相同的平台上给“反序列化”出来。
序列化的通俗解释
假设我有一条叫rex的小狗,我想把它寄养给远方亲戚。
那么问题来了,小狗是活的,是会呼吸的一种动物,我怎样通过电话线来传达一个对象呢?我总不能直接把小狗塞进电话话筒里吧,所以,我不得不把这个小狗转换成另一种形式来代表它,从而可以通过电话传输。换句话说,我们就可以对小狗进行序列化操作,并且通过电话线把序列化的信息给发送过去:
这就是一个完美的小狗代表,一个被序列化的小狗。
序列化意味着把我的小狗对象转化成了另一种代表形式——一个JSON对象。这个对象可以通过电话线以一串0,1,0,1......的二进制串来传播。电话线另一端的远方亲戚可以通过把这一串0,1,0,1的二进制串翻译成一个JSON对象——也就是我的小狗。因此,我的小狗的信息就完美的通过电话线使用二进制符号传播出去了。
如何实现序列化
为了使一个Java对象能够被“序列化”,我们必须让这个对象实现java.io.Serializable接口。
我们打开Serializable.java这个文件,看一看这个接口中定义了哪些内容:
public interface Serializable {
}
可以发现,在此接口中没有定义任何的方法,所以此接口是一个标识接口。表示一个类具备了“可序列化”的能力。
import java.io.Serializable;
public class Person implements Serializable {
private String name;
public Person(String name) {
this.name = name;
}
}
上面就是一个实现了Serializable接口的Person类对象,这个对象现在就已经具备了“可序列化”的能力。所以此类的对象是可以经过“二进制数据流”进行传输的。而如果想要完成对象的输入或者输出,还必须依靠对象输出流(ObjectOutputStream)和对象输入流(ObjectInputStream)。
使用:“对象输出流”输出序列化对象的步骤有时候也称作“序列化”,而使用“对象输入流”读入对象的过程也称作“反序列化”。
这个ObjectOutputStream(对象输出流)类包含一个writeObject()方法用来“序列化”一个对象:
而ObjectInputStream(对象输入流)类包含一个readObject()方法用来“反序列化”一个对象:
ObjectOutputStream流
我们首先需要写一个被实例化的对象,假设为Person类的对象需要被序列化。
Person类声明如下:
public class Person implements Serializable {
private static final long serialVersionUID = 7592930394427200495L;
private String name;
private int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "姓名:"+name+"年龄:"+age;
}
}
接下来使用“对象输出流”来序列化一个Java对象:
public class SerDemo {
public static void main(String[] args) throws IOException {
//实例化一个需要序列化的Person对象
Person jack = new Person("jack", 12);
//生成一个文件对象,文件不存在将自动创建文件
File f = new File("F:" + File.separator + "serTest.txt");
//构造一个对象输出流oos
ObjectOutputStream oos = null;
//构造一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream(f);
//构造对象输出流
oos = new ObjectOutputStream(fileOutputStream);
//序列化一个对象到文件变成二进制内容
oos.writeObject(jack);
oos.close();
}
}
我们打开程序序列化生成的文件serTest.txt看看里面的内容是什么:
通过以上代码可将一个Java对象以“字节流”的方式保存到文件中,因为保存的内容全部是二进制的,所以出现了上面这种乱码的情况。保存的文件本身是不可以直接修改的,因为会破坏其保存的格式。
需要注意的是,对象的“序列化”并没有保存对象所有的内容,而仅仅保留了对象的属性,没有保留对象的方法,之所以这么做的原因是同一类对象中每个对象都具备相同的方法,但是每个对象的属性却不一定相同,所以我们序列化时只要保存对象的属性就可以了。
ObjectInputStream流
使用ObjectInputStream可以直接把被序列化的对象给反序列化出来。
下面使用ObjectInputStream流把我们刚刚序列化的Person对象给反序列化出来,使其成为一个实际的Java对象。
public class DeSerDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//生成一个文件对象
File f = new File("F:" + File.separator + "serTest.txt");
//构建对象输入流对象
ObjectInputStream oos = null;
//构建文件输入流对象
FileInputStream fileOutputStream = new FileInputStream(f);
oos = new ObjectInputStream(fileOutputStream);
//读取序列化
System.out.println(Person);
}
}
控制台打印如下:
我们发现,文件中的二进制内容被“反序列”化成为了一个真实的Java对象,并且这个对象的属性和方法都完好无损。
既然序列化是如此方便的一个功能,那么我们可不可以让所有的类都去实现Serializable序列化接口呢?
答案当然是不可以的,因为在现在的JDK版本中,Serializable接口中都没有定义任何的方法,所以如果所有的类都实现这个接口在语法上当然是没有问题的,但是,如果未来的JDK版本中修改了此接口并添加了许多方法的话,那么以往的系统中的所有的类都必须修改了,这样会非常的麻烦,所以我们只在需要被序列化的对象的类实现Serializable接口。
对象序列化和反序列化的版本兼容问题
在对象进行序列化或者反序列化的操作时,要考虑JDK的版本问题。如果序列化时用的Class版本与反序列化时用的Class版本不一致的话就有可能造成异常。所以在序列化中引入了一个serialVersionUID的常量。
可以通过这个常量来验证序列化版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地的相应的serialVersionUID进行一个比较,如果相同就认为是一致的,可以进行反序列化的操作,否则就会发生因为序列化版本不一致的异常。
当实现序列化的类没有显示的定义一个serialVersionUID时,Java序列化机制会在编译时自动生成一个此版本de的serialVersionUID.当然,也可以自己显示的定义一个UID,类型为long的变量,只要不修改过这个变量值的序列化实体都可以相互序列化和反序列化。
序列化的优点
- 可以“持久化”保存一个对象。
- 可以通过网络方便的传送一个对象。
序列化的几个注意点
- 如果父类已经实现了Serializable序列化接口的话,其子类就不用再实现这个接口了,但是反过来就不成立了。
- 只有非静态的数据成员才会通过序列化操作被序列化。
- 静态(Static)数据成员和被标记了transient的数据成员在序列化的过程中将不会被序列化,因此,如果你不想要保存一个非静态的数据成员你就可以给标记上transient。
- 当一个对象被反序列化时,这个对象的构造函数将不会在被调用的。
- 需要实现序列化的对象如果内部引用了一个没有实现序列化接口的对象,在其序列化过程中将会发生错误,如下面这个例子:
如何只序列化对象的部分属性
你可以认为“序列化”就是把一个实际的Java对象转换成一连串的字节流的过程。如果您想要通过网络把一个JVM中的对象给传输到另一个JVM中的话,序列化是非常有用的。
如果你想阻止对象中的一些属性在序列化的过程中被序列化,可以在这些属性面前标记transient关键词,标注了这个关键词的属性将不会被序列化。下面是一个具体的例子:
import java.io.*;
import java.util.List;
/**
* @author Administrator
* @description
* @date Created in 2018/5/23 21:31
*/
public class TransientDemo implements Serializable {
// 下面的两个属性将会被序列化
private String aString = "The value of that string";
private int someInteger = 0;
// 但是这个属性不会被序列化,因为它被transient标记了
private transient List<File> unInterestingLongLongList;
// 主方法测试
public static void main(String[] args) throws IOException {
// 创建一个包含了默认值的对象
TransientDemo instance = new TransientDemo();
// The "ObjectOutputStream" class has the default
// definition to serialize an object.
ObjectOutputStream oos = new ObjectOutputStream(
// By using "FileOutputStream" we will
// Write it to a File in the file system
// It could have been a Socket to another
// machine, a database, an in memory array, etc.
new FileOutputStream(new File("o.ser")));
// do the magic
oos.writeObject(instance);
// close the writing.
oos.close();
}
}
当我们运行后,序列化文件o.ser已经被创建了,其中是被序列化对象的信息,让我们来看看。
接下来我们改变对象中someInteger的值看看新生成的序列化文件有什么改变:
仔细观察,我们会发现其中标红的二进制数据改变了,这更加的证明了序列化的概念,序列化”是一种把对象的状态转化成字节流的机制,“反序列”是其相反的过程,把序列化成的字节流用来在内存中重新创建一个实际的Java对象。这个机制被用来“持久化”对象。通过对象序列化,可以方便的实现对象的持久化储存以及在网络上的传输。
序列化文件的几种格式
现在让我们来看一看序列化文件中的“字节流”是如何被解释以及操作从而被还原成原始的对象状态的。这儿有几种方法能够实现它。
- XML:把对象序列化成XML格式的文件,然后就可以通过网络传输这个对象或者把它储存进文件或数据库里了。我们也可以从中取出它并且反序列化成原来的对象状态。在JAVA中我们使用 JAXB库。
- JSON:同样可以把对象序列化成JSON格式从而持久化保存对象。可以使用GSON库来实现。
- 我们也可以使用面向对象语言自身提供的序列化格式来持久化储存对象。比如在JAVA中可以通过实现序列化接口来序列化一个对象。
参考资料
1.《Java开发实战经典》 李兴华著 清华大学出版社
2.https://www.geeksforgeeks.org/serialization-in-java 作者:Mehak Narang and Shubham Juneja 翻译:刘扬俊
博客文章版权说明
第一条 本博客文章仅代表作者本人的观点,不保证文章等内容的有效性。
第二条 本博客部分内容转载于合作站点或摘录于部分书籍,但都会注明作/译者和原出处。如有不妥之处,敬请指出。
第三条 在征得本博客作者同意的情况下,本博客的作品允许非盈利性引用,并请注明出处:“作者:____转载自____”字样,以尊重作者的劳动成果。版权归原作/译者所有。未经允许,严禁转载。
第四条 对非法转载者,“扬俊的小屋”和作/译者保留采用法律手段追究的权利。
第五条 本博客之声明以及其修改权、更新权及最终解释权均属“扬俊的小屋”。
第六条 以上声明的解释权归“扬俊的小屋”所有。