什么是序列化和反序列化
序列化: 把对象转换为字节序列的过程。
反序列化:把字节序列恢复为对象的过程。
对象的序列化主要有两种用途
-
把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
-
在网络上传送对象的字节序列。
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
序列化实现的方式
如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。
1.Serializable
Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
- 创建一个用户类
public class User {
private Integer id;
private String username;
private String password;
public User() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
序列化
序列化步骤:
-
步骤一:创建一个ObjectOutputStream输出流;
-
步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象
public static void main(String[] args) throws IOException {
User user = new User();
user.setId(1);
user.setPassword("123456");
user.setUsername("zhangsan");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist\\user.text"));
oos.writeObject(user);
}
反序列化
-
步骤一:创建一个ObjectInputStream输入流;
-
步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。
我们将上面序列化到uer.text的person对象反序列化回来
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\user.text"));
User user = (User) ois.readObject();
System.out.println(user);
}
Serializable接口有何用?
上面在定义User类时,实现了一个Serializable
接口,然而当我们点进Serializable
接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!
public interface Serializable {
}
试想,如果上面在定义Student
类时忘了加implements Serializable
时会发生什么呢?
实验结果是:此时的程序运行会报错,并抛出NotSerializableException
异常:
源码跟踪:ObjectOutputStream
的writeObject0()
方法底层一看,才恍然大悟:
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable
接口的话,在序列化时就会抛出NotSerializableException
异常!
Serializable
接口也仅仅只是做一个标记用!!!
它告诉代码只要是实现了Serializable
接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。
serialVersionUID
号有何用?
private static final long serialVersionUID = 1217369220830889326L;
继续来做一个简单实验,还拿上面的User
类为例,并没有人为在里面显式地声明一个serialVersionUID
字段。
我们首先还是调用上面的serialize()
方法,将一个User
对象序列化到本地磁盘上的user.txt
文件:
public static void main(String[] args) throws IOException {
User user = new User();
user.setId(1);
user.setPassword("123456");
user.setUsername("zhangsan");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist\\user.txt"));
oos.writeObject(user);
oos.close();
}
接下来我们在User类里面动点手脚,比如在里面再增加一个名为sex的字段,表示用户性别:
实现反序列化:
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\user.txt"));
User user = (User) ois.readObject();
System.out.println(user);
ois.close();
}
运行报错:
这地方提示的信息非常明确了:序列化前后的serialVersionUID
号码不兼容!
从这地方最起码可以得出两个重要信息:
- 1、serialVersionUID是序列化前后的唯一标识符
- 2、默认如果没有人为显式定义过
serialVersionUID
,那编译器会为它自动声明一个!
两种特殊情况
- 1、凡是被
static
修饰的字段是不会被序列化的 - 2、凡是被
transient
修饰符修饰的字段也是不会被序列化的
对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static
静态域也是理所应当的。
对于第二点,就需要了解一下transient
修饰符的作用了。
如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码
等),那这时就可以用transient
修饰符来修饰该字段。
比如在之前定义的User
类中,加入一个性别字段,但是不希望序列化到txt
文本,则可以:
这样在序列化User
类对象时,password
字段会设置为默认值null
,这一点可以从反序列化所得到的结果来看出:
序列化的受控和加强
自行编写readObject()
函数,用于对象的反序列化构造,从而提供约束性。
自行编写readObject()
函数,那就可以做很多可控的事情:比如各种判断工作。
还以上面的User
类为例,一般来说用户的年龄的应该在0 ~ 100
之间,我们为了防止用户的的年龄在反序列化时被别人篡改成一个奇葩值,我们可以在用户(User)类中自行编写readObject()
函数用于反序列化的控制:
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
// 调用默认的反序列化函数
objectInputStream.defaultReadObject();
// 手工检查反序列化后年龄的的有效性,若发现有问题,即终止操作!
if( age<0 || age >100 ) {
throw new IllegalArgumentException("年龄只能在0岁到100之间!");
}
}
比如我故意将用户年龄改外200,此时反序列化立马终止并且报错:
为什么自定义的private
的readObject()
方法可以被自动调用,这就需要你跟一下底层源码来一探究竟了
又是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即使是类中定义的private
私有方法,也能被抠出来执行了。
单例模式增强
一个容易被忽略的问题是:可序列化的单例类有可能并不单例!
举个代码小例子就清楚了。
懒汉模式
public class Singleton implements Serializable {
/*构造函数私有化*/
private Singleton(){
}
private static Singleton singleton = null;
//构造实例化方法
public static Singleton getSingleton(){
if (singleton == null){
singleton = new Singleton();
}else {
return singleton;
}
return singleton;
}
}
写一个验证主函数
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist/2.txt"));
Singleton singleton = Singleton.getSingleton();
oos.writeObject(singleton);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\2.txt"));
Singleton singleton1 = (Singleton) ois.readObject();
ois.close();
//运行结果false
System.out.println(singleton==singleton1);
}
运行后我们发现:反序列化后的单例对象和原单例对象并不相等了。
解决办法是:在单例类中手写readResolve()
函数,直接返回单例对象,来规避之:
private Object readResolve() {
return Singleton.singleton;
}
这样一来,当反序列化从流中读取对象时,readResolve()
会被调用,用其中返回的对象替代反序列化新建的对象。
2.Externalizable:强制自定义序列化
通过实现Externalizable接口,必须实现writeExternal、readExternal方法。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
实体类,必须有无参构造函数
public class User1 implements Externalizable {
private Integer id;
private String username;
private Integer age;
private static String liang;
public Integer getId() {
return id;
}
public User1(Integer id, String username, Integer age) {
this.id = id;
this.username = username;
this.age = age;
}
public User1() {
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
StringBuffer stringBuffer = new StringBuffer(username);
//字符串反转
stringBuffer.reverse();
out.writeObject(stringBuffer);
out.writeInt(id);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.username =((StringBuffer) in.readObject()).toString();
this.id = in.readInt();
this.age = in.readInt();
}
}
如果没有无参构造函数,反序列化的时候会报以下错误
序列化和反序列化
public static void main(String[] args) throws IOException {
User1 user = new User1(1,"aaa",100);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist\\1.txt"));
oos.writeObject(user);
oos.close();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\1.txt"));
User1 user = (User1) ois.readObject();
ois.close();
System.out.println(user);
}
注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。
两种序列化对比
- 序列化内容:Externalizable自定义序列化可以控制序列化的过程和决定哪些属性不被序列化。
- Serializable序列化时不会调用默认的构造器,而Externalizable序列化时会调用默认构造器的
- 使用Externalizable时,必须按照写入时的确切顺序读取所有字段状态。否则会产生异常。例如,如果更改ExternalizableDemo类中的number和name属性的读取顺序,则将抛出java.io.EOFException。而Serializable接口没有这个要求。
- Externalizable性能略好,Serializable性能略差
虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
源码导读
建议看下这个博客
以上内容属于个人笔记整理,如有错误,欢迎批评指正