Java中的序列化
Posted codeloong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的序列化相关的知识,希望对你有一定的参考价值。
序列化是什么意思,能不能给我通俗的讲一下?
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。 为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
为什么需要要序列化?
- 一般Java对象的生命周期比Java虚拟机短,而实际的开发中,我们需要在Jvm停止后能够继续持有对象,这个时候就需要用到序列化技术将对象持久到磁盘或数据库。
- 在多个项目进行RPC调用的,需要在网络上传输JavaBean对象。我们知道数据只能以二进制的形式才能在网络上进行传输。所以也需要用到序列化技术。
Java序列化的实现
在Java中,要把一个对象序列化,必须要实现下面两个接口之一:
- Serializble
- Externalizable
Serializble接口
一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传输。不想序列化的字段可以使用transient修饰。
Externalizable接口
它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
两者之间的对比
使用时,你只想隐藏一个属性,比如用户对象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接口方式来实现序列化。
序列化版本号
在Java序列化中,可以控制序列化的版本,该字段为被序列化对象中的serialVersionUID字段。
private static final long serialVersionUID = 1L;
一个对象数据,在反序列化过程中,如果序列化串中的serialVersionUID与当前对象值不同,则反序列化失败,否则成功。
如果serialVersionUID没有显式生成,系统就会自动生成一个。生成的输入有:类名、类及其属性修饰符、接口及接口顺序、属性、静态初始化、构造器。任何一项的改变都会导致serialVersionUID变化。
为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的生明一个serialVersionUID。
显式定义serialVersionUID的两种用途:
-
希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID;
-
不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。如果我们保持了serialVersionUID的一致,则在反序列化时,对于新增的字段会填入默认值null(int的默认值0),对于减少的字段则直接忽略。
序列化代码实现
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String gender;
private int age;
private Date birth;
private Pet pet;
public User() {
}
public User(String username, String gender, int age, Date birth, Pet pet) {
this.username = username;
this.gender = gender;
this.age = age;
this.birth = birth;
this.pet = pet;
}
//序列化时默认调用
private void writeObject(ObjectOutputStream out) throws IOException {
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = encoder.encode(this.username.getBytes());
this.username = new String(bytes);
out.defaultWriteObject();
}
//反序列化时默认调用
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Base64.Decoder decoder = Base64.getDecoder();
byte[] bytes = decoder.decode(this.username);
this.username = new String(bytes);
}
//=======================测试一下==========================
public static void main(String[] args) {
User user = new User("jay", "male",
18, new Date(), new Pet("wangcai", "male"));
File file = new File("user.dat");
if (!file.exists()){
try {
if(!file.createNewFile()){
System.out.println("文件创建失败!");
return;
}
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file, false));
oos.writeObject(user);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void test(){
File file = new File("user.dat");
if (!file.exists()) return;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
User user = (User) ois.readObject();
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在网络中传输使用socket模拟即可,不写了
序列化和单例模式
所谓单例:就是单例模式就是在整个全局中(无论是单线程还是多线程),该对象只存在一个实例,而且只应该存在一个实例,没有副本。
序列化对单例有破坏:
- 1、通过对某个对象的序列化与反序列化得到的对象是一个新的对象,这就破坏了单例模式的单例性。
- 2、我们知道readObject()的时候,底层运用了反射的技术,序列化会通过反射调用无参数的构造方法创建一个新的对象。这破坏了对象的单例性。
- 3、解决方案:在需要的单例的对象类中添加如下代码
private Object readResolve(){
return instance;
}
为什么说序列化并不安全
因为序列化的对象数据转换为二进制,并且完全可逆。但是在RMI调用时所有private字段的数据都以明文二进制的形式出现在网络的套接字上,这显然是不安全的。
解决方案:
- 序列化Hook化(移位和复位)
- 序列数据加密和签名
- 利用transient的特性解决
- 打包和解包代理
以上是关于Java中的序列化的主要内容,如果未能解决你的问题,请参考以下文章
片段 A 的列表视图中的片段 B 中的新列表视图,单击 A 的列表项