什么是序列化
内容主要参考 https://blog.csdn.net/xlgen157387/article/details/79840134
Java的序列化(Object serialization)机制,就是将对象编码成一个字节流(序列化serialization),以及从字节流编码中重新构建对象(反序列化deserialization)的过程。一旦将对象序列化后,一方面可以将其持久化到磁盘上,供以后反序列化使用;另一方面在分布式环境中经常将对象从这一端网络传递到另一端,需要一种在两端传输数据的协议,而java序列化机制就提供了这种协议的实现。
- 序列化
对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。 - 反序列化
客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
为什么要序列化
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。
那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!
换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
总的来说可以归结为以下几点:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;
序列化测试
import java.io.Serializable;
/**
* @author Twilight
* @date 18-12-1 下午1:55
*/
public class Test implements Serializable {
private String name;
private int age;
private transient String temporary;
private static String aekc = "666";
//这里省略了set get方法
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
", age=" + age +
", temporary='" + temporary + '\'' +
", aekc='" + aekc + '\'' +
'}';
}
}
测试方法
import java.io.*;
/**
* @author Twilight
* @date 18-12-1 下午1:59
*/
public class TestSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializeTest();
Test test = deserializeTest();
System.out.println(test.toString());
}
private static void serializeTest() throws IOException {
Test test = new Test();
test.setAge(10);
test.setName("小明");
test.setTemporary("序列化测试");
FileOutputStream fileOutputStream = new FileOutputStream(new File("testSerializable.txt"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(test);
System.out.println("序列化成功");
objectOutputStream.close();
}
private static Test deserializeTest() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream(new File("testSerializable.txt"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Test test = (Test) objectInputStream.readObject();
System.out.println("反序列化成功");
return test;
}
}
最后输出如下
序列化成功
反序列化成功
Test{name='小明', age=10, temporary='null', aekc='666'}
可以看到temporary为空,所以声明为transient的成员不能被序列化。transient代表对象的临时数据。
我们序列化后,在反序列化之前修改下静态变量aekc的值
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializeTest();
Test.setAekc("888");
Test test = deserializeTest();
System.out.println(test.toString());
}
输出如下
序列化成功
反序列化成功
Test{name='小明', age=10, temporary='null', aekc='888'}
可见声明为static类型的成员也不能被序列化。因为static代表类的状态。
如果被序列化的类中的成员变量是一个对象呢?
import java.io.Serializable;
public class Test implements Serializable {
private String name;
private int age;
private transient String temporary;
private static String aekc = "666";
private Test2 test2;
//这里省略了set get方法
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
", age=" + age +
", temporary='" + temporary + '\'' +
", test2=" + test2 +
", aekc='" + aekc + '\'' +
'}';
}
}
public class Test2 implements Serializable {
private String name;
public Test2(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test2{" +
"name='" + name + '\'' +
'}';
}
}
对象成员对应的类也要实现Serializable接口,否则就会抛出NotSerializableException异常。如果是父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
序列化与单例模式
import java.io.Serializable;
public class Singleton implements Serializable {
private Singleton() {}
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
测试方法
import java.io.*;
public class TestSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializeTest();
System.out.println(deserializeTest() == Singleton.getInstance());
}
private static void serializeTest() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("testSerializable.txt"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(Singleton.getInstance());
System.out.println("序列化成功");
objectOutputStream.close();
}
private static Singleton deserializeTest() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream(new File("testSerializable.txt"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
System.out.println("反序列化成功");
return (Singleton) objectInputStream.readObject();
}
}
输出如下
序列化成功
反序列化成功
false
结果是false,说明
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
简单说下why,我们通过debug objectInputStream.readObject()方法,可以找到这个方法readOrdinaryObject。该方法中有一块代码
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
这里isInstantiable表示,如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
所以,我们可以知道为什么序列化可以破坏单例了吧
序列化会通过反射调用无参数的构造方法创建一个新的对象。
如何解决呢?这里需要在Singleton类中定义readResolve就可以解决该问题
import java.io.Serializable;
public class Singleton implements Serializable {
private Singleton() {}
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Object readResolve() {
return instance;
}
}
输出如下
序列化成功
反序列化成功
true
原理同样也在readOrdinaryObject方法下
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true。
invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
serialVersionUId
serialVersionUID是用来辅助序列化和反序列化过程的,序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,当反序列化的时候系统会检测当前文件中的serialVersionUID是否和当前类的serialVersionUID一致,如果一致这个时候可以反序列化成功,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的。
如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或减少了某些成员变量,那么系统会重新计算当前类的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败。所以当我们手动指定了serialVersionUID的值,就可以很大程度上避免了反序列化的失败。但是如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化还是会失败,因为类结构发生了改变。
其他序列化技术
- 专门针对Java语言的:基于Protobuf的Kryo,JBoss的Marshalling等等
- 跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack等等
下面我们测试下Kryo序列化方式和Java原生序列化方式的时间效率对比
实体类
package com.serializable.test;
import java.io.Serializable;
import java.util.Map;
public class Simple implements Serializable {
private static final long serialVersionUID = -4914434736682797743L;
private String name;
private int age;
private Map<String,Integer> map;
public Simple(){
}
public Simple(String name,int age,Map<String,Integer> map){
this.name = name;
this.age = age;
this.map = map;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Map<String, Integer> getMap() {
return map;
}
public void setMap(Map<String, Integer> map) {
this.map = map;
}
}
Java原生序列化方式
package com.serializable.test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;
public class OriginalSerializable {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
setSerializableObject();
System.out.println("java原生序列化时间:" + (System.currentTimeMillis() - start) + " ms" );
start = System.currentTimeMillis();
getSerializableObject();
System.out.println("java原生反序列化时间:" + (System.currentTimeMillis() - start) + " ms");
}
public static void setSerializableObject() throws IOException{
FileOutputStream fo = new FileOutputStream("file1.bin");
ObjectOutputStream so = new ObjectOutputStream(fo);
for (int i = 0; i < 100000; i++) {
Map<String,Integer> map = new HashMap<String, Integer>(2);
map.put("zhang0", i);
map.put("zhang1", i);
so.writeObject(new Simple("zhang"+i,(i+1),map));
}
so.flush();
so.close();
}
public static void getSerializableObject(){
FileInputStream fi;
try {
fi = new FileInputStream("file1.bin");
ObjectInputStream si = new ObjectInputStream(fi);
Simple simple =null;
while((simple=(Simple)si.readObject()) != null){
//System.out.println(simple.getAge() + " " + simple.getName());
}
fi.close();
si.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
//e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
kyro序列化方式
package com.serializable.test;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.objenesis.strategy.StdInstantiatorStrategy;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class KyroSerializable {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
setSerializableObject();
System.out.println("Kryo 序列化时间:" + (System.currentTimeMillis() - start) + " ms" );
start = System.currentTimeMillis();
getSerializableObject();
System.out.println("Kryo 反序列化时间:" + (System.currentTimeMillis() - start) + " ms");
}
public static void setSerializableObject() throws FileNotFoundException {
Kryo kryo = new Kryo();
kryo.setReferences(false);
kryo.setRegistrationRequired(false);
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
kryo.register(Simple.class);
Output output = new Output(new FileOutputStream("file2.bin"));
for (int i = 0; i < 100000; i++) {
Map<String,Integer> map = new HashMap<String, Integer>(2);
map.put("zhang0", i);
map.put("zhang1", i);
kryo.writeObject(output, new Simple("zhang"+i,(i+1),map));
}
output.flush();
output.close();
}
public static void getSerializableObject() {
Kryo kryo = new Kryo();
kryo.setReferences(false);
kryo.setRegistrationRequired(false);
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
Input input;
try {
input = new Input(new FileInputStream("file2.bin"));
Simple simple = null;
while((simple = kryo.readObject(input, Simple.class)) != null) {
//System.out.println(simple.getAge() + " " + simple.getName() + " " + simple.getMap().toString());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
最后时间效率对比
java原生序列化时间:8826 ms
java原生反序列化时间:10354 ms
Kryo 序列化时间:730 ms
Kryo 反序列化时间:20 ms
经过对比,可以发现kryo是java原生序列化性能十几倍
序列化后文件大小对比
java原生序列化文件大小:7606k
Kryo序列化文件大小:5337k