1.背景
某天,我在写代码定义 bean 的时候,顺手写了个 public class User implements Serializable
,旁边的小哥哥看到了问我:你为什么要实现 Serializable
接口?你哪里用到它了吗?不实现这个接口可以吗?
emmm,皱眉沉思一下,好像也可以?
好吧,那先来了解一下 Serializable
接口涉及到的相关概念。
2.序列化协议+序列化和反序列化
- 序列化是指:将数据结构或对象转换成特定的格式,使其可以在网络中传输,或可存储在内存/文件中。
序列化后的数据必须是可保持或可传输的数据格式,例如:二进制串/字节流、XML、JSON等。
- 反序列化:是序列化的逆过程,将对象从序列化数据中还原出来。
自问自答
- 问:序列化的目的是什么?
- 答:方便的进行数据的交换和传输工作。
3.JDK类库中的序列化API
Java本身提供了对数据/对象序列化的支持。
-
输入输出流
-
ObjectOutputStream
对象输出流,其writeObject(Object obj)
方法可对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。 -
ObjectInputStream
对象输入流,其readObject()
方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
-
-
接口
- 只有实现了
Serializable
和Externalizable
接口的类的对象才能被序列化。 -
Externalizable
接口继承自Serializable
接口。 - 实现
Externalizable
接口的类完全由自身来控制序列化的行为;而仅实现Serializable
接口的类可以采用默认的序列化方式 。
- 只有实现了
-
对象序列化步骤:
- 创建一个对象输出流。
- 通过对象输出流的
writeObject()
方法写对象。
-
对象反序列化步骤:
- 创建一个对象输入流。
- 通过对象输入流的
readObject()
方法读取对象。
3.1 对象序列化到文件
// User.java
package com.ann.javas.javacores.serialization.demo1;
import java.io.Serializable;
public class User implements Serializable{
private static String HH="我是静态变量,我不会被序列化";
private int userId;
private String userName;
private String address;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public static String getHH() {
return HH;
}
public static void setHH(String HH) {
User.HH = HH;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", address='" + address + '\'' +
'}';
}
}
// Client.java
package com.ann.javas.javacores.serialization.demo1;
import java.io.*;
public class Client {
public static void main(String[] args) throws Exception {
toFile();
fromFile();
}
// Object -> 文件
public static void toFile() throws Exception {
User user = new User();
user.setUserId(1223);
user.setUserName("令习习习");
user.setAddress("北京");
System.out.println("对象:"+user.toString());
System.out.println("对象中的静态变量:"+user.getHH());
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("./out.txt")));
oo.writeObject(user);
System.out.println("序列化成功");
oo.close();
}
// 文件 -> Object
public static void fromFile() throws Exception{
User tmp = new User();
tmp.setHH("我是静态变量,我的值是存在JVM静态存储区的,不是反序列化来的");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./out.txt")));
System.out.println("反序列化成功");
User user = (User) ois.readObject();
System.out.println("对象:"+user.toString());
System.out.println("对象中的静态变量:"+user.getHH());
ois.close();
}
}
运行结果:
对象:User{userId=1223, userName='令习习习', address='北京'}
对象中的静态变量:我是静态变量,我不会被序列化
序列化成功
反序列化成功
对象:User{userId=1223, userName='令习习习', address='北京'}
对象中的静态变量:我是静态变量,我的值是存在JVM静态存储区的,不是反序列化来的
这是一个简单的序列化和反序列化例子,创建一个
User
实例,将其全部数据序列化到文件;然后再从文件读取数据反序列化为对象。需要特别关注的是:对象序列化保存的是对象的"状态",即它的成员变量。因此,对象序列化不会关注类中的静态变量。
3.2 隐藏指定字段
在某些场景下,你希望某些字段不要被序列化,此时可以使用 transient
关键字来进行排除。
-
transient
关键字只修饰变量,不修饰方法和类。 - 被
transient
关键字修饰的变量不再能被序列化,自然也不会被反序列化回来。
// User.java
package com.ann.javas.javacores.serialization.demo2;
import java.io.Serializable;
public class User implements Serializable{
private int userId;
private String userName;
private transient String address;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", address='" + address + '\'' +
'}';
}
}
//Client.java
package com.ann.javas.javacores.serialization.demo2;
import java.io.*;
public class Client {
public static void main(String[] args) throws Exception {
toFile();
fromFile();
}
// Object -> 文件
public static void toFile() throws Exception {
User user = new User();
user.setUserId(1223);
user.setUserName("令习习习");
user.setAddress("北京");
System.out.println("对象:"+user.toString());
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("./out.txt")));
oo.writeObject(user);
System.out.println("序列化成功");
oo.close();
}
// 文件 -> Object
public static void fromFile() throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./out.txt")));
System.out.println("反序列化成功");
User user = (User) ois.readObject();
System.out.println("对象:"+user.toString());
ois.close();
}
}
运行结果:
对象:User{userId=1223, userName='令习习习', address='北京'}
序列化成功
反序列化成功
对象:User{userId=1223, userName='令习习习', address='null'}
这里使用 transient
修饰了 User
的 address
变量,因此address不会被序列化,也不会被反序列化。
自问自答
- 问:使用
transient
修饰的变量,就一定不会被序列化了吗?- 答:不一定,要取决于你的程序是怎么写的。
3.3 Serializable 的 readObject 和 writeObject
// User.java
package com.ann.javas.javacores.serialization.demo3;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class User implements Serializable{
private int userId;
private String userName;
private transient String address;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", address='" + address + '\'' +
'}';
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
address = (String)in.readObject();
}
}
// Client.java
package com.ann.javas.javacores.serialization.demo3;
import java.io.*;
public class Client {
public static void main(String[] args) throws Exception {
toFile();
fromFile();
}
// Object -> 文件
public static void toFile() throws Exception {
User user = new User();
user.setUserId(1223);
user.setUserName("令习习习");
user.setAddress("北京");
System.out.println("对象:"+user.toString());
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("./out.txt")));
oo.writeObject(user);
System.out.println("序列化成功");
oo.close();
}
// 文件 -> Object
public static void fromFile() throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./out.txt")));
System.out.println("反序列化成功");
User user = (User) ois.readObject();
System.out.println("对象:"+user.toString());
ois.close();
}
}
运行结果:
对象:User{userId=1223, userName='令习习习', address='北京'}
序列化成功
反序列化成功
对象:User{userId=1223, userName='令习习习', address='北京'}
在这个例子中,User
定义了两个private方法:readObject()
和 writeObject()
。
在 writeObject()
方法中会先调用 ObjectOutputStream
中的 defaultWriteObject()
方法,该方法会执行默认的序列化机制,此时会忽略掉被 transient
修饰的address字段。然后再调用 writeObject()
方法显示地将address字段写入到 ObjectOutputStream
中。
readObject()
的作用则是针对对象的读取,其原理与 writeObject()
方法相同。
3.4 实现 Externalizable 接口
在Java中,对象的序列化可以通过实现两种接口来实现:
- 若实现的是
Serializable
接口,则所有的序列化将会自动进行,如果你希望在此基础之上加点自定义的内容,就可以像上面那样加两个方法就ok了。 - 若实现的是
Externalizable
接口,则没有任何东西可以自动序列化,需要在
writeExternal()
方法中进行手工指定所要序列化的变量,以及如何序列化,这与是否被transient
修饰无关(也就是说,当你不需要java自动为你序列化的时候,transient就失效了);当然readExternal()
也需要做相应的处理。
// User.java
package com.ann.javas.javacores.serialization.demo3;
import java.io.*;
public class User implements Externalizable{
private int userId;
private String userName;
private transient String address;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", address='" + address + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(userId + 122);
out.writeObject(userName);
out.writeObject(address);
System.out.println("writeExternal:我没有存原文哦");
out.flush();
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
userId = in.readInt();
userName = (String)in.readObject();
address = (String)in.readObject();
}
}
// Client.java
package com.ann.javas.javacores.serialization.demo3;
import java.io.*;
public class Client {
public static void main(String[] args) throws Exception {
toFile();
fromFile();
}
// Object -> 文件
public static void toFile() throws Exception {
User user = new User();
user.setUserId(1223);
user.setUserName("令习习习");
user.setAddress("北京");
System.out.println("对象:"+user.toString());
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("./out.txt")));
oo.writeObject(user);
System.out.println("序列化成功");
oo.close();
}
// 文件 -> Object
public static void fromFile() throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./out.txt")));
System.out.println("反序列化成功");
User user = (User) ois.readObject();
System.out.println("对象:"+user.toString());
ois.close();
}
}
运行结果:
对象:User{userId=1223, userName='令习习习', address='北京'}
writeExternal:我没有存原文哦
序列化成功
反序列化成功
对象:User{userId=1345, userName='令习习习', address='北京'}
这里有几个关键单需要说明:
- 实现
Externalizable
接口,一定要自定义序列化方法,如果你把writeExternal()
和readExternal()
这里面的实现都丢掉,就会发现,java真的什么都不会做。 - 如上面所说,当实现了
Externalizable
接口的时候,transient
关键字不再生效。 - 反序列化时,实际上调用了
User
的无参构造函数,因此在自定义序列化方案的时候,请一定要记得提供一个 公共无参构造函数 ,不然就悲剧了。
4.关于 Serializable 和 Externalizable 的总结和附加说明
-
构造器:
-
Serializable
序列化时不会调用默认的构造器; - 而
Externalizable
序列化时会调用默认构造器。
-
-
功能
- 一个对象想要被序列化,那么它的类就要实现
Serializable
接口,这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。 -
Externalizable
是Serializable
接口的子类,有时我们不希望序列化那么多,可以使用这个接口,这个接口的writeExternal()
和readExternal()
方法可以指定序列化哪些属性。
- 一个对象想要被序列化,那么它的类就要实现
-
关键字
- 由于
Externalizable
对象默认不保存对象的任何字段,所以transient
关键字只能伴随Serializable
使用,虽然Externalizable
对象中使用transient
关键字也不报错,但不起任何作用。
- 由于
-
方法
-
Serializable
接口的writeObject()
和readObject()
方法是可选实现,若没有自定义,则使用默认的。 -
Externalizable
接口的writeExternal()
和readExternal()
方法是必选实现,当然你可以在里面什么都不做。
-
自问自答
- 问:
writeObject()
和readObject()
都是private方法,它们是怎么被调用的呢?- 答:很显然,反射。详情可见
ObjectOutputStream
中的writeSerialData
方法,以及ObjectInputStream
中的readSerialData
方法。