一.Java中序列化概念
-
Java中的序列化是指
把一个Java对象变成二进制内容
,本质上就是把java对象转换成一个byte[]数组
。 -
为什么要把Java对象序列化呢?
- 因为序列化后可以把byte[]数组保存到文件(磁盘)中,或者把byte[]数组通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
-
Java 中的反序列化,即
把一个二进制内容(也就是byte[]数组)变回Java对象
。有了反序列化,保存到文件
中的byte[]数组
又可以“变回”Java对象“
,或者从网络
上读取byte[]
并把它“变回”Java对象"
。
当两个进程进行交互时,双方发送各种类型的数据,包括
文本、图片、音频、视频
等, 都会以二进制序列(byte[]
,字节流
)的形式在网络上传输。
- 序列化与反序列化则可以实现
网络数据传输
,发送方
需要把这个Java对象
转换为字节序列(字节流)
,才能在网络上传送;接收方
则需要把字节序列(字节流)
再恢复为Java对象。
二.序列化注意事项
1.序列化能保存的元素
-
只能保存对象的非静态成员变量
-
不能保存任何成员方法和静态的成员变量
-
不保存transient成员变量
(transient关键字标记的成员变量不参与序列化过程。)
-
如果一个对象的成员变量是一个引用类型的变量,这个对象的成员变量也会保存
-
序列化保存的只是变量的值,对于变量的任何修饰符,都不能保存
-
如果一个可序列化的对象包含对某个不可序列化的对象成员变量,那么整个序列化操 作将会失败,并且会抛出一个
NotSerializableException。
如果我们将这个成员变量设置为transient
,那么对象仍然可以序列化。
2.同一个对象多次序列化的处理
- 所有保存到磁盘中的对象都有一个序列化编号
- 序列化一个对象中,首先检查该对象是否已经序列化过
- 如果没有,进行序列化 ,如果已经序列化,将不再重新序列化,而是输出编号即可
3.敏感属性禁止序列化
如果不希望某些属性(敏感)序列化,或不希望出现递归序列
- 为属性添加
transient
关键字(transient关键字标记的成员变量不参与序列化过程。)
自定义序列化
(不仅可以决定哪些成员变量不参与序列化,还可以定义成员变量具体如何序列化
4.序列化版本不兼容
- 修改了引用类型成员变量后,会影响版本号,从而导致反序列化不成
解决方案:为Java对象指定序列化版本号serialVersionUID
5.对象序列化的条件
-
只有实现了Serializable接口的类的对象才可以被序列化。
Serializable接口中没有任何的方法,称为标记接口,用于,标记当前类是否可以序列化。 -
如果对象的成员是对象,成员变量对应类也必须实现
Serializable接口
三 序列化
将Java对象转换成字节序列(IO字节流) ,简单来说就是:使用ObjectOutputStream
,把一个Java对象变为byte[]数组
public static void main(String[] args) throws IOException {
try (
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(buffer)
) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object
//output.writeObject(Double.valueOf(123.456));
//获取写入字节
//System.out.println(Arrays.toString(buffer.toByteArray()));
}
}
ObjectOutputStream
既可以写入基本类型,如int,boolean
,也可以写入String(以UTF-8编码)
,还可以写入实现了Serializable接口的Object。
- 因为写入Object时需要大量的类型信息,所以写入的内容很大。
四.反序列化
从字节序列中恢复Java对象,简单来说就是: 使用ObjectInputStream
把一个byte[]数组
变成Java对象
try (ObjectInputStream input = new ObjectInputStream("text.dat")) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
- 除了能读取
基本类型
和String类型
外,调用readObject()
可以直接返回一个Object对象
。要把它变成一个特定类型,必须强制转型
。
readObject()可能抛出的异常有:
ClassNotFoundException
:没有找到对应的Class;
,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。InvalidClassException
:Class不匹配。- 对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
- 为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的
serialVersionUID静态变量
,用于标识Java类的序列化“版本”
,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本:
public class Person implements Serializable { private static final long serialVersionUID = 2709425275741743919L; }
要特别注意反序列化的几个重要特点:
-
反序列化
时,由JVM直接构造出Java对象,不调用构造方法
,构造方法内部的代码,在反序列化时根本不可能执行。 -
因为Java的序列化机制可以
导致一个实例能直接从byte[]数组创建,而不经过构造方法,
因此,它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码
,从而导致严重的安全漏洞。
实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。
更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
五.小结
-
要序列化的Java对象必须实现
java.io.Serializable接口
,类似Serializable这样的空接口被称为“标记接口”(Marker Interface)
; -
反序列化时不调用构造方法,可设置serialVersionUID作为版本号(非必需)
; -
Java的序列化机制仅适用于Java
,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。
-
当对象中自定义了 writeObject 和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。