一、概念
java对象序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。
序列化:将java对象转化为字节序列的过程。
反序列化:将字节序列转化为java对象的过程。
二、使用对象序列化的原因
1、保存(持久化)对象及其状态到内存或者磁盘
- Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
- 序列化对象以字节数组保持-静态成员不保存使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
举例:在使用tomcat开发JavaEE相关项目的时候,我们关闭tomcat后,相应的session中的对象就存储在了硬盘上,如果我们想要在tomcat重启的时候能够从tomcat上面读取对应session中的内容,那么保存在session中的内容就必须实现相关的序列化操作。
2、序列化用户远程对象传输
除了在持久化对象时会用到对象序列化之外,java对象要在分布式中使用(spark,flink......)、当使用RMI(远程方法调用)、在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。
三、序列化实现方式
1、Serializable实现序列化
- 在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
- 通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化。
// serialization/Worm.java
// Demonstrates object serialization
import java.io.*;
import java.util.*;
class Data implements Serializable {
private int n;
Data(int n) { this.n = n; }
@Override
public String toString() {
return Integer.toString(n);
}
}
public class Worm implements Serializable {
private static Random rand = new Random(47);
private Data[] d = {
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10))
};
private Worm next;
private char c;
// Value of i == number of segments
public Worm(int i, char x) {
System.out.println("Worm constructor: " + i);
c = x;
if(--i > 0)
next = new Worm(i, (char)(x + 1));
}
public Worm() {
System.out.println("No-arg constructor");
}
@Override
public String toString() {
StringBuilder result = new StringBuilder(":");
result.append(c);
result.append("(");
for(Data dat : d)
result.append(dat);
result.append(")");
if(next != null)
result.append(next);
return result.toString();
}
public static void
main(String[] args) throws ClassNotFoundException,
IOException {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
try(
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("worm.dat"))
) {
out.writeObject("Worm storage\n");
out.writeObject(w);
}
try(
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("worm.dat"))
) {
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
System.out.println(s + "w2 = " + w2);
}
try(
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out2 =
new ObjectOutputStream(bout)
) {
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
try(
ObjectInputStream in2 = new ObjectInputStream(
new ByteArrayInputStream(
bout.toByteArray()))
) {
String s = (String)in2.readObject();
Worm w3 = (Worm)in2.readObject();
System.out.println(s + "w3 = " + w3);
}
}
}
}
输出为:
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
2、实现接口Externalizable(控制序列化)
Externalizable 接口继承了 Serializable 接口,同时增添了两个方法:writeExternal() 和 readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。在writeExternal()方法里定义了哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Author lenovo
**/
public class User implements Externalizable {
private static final long serialVersionUID = 1L;
private String userName;
private String password;
private String age;
public User(String userName, String password, String age) {
super();
this.userName = userName;
this.password = password;
this.age = age;
}
public User() {
super();
}
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;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
/**
* 序列化操作的扩展类
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//增加一个新的对象
Date date = new Date();
out.writeObject(userName);
out.writeObject(password);
out.writeObject(date);
}
/**
* 反序列化操作的扩展类
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 这里的接受顺序是有限制
userName=(String) in.readObject();
password=(String) in.readObject();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
Date date=(Date)in.readObject();
System.out.println("反序列化后的日期为:"+sdf.format(date));
}
@Override
public String toString() {
//这里的年龄是不会被序列化的,所以在反序列化的时候是读取不到数据的
return "用户名:"+userName+",密 码:"+password+",年龄:"+age;
}
}
/**
* 序列化和反序列化的相关操作类
*/
class Operator{
/**
* 序列化方法
*/
public void serializable(User user) throws FileNotFoundException, IOException{
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("hjw.txt"));
outputStream.writeObject(user);
}
/**
* 反序列化的方法
*/
public User deSerializable() throws FileNotFoundException, IOException, ClassNotFoundException{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("hjw.txt"));
return (User) ois.readObject();
}
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
Operator operate=new Operator();
User person=new User("hjw","123456","31");
System.out.println("序列化之前的数据:");
System.out.println(person.toString());
operate.serializable(person);
User newPerson=operate.deSerializable();
System.out.println("---------------------------------------------");
System.out.println("序列化之后的数据:");
System.out.println(person.toString());
}
}
================================================================
运行结果:
序列化之前的数据:
用户名:hjw,密 码:123456,年龄:31
反序列化后的日期为:2020-12-16
---------------------------------------------
序列化之后的数据:
用户名:hjw,密 码:123456,年龄:31
3、使用readResolve增强单例
但是如果Sinleton类实现了序列化,那么它不再是一个Singleton,无论该类使用了默认的序列化形式,还是自定义的序列化形式,还是是否提供显式的readObject方法都没关系。任何一个readObject方法,不管是显式还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。
readResolve特性允许使用readObject创建实例代替另一个实例,如果一个类定义了readResolve方法,并且具备正确的声明,那么在反序列化的之后,新建的readResolve方法就会被调用,然后返回的对象引用将被返回,取代新建的对象。
public class Singleton implements Serializable {
private static Singleton INSTANCE= new Singleton();
private Singleton(){};
private Object readResolve(){
return INSTANCE;
}
}
4、把对象包装成JSON字符串传输
采用JSON格式同时使用采用Google的gson-2.2.4.jar 进行转义
5、采用谷歌的ProtoBuf
- protocol buffers 是google内部得一种传输协议,目前项目已经开源(http://code.google.com/p/protobuf/)。它定义了一种紧凑得可扩展得二进制协议格式,适合网络传输,并且针对多个语言有不同得版本可供选择。
- proto文件是对数据的一个描述,包括字段名称,类型,字节中的位置。protoc工具读取proto文件生成对应builder代码的类库。protoc xxxxx --java_out=xxxxxx 生成java类库。builder类根据自己的算法把数据序列化成字节流,或者把字节流根据反射的原理反序列化成对象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。
总结:
- 五种方式对比传输同样的数据,google protobuf只有53个字节是最少的。结论:
方式 | 优点 | 缺点 |
JSON | 跨语言、格式清晰一目了然 | 字节数比较大,需要第三方类库 |
Externalizable | java原生方法不依赖外部类库 | 字节数比较大,不能跨语言 |
Object Serialize | java原生方法不依赖外部类库 | 字节数比较大,不能跨语言 |
使用readResolve增强单例 | java原生方法不依赖外部类库,安全性高 | 字节数比较大,不能跨语言 |
Google protobuf | 跨语言、字节数比较少 | 编写.proto配置用protoc工具生成对应的代码 |
四、序列化代价
--参考
1、可能会导致InvalidClassException异常
如果没有显式声明序列版本UID,对对象的需求进行了改动,那么兼容性将会遭到破坏,在运行时导致InvalidClassException。比如:增加一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化,则会出现序列版本UID不一致的情况。所以最好还是显式的增加序列版本号UID。
2、增加了出现Bug和安全漏洞的可能性
序列化机制是一种语言之外的对象创建机制,反序列化机制都是一个“隐藏的构造器”,具备与其他构造器相同的特点,正式因为反序列化中没有显式构造器,所以很容易就会忽略:不允许攻击者访问正在构造过程中的对象内部信息。换句话说,序列化后的字节流可以被截取进行伪造,之后利用readObject方法反序列会不符合要求甚至不安全的实例。
3、随着类发行新的版本,测试负担也会增加。
一个可序列化的类被修订时,需要检查是否“在新版本中序列化一个实例,可以在旧版本中反序列化”,如果一个实现序列化的类有很多的子类或者是被修改时,就不得不加以测试。
五、序列化的缺陷
1、序列化是保存对象的状态,也就是不会关心static静态域,静态域不会被序列化;
2、在序列化对象时,如果该对象中有引用对象域名,那么也要要求该引用对象是可实例化的;
3、默认序列化的过程可能消耗大量内存空间和时间,甚至可能会引起栈溢出:
因为第二条的原因,如果一个类中大量存在引用对象域,并且都需要实现序列化,那么整个序列化过程可能会很消耗时间,在通信传输过程中更是如此,同时序列化后的字节流需要足够大的内存。
总结:
- java中的序列化时transient变量(这个关键字的作用就是告知JAVA我不可以被序列化)和静态变量不会被序列;
- 如果你先序列化对象A后序列化B,那么在反序列化的时候一定记着JAVA规定先读到的对象;
- 实现序列化接口的对象并不强制声明唯一的serialVersionUID,是否声明serialVersionUID对于对象序列化的向上向下的兼容性有很大的影响;
- 如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
- 结合业务场景,从安全性,性能、成本三方面因素选择序列化方式