你真的有好好了解过序列化吗:Java序列化实现的原理

365bet体育在线总站 📅 2025-08-28 09:38:14 👤 admin 👁️ 8505 ❤️ 934
你真的有好好了解过序列化吗:Java序列化实现的原理

前言

在开发过程中经常会对实体进行序列化,但其实我们只是在“只知其然,不知其所以然”的状态,很多时候会有这些问题:

什么是序列化和反序列化?为什么要序列化?

怎么实现序列化?

序列化的原理是什么呢?

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中序列化实现原理研究

关于Java序列化你应该知道的一切

本文已授权发布在微信公众号:Java后端。

相关推荐

程的繁体字
365bet最新备用

程的繁体字

📅 07-08 👁️ 6098
惠州籍运动员梁文博获世界杯亚军!教练赞他有三点过人之处|163
持续推进工业兴市科技强市 全面塑造高质量发展新优势