Java 串行化
Serialization 是把对象的状态转换为字节流,同时字节流也可以转换为对象,反向过程叫做 Deserialization
串行化可以把对象的状态保存到文件中,也可以通过网络传输对象
串行化接口
java.io.Serializable
接口是一个标记接口(不含有数据和方法),String
和所有的原始数据类型的包装器类都默认实现了该接口
ObjectOutputStream
类用来串行化对象为OutputStream
,类字段如果是引用,对应的引用对象也需要序列化
ObjectInputStream
类用来反序列化先前串行化的原始数据和对象,重构对象
可以写入多个对象或原始数据类型到输出流,这些对象必须从相应的ObjectInputstream
读取,类型和顺序应该要和写入的相同
FileOutputStream fos = new FileOutputStream("t.tmp");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeInt(12345);
oos.writeObject("Today");
oos.writeObject(new Date());
oos.close();
FileInputStream fis = new FileInputStream("t.tmp");
ObjectInputStream ois = new ObjectInputStream(fis);
int i = ois.readInt();
String today = (String) ois.readObject();
Date date = (Date) ois.readObject();
ois.close();
- 可以使用try/catch,处理转换过程中可能出现的异常
- 使用
transient
修饰符的字段,不会被序列化 - 静态字段不会序列化(serialVersionUID例外)
- 不是每个类都可序列化,有些类是不能序列化的, 例如涉及线程的类
- 子类实现Serializable接口而父类未实现时,父类不会被序列化
- 父类实现序列化,子类自动实现序列化
如果需要特殊处理序列化和反序列化,可以在类中自定义序列化方法
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void readObjectNoData()
throws ObjectStreamException;
为了避免因为,JAVA的序列化机制采用了一种特殊的算法:
1、所有保存到磁盘中的对象都有一个序列化编号
2、当程序试图序列化一个对象时,会先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化,系统才会将该对象转换成字节序列并输出
3、如果对象已经被序列化,程序将直接输出一个序列化编号,而不是重新序列化
serialVersionUID
用来确保在反序列化的过程中,加载的是同样的类(序列号对应的类)
语法:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
原因:
有可能序列化一个对象到文件中,几个月后才在不同的JVM进行反序列化,此时对应的类可能已经改变了。
如果要反序列化的serialVersionUID
不相同,产生异常InvalidClassException
。
生成方式:
- 显式声明,比如和系统的版本保持一致
- 自动生成,没有显式声明的时候,进行序列化的时候根据相关规则产生一个默认的
serialVersionUID
,但是相应的计算对类的细节非常敏感,可能编译器的实现而有所不同,所以强烈建议所有可序列化的类都明确声明serialVersionUID
值
Externalizable
是Serializable
接口的子类,通过特定的两个方法来指定要序列化的对象,而父类直接序列化所有对象
writeExternal(ObjectOutput out)
readExternal(ObjectInput in)
与父类
Serializable
的区别,反序列化重构对象时,先通过一个public的无参数构造函数创建对象,再调用readExternal
方法,父类是直接通过ObjectInputStream
创建的
测试
import java.io.*;
public class Solution {
public static void main(String[] args) throws IOException {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/home/wdy/Desktop/test"));
outputStream.writeObject(new Test(0xBBBBBBBB,"Wang"));
outputStream.close();
}
public static class Test implements Serializable {
public static final long serialVersionUID = 0xAAAAAAAAAAAAAAAAL;
int num;
String name;
public Test(int num, String name) {
this.num = num;
this.name = name;
}
}
}
写出的二进制数据:
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: AC ED 00 05 73 72 00 21 63 6F 6D 2E 67 69 74 68 ,m..sr.!com.gith
00000010: 75 62 2E 77 61 6E 67 64 79 31 32 2E 53 6F 6C 75 ub.wangdy12.Solu
00000020: 74 69 6F 6E 24 54 65 73 74 AA AA AA AA AA AA AA tion$Test*******
00000030: AA 02 00 02 49 00 03 6E 75 6D 4C 00 04 6E 61 6D *...I..numL..nam
00000040: 65 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 et..Ljava/lang/S
00000050: 74 72 69 6E 67 3B 78 70 BB BB BB BB 74 00 04 57 tring;xp;;;;t..W
00000060: 61 6E 67 ang
解释:
序列化会记录每个类的名称,字段的名称和类型,最后才是具体的数据类型
ObjectOutputStream
初始化时就会写出头信息:
- 写出
AC ED
表示魔数,即文件类型,写出版本号00 05
,这两个字段都是固定的
写一个普通的对象writeOrdinaryObject
- 对象标志
0x73
类描述信息writeClassDesc
- 类标志
0x72
,表示一个新的类描述 - 类名称(
modified-utf-8
格式,这里编码长度为33字节即0x21
,大字节序写出为short形式,之后是UTF内容,这里全部是单字节编码com.github.wangdy12.Solution$Test
),序列号(8个0xAA
) - 标志,一个字节,表示不同的序列化类型(例如对象是否自定义了
writeObject
方法),这里为0x02
- 字段数目,两个字节,这里为
00 02
,即两个字段,接下来处理每个字段的描述信息 - int字段的写出,类型占用一个字节,对应为
I
,即49
,接下来是字段名称,前两个字节表示长度00 03
,后面是具体的名称num
- String字段的写出:签名第一个字符对应为
L
,即0x4C
,然后是字段名称,前两个字节表示长度00 04
,后面是字段名称name
,如果不是原始类型,再写出其类型签名,这里是Ljava/lang/String;
,以writeString
写出其类型签名,先写标志0x74
,然后内部再调用writeUTF
以UTF形式写出,长度0x12
,即18,内容Ljava/lang/String;
- 对象块的结束标志
0x78
- 递归写出对象的父类信息描述,递归调用
writeClassDesc
,这里为null,写出0x70
写出具体的数据writeSerialData
- 如果有自定义的
writeObject
就调用,如果没有使用默认的defaultWriteFields
写出,先写出原始数据类型的值,然后写出对象字段中的实际数据
序列化使用的常量位于ObjectStreamConstants
类中,内部包含一些标志位
static final short STREAM_MAGIC = (short)0xaced;
static final short STREAM_VERSION = 5;
static final byte TC_NULL = (byte)0x70;
static final byte TC_CLASSDESC = (byte)0x72;
static final byte TC_OBJECT = (byte)0x73;
static final byte TC_STRING = (byte)0x74;
static final byte TC_ENDBLOCKDATA = (byte)0x78;
Kryo
一种更高效的的序列化方式,相同对象的序列化,大小大大减小
public class Solution {
public static void main(String[] args) throws IOException {
Kryo kryo = new Kryo();
kryo.register(Test.class);//需要进行注册,不注册时改为 kryo.setRegistrationRequired(false);
Test test = new Test(0xBBBBBBBB,"Wang");
Output output = new Output(new FileOutputStream("/home/wdy/Desktop/test-kryo"));
kryo.writeClassAndObject(output, test);
output.close();
Input input = new Input(new FileInputStream("/home/wdy/Desktop/test-kryo"));
Object object2 = kryo.readClassAndObject(input);
input.close();
System.out.println(((Test)object2).num);
}
}
注册后序列化结果只有10个字节,不包含类型信息,第一个字节是一个变长int,表示注册对应的序号,之后四个字节表示Wang
,且g
最后一个字节的最高位为1,即最后一个字节为负数,表示字符串结束,最后五个字节是一个边长编码的int,即0xBBBBBBBB
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: 0B 57 61 6E E7 89 91 A2 C4 08 .Wang.."D.
如果不进行注册,对应结果会记录类名称
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: 01 00 63 6F 6D 2E 67 69 74 68 75 62 2E 77 61 6E ..com.github.wan
00000010: 67 64 79 31 32 2E 53 6F 6C 75 74 69 6F 6E 24 54 gdy12.Solution$T
00000020: 65 73 F4 57 61 6E E7 89 91 A2 C4 08 estWang.."D.