我在一个IBM工程师的博客里面看到一个说法,我感觉对于我理解序列化很有帮助,他说序列化的过程,就是一个“freeze”的过程,它将一个对象freeze住,然后进行存储,等到再次需要的时候,再将这个对象de-freeze就可以立即使用。
客户端访问了某个能开启会话功能的资源, web服务器就会创建一个与该客户端对应的HttpSession对象,每个HttpSession对象都要站用一定的内存空间。如果在某一时间段内访问站点的用户很多,web服务器内存中就会积累大量的HttpSession对象,消耗大量的服务器内存,即使用户已经离开或者关闭了浏览器,web服务器仍要保留与之对应的HttpSession对象,在他们超时之前,一直占用web服务器内存资源。
web服务器通常将那些暂时不活动但未超时的HttpSession对象转移到文件系统或数据库中保存,服务器要使用他们时再将他们从文件系统或数据库中装载入内存,这种技术称为Session的持久化。
将HttpSession对象保存到文件系统或数据库中,需要采用序列化的方式将HttpSession对象中的每个属性对象保存到文件系统或数据库中;将HttpSession对象从文件系统或数据库中装载如内存时,需要采用反序列化的方式,恢复HttpSession对象中的每个属性对象。所以存储在HttpSession对象中的每个属性对象必须实现Serializable接口。
serialVersionUID 的作用
serialVersionUID 用来表明类的不同版本间的兼容性
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID 。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。
引起这个疑问,还是从Hibernate使用查询缓存说起;对象实例除了存在于内存,二级缓存还会将对象写进硬盘在需要的时候再读取出来使用,此时就必须提到一个概念:序列化。
程序在运行时实例化出对象,这些对象存在于内存中,随着程序运行停止而消失,但如果我们想把某些对象(一般都是各不相同的属性)保存下来或者传输给其他进程,在程序终止运行后这些对象仍然存在,可以在程序再次运行时读取这些对象的信息,或者在其他程序中利用这些保存下来的对象信息恢复成实例对象。这种情况下就要用到对象的序列化和反序列化。
其实很早就知道的,在Java中常见的几个类,如:Interger/String等,都实现了java.io.Serializable接口。这个序列化接口没有任何方法和域,仅用于标识序列化语意;实现 Serializable 接口的类是可序列化的,没有实现此接口的类将不能被序列化和反序列化。序列化类的所有子类本身都是可序列化的,不再需要显式实现 Serializable 接口。只有经过序列化,才能兼容对象在磁盘文本以及在网络中的传输,以及恢复对象的时候反序列化等操作。
问题一:为何要实现序列化?
答:序列化就是对实例对象的状态(State 对象属性而不包括对象方法)进行通用编码(如格式化的字节码)并保存,以保证对象的完整性和可传递性。
简而言之:序列化,就是为了在不同时间或不同平台的JVM之间共享实例对象
// 经常使用如下:
public static void main(String[] args) throws Exception {
File file = new File("user.ser");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
User user = new User("zhang", 18, Gender.MALE);
oout.writeObject(user);
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newUser = oin.readObject();
oin.close();
System.out.println(newUser);
}
如没有 实现Serializable接口,在序列化时,使用ObjectOutputStream的write(object)方法将对象保存时将会出现异常。其实 java.io.Serializable 只是一个没有属性和方法的空接口,但是问题来了。。
问题二:为何一定要实现 Serializable 才能进行序列化呢?
使用 ObjectOutputStream 来持久化对象, 对于此处抛出的异常,查看该类中实现如下:
private void writeObject0(Object obj, boolean unshared) throws IOException {
// ...
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
// ...
}
从此可知, 如果被写对象类型是String、数组、Enum、Serializable,就可以进行序列化,否则将抛出NotSerializableException。
工作后做的第一个项目是电商项目。当时不会做项目,只能照猫画虎。其中一个VO类为何要实现Serializable接口一直没有理解,不实现这个Serializable,会报错。如下是随手写的一个VO类Person.java:
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
//两个属性及getter、setter方法
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
二、什么是序列化、反序列化
翻阅JDK1.8的官方开发文档,发现Serializable接口没有方法和字段,仅用于标识可序列化的VO类。不实现此接口的类的任何字段(属性)都不能序列化和反序列化。那么什么是序列化和反序列化呢?
由于序列化和反序列化概念太抽象,还是通过两个场景来理解为上策。
(场景一)以上面提到的Person.java为例。这个VO类中的两个字段name和age在程序运行后都在堆内存中,程序执行完毕后内存得到释放,name和age的值也不复存在。如果现在计算机要把这个类的实例发送到另一台机器、或是想保存这个VO类的实例到数据库(持久化对象),以便以后再取出来用。这时就需要对这个类进行序列化,便于传送或保存。用的时候再反序列化重新生成这个对象的实例。
(场景二)以搬桌子为例,桌子太大了不能通过比较小的门,我们要把它拆了再运进去,这个拆桌子的过程就是序列化。同理,反序列化就是等我们需要用桌子的时候再把它组合起来,这个过程就是反序列化。
学到这里就可以给序列化下一个定义了:
把原本在内存中的对象状态 变成可存储或传输的过程称之为序列化。序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
序列化前的对象和反序列化后得到的对象,内容是一样的(且对象中包含的引用也相同),但两个对象的地址不同。换句话说,序列化操作可以实现对任何可Serializable对象的”深度复制(deep copy)"。
三、什么情况下需要序列化
通过上面的分析,我们不难看出,一下三种情况需要使用序列化接口:
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中,以便可以在以后重新创建精确的副本;
b)当你想用套接字在网络上传送对象的时候(从一个应用程序域发送到另一个应用程序域中);
c)当你想通过RMI传输对象的时候;
四、注意事项:
a)序列化时,只对对象的状态进行保存,而不管对象的方法;
b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
d)并非所有的对象都可以序列化。
e) 序列化会忽略静态变量,即序列化不保存静态变量的状态。静态成员属于类级别的,不能序列化。添加了static、transient关键字后的变量不能序列化。
1、在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,如此引用传递序列化。如果一个对象包含的成员变量是容器类等并深层引用,那么序列化过程开销也较大。
2、当字段被声明为 transient 后,默认序列化机制就会忽略该字段。(还有方法就是自定义writeObject方法,见下代码示例)
3、在单例类中添加一个readResolve()方法(直接返回单例对象),以保证在序列化过程仍保持单例特性。
五、序列化ID
private static final long serialVersionUID = 1L;
序列化 ID在 Eclipse 下提供了两种生成策略,一个设为固定的 1L,另一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成)。一般如果没有特殊需求,用默认的 1L 就可以,这样可以确保反序列化成功。因为不同的序列化id之间不能进行序列化和反序列化。
此外补充一下,
在路径下jdk中还有另外一种形式的对象持久化,即:外部化(Externalization)。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
外部化和序列化是实现同一目标的两种不同方法。
通过 Serializable 接口对对象序列化的支持是jdk内支持的 API ,但是java.io.Externalizable的所有实现者必须提供读入和写出的具体实现,怎么实现完全由你自定义。序列化(Serializable )会自动存储所有必要的信息(如属性以及属性类型等),用以反序列化成原来一样的实例,而外部化(Externalizable)则只保存被存储实例中你需要的信息。
public class User implements Externalizable {
private String name;
transient private Integer age; // 屏蔽字段
private Gender gender;
public User() {
System.out.println("none constructor");
}
public User(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
// 实现读写
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
// 屏蔽gender
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
// 具体重写
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
// 屏蔽gender
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}