参考资料:
写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。
目录
3、externalizable和Serializable的区别
一、序列化是什么
1、序列化的定义
序列化的意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:
- 序列化:把Java对象转换为字节序列。
- 反序列化:把字节序列恢复为原先的Java对象。
注意:序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
2、序列化的方式
序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:JDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言)。
3、序列化技术选型的注意点
序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。
(1)协议是否支持跨平台
如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你JDK序列化出来的格式,其他语言并没法支持。
(2)序列化的速度
如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。
(3)序列化出来的大小
如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。
二、java中序列化的方式
在Java中,如果一个对象要想实现序列化,必须要实现Serializable接口或者Externalizable接口。其中Externalizable接口继承自Serializable接口。
1、通过Serializable接口实现序列化
(1)使用方法
实体类:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = null;
private Integer age = null;
private Sex sex;
public Person() {
System.out.println("调用默认构造函数");
}
public Person(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + ", sex=" + sex + '}';
}
}
class SerializeDemo01 {
enum Sex {
MALE,
FEMALE
}
}
调用方法:
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(new Person("Jack", 30, Sex.MALE));
oos.close();
fos.close();
FileInputStream fis = new FileInputStream("D:\\temp.txt");
ObjectInputStream oin = new ObjectInputStream(fis);
Object obj = oin.readObject(); // 读取对象
oin.close();
fis.close();
System.out.println(obj);
}
结果如下:
Person{name='Jack', age=30, sex=MALE}
这样,我们成功的将实体类序列化后再反序列化。
(2)、Serializable接口是如何实现序列化的
我们进入Serializable接口的源码,发现这是一个空接口,没有任何实现。
public interface Serializable {
}
那也就意味着,Serializable接口是作为标记而存在的,这里我们将实体类中的继承关系删除,再尝试进行序列化操作,发现如下报错。
我们根据报错的堆栈信息,进入java.io.ObjectOutputStream类的 writeObject0()方法,发现了如下判断。这里对序列化的对象进行了判断,需要是String、数组、枚举、或Serializable子类才可以进行序列化操作,否则抛出异常。这也验证了我们的猜想,Serializable接口其实是作为标记特征而存在的,真正实现序列化过程的是java.io.ObjectOutputStream类。相应的,反序列化则由java.io.ObjectInputStream类实现。
// 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());
}
}
简单总结一下:
序列化由java.io.ObjectOutputStream
类的writeObject()
方法实现;反序列化由java.io.ObjectInputStream
类的readObject()
方法实现。
(3)、如何修改默认的序列化
在Serializable序列化机制中,默认调用writeObject、readObject方法进行序列化和反序列化,这两个方法会对该对象的所有属性进行操作,如果希望某个属性不序列化,我们可以加上transient修饰符。
private transient String mood;
同时,我们还可以通过自定义readObject实现对反序列化数值的校验。
public class Person implements Serializable{
//其余省略
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
// 调用默认的反序列化函数
objectInputStream.defaultReadObject();
// 手工检查反序列化后数据的正确性,若发现有问题,即终止操作!
if( 0 > age) {
throw new IllegalArgumentException("年龄不能小于0!");
}
}
}
再次进行序列化和反序列化,会抛出异常。
public static void main(String[] args) throws IOException, ClassNotFoundException {
//其余省略
oos.writeObject(new Person("Jack", -1, Sex.MALE));
//其余省略
}
2、通过Externalizable接口实现序列化
Externalizable是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public。
(1)使用方法
实体类:
public class Person implements Externalizable{
private static final long serialVersionUID = 1L;
private String name = null;
private Integer age = null;
private transient String mood = null;
private Sex sex;
public Person() {
System.out.println("调用默认构造函数");
}
public Person(String name, Integer age, Sex sex, String mood) {
this.name = name;
this.age = age;
this.sex = sex;
this.mood = mood;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + ", sex=" + sex + ", mood=" + mood +'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 指定序列化时候写入的属性。这里仍然不写入年龄
out.writeObject(name);
out.writeObject(age);
out.writeObject(sex);
out.writeObject(mood);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 指定反序列化的时候读取属性的顺序以及读取的属性
// 如果你写反了属性读取的顺序,你可以发现反序列化的读取的对象的指定的属性值也会与你写的读取方式一一对应。因为在文件中装载对象是有序的
name=(String) in.readObject();
age=(Integer) in.readObject();
sex=(Sex) in.readObject();
mood=(String) in.readObject();
}
}
调用方法:
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(new Person("Jack", -1, Sex.MALE,"sad"));
oos.close();
fos.close();
FileInputStream fis = new FileInputStream("D:\\temp.txt");
ObjectInputStream oin = new ObjectInputStream(fis);
Object obj = oin.readObject(); // 读取对象
oin.close();
fis.close();
System.out.println(obj);
}
结果如下
可以看到是,使用Externalizable接口,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,因此Externalizable对象必须有默认构造函数,而且必需是public的。同时我们可以在重写writeExternal、readExternal方法时指定哪些成员变量参与序列化,所以transient在这里无效。
3、externalizable和Serializable的区别
(1)、实现serializable接口是默认序列化所有属性,如果有不需要序列化的属性使用transient修饰。externalizable接口是serializable的子类,实现这个接口需要重写writeExternal和readExternal方法,指定对象序列化的属性和从序列化文件中读取对象属性的行为。
(2)、实现serializable接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个持久化状态,再将这个状态赋值给该类的另一个变量。实现externalizable接口的对象序列化文件进行反序列化先走构造方法得到控对象,然后调用readExternal方法读取序列化文件中的内容给对应的属性赋值。
(3)使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。
当然这里我们有一些疑惑,Serializable 中的writeObject()方法与readObject()方法科可以实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他们有什么异同呢?
- readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。
- 需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。
- 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。
三、序列化版本号serialVersionUID
serialVersionUID 是 Java 为每个序列化类产生的版本标识。它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出 InvalidClassException。
如果可序列化类没有显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。尽管这样,还是建议在每一个序列化的类中显式指定 serialVersionUID 的值。因为不同的 jdk 编译很可能会生成不同的 serialVersionUID 默认值,从而导致在反序列化时抛出 InvalidClassExceptions 异常。
serialVersionUID 字段必须是 static final long 类型。
1、使用默认serialVersionUID
接下来我们在Person类里面动点手脚,比如在里面再增加一个名为id的字段,再序列后后再删除这个字段,然后进行反序列化,结果如下:
因此我们得出结论,使用默认serialVersionUID必须保证实体类在序列化前后严格一致,否则将会导致无法反序列化。
2、使用自定义的serialVersionUID
我们自定义一个serialVersionUID,注意类型必须是static final long。
public class Person implements Externalizable{
private static final long serialVersionUID=1L;
// 其余省略
}
然后再进行和上一节同样的操作,发现可以成功反序列化。
综上所述,我们大概可以清楚:serialVersionUID 用于控制序列化版本是否兼容。若我们认为修改的可序列化类是向后兼容的,则不修改 serialVersionUID。
四、序列化的注意要点
1、父类是 Serializable,所有子类都可以被序列化。
2、子类是 Serializable ,父类不是,则子类可以正确序列化,但父类的属性不会被序列化(不报错,数据丢失)。
3、如果序列化的属性是对象,则这个对象也必须是 Serializable ,否则报错。
4、反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错。
5、反序列化时,如果 serialVersionUID 被修改,则反序列化会失败。
6、序列化会破坏单例的限制,无论是Serializable还是Externalizable都会构建一个新的对象,如果要做限制可以增加readResolve(),对反序列化的行为做出规定。
五、Java 序列化的缺陷
1、无法跨语言
Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
2、容易被攻击
对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。
3、序列化后的流太大
Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,编码后的数组很大,非常影响存储和传输效率。
4、序列化性能
Java 的序列化耗时比较大。序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。
5、序列化的限制
Java 官方的序列化一定需要实现 Serializable 接口,同时需要关注 serialVersionUID。