前言
在开发过程中经常会对实体进行序列化,但其实我们只是在“只知其然,不知其所以然”的状态,很多时候会有这些问题:
- 什么是序列化和反序列化?为什么要序列化?
- 怎么实现序列化?
- 序列化的原理是什么呢?
- transient关键字
- 序列化时应注意什么?
如果你也有这些疑问,不妨看看本文?
(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)
什么是序列化和反序列化?
Java序列化是指把Java对象转换为字节序列的过程;
Java反序列化是指把字节序列恢复为Java对象的过程;
为什么要序列化?
其实我们的对象不只是存储在内存中,它还需要在传输网络中进行传输,并且保存起来之后下次再加载出来,这时候就需要序列化技术。
- 一般Java对象的生命周期比Java虚拟机端,而实际开发中如果需要JVM停止后能够继续持有对象,则需要用到序列化技术将对象持久化到磁盘或数据库。
- 在多个项目进行RPC调用时,需要在网络上传输JavaBean对象,而网络上只允许二进制形式的数据进行传输,这时则需要用到序列化技术。
Java的序列化技术就是把对象转换成一串由二进制字节组成的数组,然后将这二进制数据保存在磁盘或传输网络。而后需要用到这对象时,磁盘或者网络接收者可以通过反序列化得到此对象,达到对象持久化的目的。
怎么实现序列化?
序列化的过程一般会是这样的:
- 将对象实例相关的类元数据输出
- 递归地输出类的超类描述,直到没有超类
- 类元数据输出之后,开始从最顶层的超类输出对象实例的实际数据值
- 从上至下递归输出实例的数据
所以,如果父类已经序列化了,子类继承之后也可以进行序列化。
实现第一步,则需要的先将对象实例相关的类标记为需要序列化。
实现序列化的要求:目标对象实现Serializable接口
我们先创建一个NY类,实现Serializable接口,并生成一个版本号:
public class NY implements Serializable {
private static final long serialVersionUID = 8891488565683643643L; //使用idea生成
private String name;
private String blogName;
//省略getter和setter...
@Override
public String toString() {
return "NY{" +
"name='" + name + '\'' +
", blogName='" + blogName + '\'' +
'}';
}
}
在这里,Serializable接口的作用只是标识这个类是需要进行序列化的,而且Serializable接口中并没有提供任何方法。而且serialVersionUID序列化版本号的作用是用来区分我们所编写的类的版本,用于反序列化时确定版本。
JDK类库中序列化和反序列化API
-
java.io.ObjectInputStream:对象输入流
该类中的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
-
java.io.ObjectOutputStream:对象输出流
该类的writeObject()方法将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。
结合上面的NY类,我们来看看使用JDK类库中的API怎么实现序列化和反序列化:
public class SerializeNY {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializeNY();
NY ny = deserializeNY();
System.out.println(ny.toString());
}
private static void serializeNY() throws IOException {
NY ny = new NY();
ny.setName("NY");
ny.setBlogName("NYfor2020");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\serialable.txt")));
oos.writeObject(ny);
System.out.println("NY 对象序列化成功!");
oos.close();
}
private static NY deserializeNY() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\serialable.txt")));
NY ny = (NY) ois.readObject();
System.out.println("NY 对象反序列化成功");
return ny;
}
}
运行结果为:
NY 对象序列化成功!
NY 对象反序列化成功
NY{name='NY', blogName='NYfor2020'}
可以看到,这整个过程简单来说就是把对象存在磁盘,然后再从磁盘读出来。
但是我们平时看到序列化的实体中的serialVersionUID,为什么有的是1L,有的是一长串数字?
上面我们的提到serialVersionUID作用就是用来区分类的版本,所以无论是1L还是一长串数字,都是用来确认版本的。如果序列化的类版本改变,则在反序列化的时候就会报错。
举个栗子,刚刚我们已经在磁盘中生成了NY对象的序列化文件,如果我们对NY类的serialVersionUID稍作改动,改成:
private static final long serialVersionUID = 8891488565683643643L; //将末尾的2改成3
再执行一次反序列化方法,运行结果如下:
Exception in thread "main" java.io.InvalidClassException: NY; local class incompatible: stream classdesc serialVersionUID = 8891488565683643642, local class serialVersionUID = 8891488565683643643
......
至于怎么让idea生成serialVersionUID,则需要在idea设置中改个配置即可:
之后再使用"Alt+Enter"键即可调出下图选项:
序列化的原理是什么呢?
既然知道了序列化是怎么使用的,那么序列化的原理是怎么样的呢?
我们用上面的例子来作为探寻序列化原理的入口:
private static void serializeNY() throws IOException {
NY ny = new NY();
ny.setName("NY");
ny.setBlogName("NYfor2020");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\serialable.txt")));
oos.writeObject(ny);
System.out.println("NY 对象序列化成功!");
oos.close();
}
- 进入ObjectOutputStream的构造函数:
public ObjectOutputStream(OutputStream out) throws IOException {
//检查是否为ObjectOutputStream的实例
verifySubclass();
//bout是底层的数据字节容器
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
//写入序列化文件头
writeStreamHeader();
//设置文件缓存刷新配置
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
- 我们进入**writeStreamHeader()**方法:
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
- 这个方法是将序列化文件的魔数和版本写入序列化文件头:
/**
* Magic number that is written to the stream header.
*/
final static short STREAM_MAGIC = (short)0xaced;
/**
* Version number that is written to the stream header.
*/
final static short STREAM_VERSION = 5;
- 在**writeObject()**方法进行具体的序列化写入操作:
public final void writeObject(Object obj) throws IOException {
//表示使用writeObjectOverride()方法进行序列化写入,一般为不执行
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
//调用writeObject0()方法进行具体的序列化操作
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
- 进入**writeObject0()**方法:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
//此处对缓存刷新进行默认配置
boolean oldMode = bout.setBlockDataMode(false);
//递归深度
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
// check for replacement object
Object orig = obj;
//需要序列的对象的Class对象
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
//创建描述c1的ObjectStreamClass对象
desc = ObjectStreamClass.lookup(cl, true);
//判断是否有可替换的写方法,一般是没有的
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
//判断此对象是否可以被替换
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
// if object replaced, run through original checks a second time
//如果这个obj对象被替换了
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}
// remaining cases
//根据实际要写入的类型,进行不同的写入操作
//其中,String、Array、Enum类型是直接写入操作的
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) {
//实现序列化接口的对象都会执行下面的方法
//其实在这里就可以看出,Serializable只是一个标记接口,其本身并没有什么意义
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
//此层递归结束
depth--;
bout.setBlockDataMode(oldMode);
}
}
这一段代码中创建了ObjectStreamClass对象,并根据不同的对象类型来执行不同的写入操作。而在此例子中,对象对应的类实现了Serializable接口,所以下一步会执行writeOrdinaryObject()方法。
- **writeOrdinaryObject()**是当对象对应的类实现了Serializable接口的时才会被调用:
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
if (extendedDebugInfo) {
debugInfoStack.push(
(depth == 1 ? "root " : "") + "object (class \"" +
obj.getClass().getName() + "\", " + obj.toString() + ")");
}
try {
desc.checkSerialize();
//写入Object的标记位符号,表示这是一个新的Object对象
bout.writeByte(TC_OBJECT);
//将对类的描述写入
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//写入序列化对象具体的实例数据
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
- 接下来是将类的描述写入类元数据中的writeClassDesc():
private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
int handle;
if (desc == null) {
//如果desc为null,则写入null
writeNull();
} else if (!unshared && (handle = handles.lookup(desc)) != -1) {
writeHandle(handle);
} else if (desc.isProxy()) {
writeProxyDesc(desc, unshared);
} else {
writeNonProxyDesc(desc, unshared);
}
}
- 在desc为null时,会执行**writeNull()**方法:
private void writeNull() throws IOException {
bout.writeByte(TC_NULL);
}
/**
* Null object reference.
*/
final static byte TC_NULL = (byte)0x70;
可以看到,在writeNull()中,会将表示NULL的标识写入序列中。
- 那么如果desc不为null时,一般执行**writeNonProxyDesc()**方法:
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
//类元信息的标记位
//表示接下来的数据为Class描述符
bout.writeByte(TC_CLASSDESC);
handles.assign(unshared ? null : desc);
if (protocol == PROTOCOL_VERSION_1) {
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
//一般会执行此方法,将类描述写入
writeClassDescriptor(desc);
}
Class<?> cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
//根据cl的类型进行处理
annotateClass(cl);
bout.setBlockDataMode(false);
//表示对一个object的描述块的结束
bout.writeByte(TC_ENDBLOCKDATA);
‘
//此处会将对象相应的类的父类写入
writeClassDesc(desc.getSuperDesc(), false);
}
- 在上一个方法执行过程中,会执行**writeClassDescriptor()**方法将类的描述写入类元数据中:
protected void writeClassDescriptor(ObjectStreamClass desc)
throws IOException{
desc.writeNonProxy(this);
}
- 在这里我们可以看到,写入类元信息的方法调用了**writeNonProxy()**方法:
void writeNonProxy(ObjectOutputStream out) throws IOException {
//写入类名
out.writeUTF(name);
//写入serialVersionUID,看!这里显示了序列号的重要性
out.writeLong(getSerialVersionUID());
//类的标记
byte flags = 0;
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
int protocol = out.getProtocolVersion();
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
} else if (serializable) {
//一般程序会执行到这里,标识类执行序列化
// final static byte SC_SERIALIZABLE = 0x02;
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
// 自定义writeObject方法
// final static byte SC_WRITE_METHOD = 0x01;
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
//枚举标记
//final static byte SC_ENUM = 0x10;
flags |= ObjectStreamConstants.SC_ENUM;
}
//将标记写入类元信息中
out.writeByte(flags);
//写入对象的字段数量
out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
//写入字段类型对象的Code,类似标记
out.writeByte(f.getTypeCode());
//写入字段的名字
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
//如果是对象或接口,则会写入表示对象的字符串
out.writeTypeString(f.getTypeString());
}
}
}
这次方法中我们可以看到:
- 调用writeUTF()方法将对象所属类的名字写入。
- 调用writeLong()方法将类的序列号serialVersionUID写入。
- 判断被序列化对象所属类的流类型flag,写入底层字节容器中(占两个字节)。
- 写入对象中的所有字段,以及对应的属性
所以直到这个方法的执行,一个对象及其对应类的所有属性和属性值才被序列化。当上述流程完成之后,回到**writeOrdinaryObject()**方法中,继续往下运行:
private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared)
throws IOException{
...
writeClassDesc(desc, false);
//------------------------
//程序会在这里继续往下运行,已将对象的相关序列化数据写入流中了
//------------------------
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//将序列化对象的实例化数据写入
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
- 调用**writeSerialData()**方法将实例化数据写入:
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
//获取序列化对象的数据布局ClassDataSlot
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//如果序列化对象实现了自己的writeObject()方法,则进入if代码块,否则进入else代码块,执行默认的写入方法
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
//一般执行这个方法,默认的写入实例数据
defaultWriteFields(obj, slotDesc);
}
}
}
- 当执行到**defaultWriteFields()**方法时,会将实例数据写入:
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
//检查是否可以使用默认的序列化
desc.checkDefaultSerialize();
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
//获取对象中基本类型的实例数据,并将其放到primVals数组中
desc.getPrimFieldValues(obj, primVals);
//将对象基本类型的实例数据,写入底层的字节缓冲流
bout.write(primVals, 0, primDataSize, false);
//获取类对应的引用类型的字段对象
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
//对象的字段对象值
desc.getObjFieldValues(obj, objVals);
//将对应的对象类型字段保存到objVals数组中
for (int i = 0; i < objVals.length; i++) {
if (extendedDebugInfo) {
debugInfoStack.push(
"field (class \"" + desc.getName() + "\", name: \"" +
fields[numPrimFields + i].getName() + "\", type: \"" +
fields[numPrimFields + i].getType() + "\")");
}
try {
//对序列化对象中引用类型的字段,调用writeObject0()写入对应的数据
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
}
在执行完上述方法之后,程序将会回到writeNonProxyDesc()方法中,并且在writeClassDesc()中会将对象对应的类的父类信息进行写入:
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
...
//一般会执行此方法,将类描述写入
writeClassDescriptor(desc);
}
Class<?> cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
//根据cl的类型进行处理
annotateClass(cl);
bout.setBlockDataMode(false);
//表示对一个object的描述块的结束
bout.writeByte(TC_ENDBLOCKDATA);
‘
//此处会将对象相应的类的父类写入
writeClassDesc(desc.getSuperDesc(), false);
}
至此,我们可以知道,整个序列化的过程其实就是一个递归写入的过程。
将上面的过程进行简化,可以总结为这幅图:
transient关键字
在有些时候,我们并不想将一些敏感信息序列化,如密码等,这个时候就需要transient关键字来标注属性为非序列化属性。
transient关键字的使用
将上面的NY类中的name属性稍作修改:
private transient String name;
当我们再次运行SerializeNY类中的main()方法时,运行结果如下:
NY 对象序列化成功!
NY 对象反序列化成功
NY{name='null', blogName='NYfor2020'}
我们可以看到,name属性为null,说明反序列化时根本没有从文件中获取到信息。
transient关键字的特点
-
变量一旦被transient修饰,则不再是对象持久化的一部分了,而且变量内容在反序列化时也不能获得。
-
transient关键字只能修饰变量,而不能修饰方法和类,而且本地变量是不能被transient修饰的,如果变量是类变量,则需要该类也实现Serializable接口。
-
一个静态变量不管是否被transient修饰,都不会被序列化。
关于这一点,可能会有读者感到疑惑。举个栗子,如果用static修饰NY类中的name:
private static String name;
运行SerializeNY类中的main程序,可以看到运行结果:
NY 对象序列化成功! NY 对象反序列化成功 NY{name='NY', blogName='NYfor2020'}
嘶…这是翻车了吗?并没有,因为这里出现的name值是当前JVM中对应的static变量值,这个值是JVM中的而不是反序列化得出的。
不信?我们来改变一下SerializeNY类中的**serializeNY()**函数:
private static void serializeNY() throws IOException { NY ny = new NY(); ny.setName("NY"); ny.setBlogName("NYfor2020"); ny.setTest("12"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\serialable.txt"))); oos.writeObject(ny); System.out.println("NY 对象序列化成功!"); System.out.println(ny.toString()); oos.close(); ny.setName("hey, NY"); }
笔者在NY对象被序列化之后,改变了NY对象的name值。运行结果为:
NY 对象序列化成功! NY{name='NY', blogName='NYfor2020'} NY 对象反序列化成功 NY{name='hey, NY', blogName='NYfor2020'}
transient修饰的变量真的就不能被序列化了吗?
举个栗子:
public class ExternalizableTest implements Externalizable {
private transient String content = "即使被transient修饰,我也会序列化";
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
content = (String)in.readObject();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ExternalizableTest et = new ExternalizableTest();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\externalizable.txt")));
oos.writeObject(et);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\externalizable.txt")));
et = (ExternalizableTest) ois.readObject();
System.out.println(et.content);
oos.close();
ois.close();
}
}
运行结果为:
即使被transient修饰,我也会序列化
我们可以看到,content变量在被transient修饰的情况下,还是被序列化了。因为在Java中,对象序列化可以通过实现两种接口来实现:
- 如果实现的是Serializable接口,则所有信息(不包括被static、transient修饰的变量信息)的序列化将自动进行。
- 如果实现的是Externalizable接口,则不会进行自动序列化,需要开发者在writeExternal()方法中手工指定需要序列化的变量,与是否被transient修饰无关。
序列化注意事项
- 序列化对象必须实现序列化接口Serializable。
- 序列化对象中的属性如果也有对象的话,其对象需要实现序列化接口。
- 类的对象序列化后,类的序列号不能轻易更改,否则反序列化会失败。
- 类的对象序列化后,类的属性增加或删除不会影响序列化,只是值会丢失。
- 如果父类序列化,子类会继承父类的序列化;如果父类没序列化,子类序列化了,子类中的属性能正常序列化,但父类的属性会丢失,不能序列化。
- 用Java序列化的二进制字节数据只能由Java反序列化,如果要转换成其他语言反序列化,则需要先转换成Json/XML通用格式的数据。
- 如果某个字段不想序列化,在该字段前加上transient关键字即可。(咳咳,下一篇就是写这个了,敬请关注~)
结语
第一次写关于JDK实现原理的文章,还是觉得有点难度的,但是这对于源码分析能力还是有点提升的。在这个过程中最好多打断点,多调试。
如果本文对你的学习有帮助,请给一个赞吧,这会是我最大的动力~
参考资料:
Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解释
本文已授权发布在微信公众号:Java后端。