清华主页 - 清华新闻 - 综合时讯 - 正文

Java 序列化:深入探索 Serializable 接口

在 Java 编程中,序列化是一个非常重要的概念,它允许我们将对象转换为字节流,以便于存储或传输。反序列化则是将字节流转换回对象的过程。尽管我们通常只需要让类实现 Serializable接口即可实现序列化,但深入了解其背后的机制和细节,对于提升我们的编程技能和理解 Java 的内部工作原理非常有帮助。

1 理论基础

Java 序列化是在 JDK 1.1 中引入的特性,用于将 Java 对象转换为字节数组,便于存储或传输。反序列化则是将字节数组转换回 Java 对象的过程。

序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。

要序列化的对象必须实现 Serializable接口,否则会抛出 NotSerializableException异常。

Serializable 接口定义

publicinterfaceSerializable{ }

Serializable接口是一个空接口,没有任何方法。它的作用仅仅是作为一个标识,告诉 Java 虚拟机该类的对象是可以被序列化的。

2 实战演练

首先,我们创建一个简单的类 Wanger,包含两个字段 nameage,并实现 Serializable接口。

classWangerimplementsSerializable{ privatestaticfinallongserialVersionUID =-2095916884810199532L;privateStringname;privateintage;publicStringgetName(){ returnname;}publicvoidsetName(Stringname){ this.name =name;}publicintgetAge(){ returnage;}publicvoidsetAge(intage){ this.age =age;}@OverridepublicStringtoString(){ return"Wanger{ "+"name="+name +",age="+age +"}";}}

接下来,我们创建一个测试类,通过 ObjectOutputStreamWanger对象序列化到文件中,再通过 ObjectInputStream从文件中反序列化对象。

publicclassTest{ publicstaticvoidmain(String[]args){ // 初始化Wangerwanger =newWanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try(ObjectOutputStreamoos =newObjectOutputStream(newFileOutputStream("chenmo"));){ oos.writeObject(wanger);}catch(IOExceptione){ e.printStackTrace();}// 从文件中读出对象try(ObjectInputStreamois =newObjectInputStream(newFileInputStream(newFile("chenmo")));){ Wangerwanger1 =(Wanger)ois.readObject();System.out.println(wanger1);}catch(IOException|ClassNotFoundExceptione){ e.printStackTrace();}}}

3 序列化与反序列化的内部机制

ObjectOutputStream在序列化对象时,会依次调用 writeObject()writeObject0()writeOrdinaryObject()writeSerialData()invokeWriteObject()defaultWriteFields()

ObjectInputStream在反序列化对象时,会依次调用 readObject()readObject0()readOrdinaryObject()readSerialData()defaultReadFields()

3.1 defaultWriteFields() 方法

privatevoiddefaultWriteFields(Objectobj,ObjectStreamClassdesc)throwsIOException{ // 获取对象的类,并检查是否可以进行默认的序列化Class<?>cl =desc.forClass();desc.checkDefaultSerialize();// 获取对象的基本类型字段的数量,以及这些字段的值intprimDataSize =desc.getPrimDataSize();desc.getPrimFieldValues(obj,primVals);// 将基本类型字段的值写入输出流bout.write(primVals,0,primDataSize,false);// 获取对象的非基本类型字段的值ObjectStreamField[]fields =desc.getFields(false);Object[]objVals =newObject[desc.getNumObjFields()];intnumPrimFields =fields.length -objVals.length;desc.getObjFieldValues(obj,objVals);// 循环写入对象的非基本类型字段的值for(inti =0;i <objVals.length;i++){ // 调用 writeObject0 方法将对象的非基本类型字段序列化写入输出流try{ writeObject0(objVals[i],fields[numPrimFields +i].isUnshared());}// 如果在写入过程中出现异常,则将异常包装成 IOException 抛出catch(IOExceptionex){ if(abortIOException ==null){ abortIOException =ex;}}}}

3.2 defaultReadFields() 方法

privatevoiddefaultReadFields(Objectobj,ObjectStreamClassdesc)throwsIOException{ // 获取对象的类,并检查对象是否属于该类Class<?>cl =desc.forClass();if(cl !=null&&obj !=null&&!cl.isInstance(obj)){ thrownewClassCastException();}// 获取对象的基本类型字段的数量和值intprimDataSize =desc.getPrimDataSize();if(primVals ==null||primVals.length <primDataSize){ primVals =newbyte[primDataSize];}// 从输入流中读取基本类型字段的值,并存储在 primVals 数组中bin.readFully(primVals,0,primDataSize,false);if(obj !=null){ // 将 primVals 数组中的基本类型字段的值设置到对象的相应字段中desc.setPrimFieldValues(obj,primVals);}// 获取对象的非基本类型字段的数量和值intobjHandle =passHandle;ObjectStreamField[]fields =desc.getFields(false);Object[]objVals =newObject[desc.getNumObjFields()];intnumPrimFields =fields.length -objVals.length;// 循环读取对象的非基本类型字段的值for(inti =0;i <objVals.length;i++){ // 调用 readObject0 方法读取对象的非基本类型字段的值ObjectStreamFieldf =fields[numPrimFields +i];objVals[i]=readObject0(Object.class,f.isUnshared());// 如果该字段是一个引用字段,则将其标记为依赖该对象if(f.getField()!=null){ handles.markDependency(objHandle,passHandle);}}if(obj !=null){ // 将 objVals 数组中的非基本类型字段的值设置到对象的相应字段中desc.setObjFieldValues(obj,objVals);}passHandle =objHandle;}

4 注意事项

  • static 和 transient 修饰的字段不会被序列化

4.1 代码示例

classWangerimplementsSerializable{ privatestaticfinallongserialVersionUID =-2095916884810199532L;privateStringname;privateintage;publicstaticStringpre ="沉默";transientStringmeizi ="王三";@OverridepublicStringtoString(){ return"Wanger{ "+"name="+name +",age="+age +",pre="+pre +",meizi="+meizi +"}";}}
publicclassTest{ publicstaticvoidmain(String[]args){ // 初始化Wangerwanger =newWanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try(ObjectOutputStreamoos =newObjectOutputStream(newFileOutputStream("chenmo"));){ oos.writeObject(wanger);}catch(IOExceptione){ e.printStackTrace();}// 改变 static 字段的值Wanger.pre ="不沉默";// 从文件中读出对象try(ObjectInputStreamois =newObjectInputStream(newFileInputStream(newFile("chenmo")));){ Wangerwanger1 =(Wanger)ois.readObject();System.out.println(wanger1);}catch(IOException|ClassNotFoundExceptione){ e.printStackTrace();}}}

输出结果:

Wanger{ name=王二,age=18,pre=沉默,meizi=王三}Wanger{ name=王二,age=18,pre=不沉默,meizi=null}

从结果中可以看出:

  1. static字段 pre在反序列化后保持了修改后的值,说明 static字段不会被序列化。
  2. transient字段 meizi在反序列化后为 null,说明 transient字段也不会被序列化。

4.2 源码探究

如果想要深究源码的话,你可以在 ObjectStreamClass 中发现下面这样的代码:

privatestaticObjectStreamField[]getDefaultSerialFields(Class<?>cl){ // 获取该类中声明的所有字段Field[]clFields =cl.getDeclaredFields();ArrayList<ObjectStreamField>list =newArrayList<>();intmask =Modifier.STATIC |Modifier.TRANSIENT;// 遍历所有字段,将非 static 和 transient 的字段添加到 list 中for(inti =0;i <clFields.length;i++){ Fieldfield =clFields[i];intmods =field.getModifiers();if((mods &mask)==0){ // 根据字段名、字段类型和字段是否可序列化创建一个 ObjectStreamField 对象ObjectStreamFieldosf =newObjectStreamField(field.getName(),field.getType(),!Serializable.class.isAssignableFrom(cl));list.add(osf);}}intsize =list.size();// 如果 list 为空,则返回一个空的 ObjectStreamField 数组,否则将 list 转换为 ObjectStreamField 数组并返回return(size ==0)?NO_FIELDS :list.toArray(newObjectStreamField[size]);}

其中,Modifier.STATIC | Modifier.TRANSIENT表示这两个修饰符标记的字段就没有被放入到序列化的字段中。

5 Externalizable 接口

除了 Serializable接口,Java 还提供了 Externalizable接口,它提供了更高的序列化控制能力。

Externalizable接口的类需要手动实现writeExternalreadExternal方法,以便在序列化和反序列化过程中对对象进行自定义的处理。

5.1 代码示例

classWangerimplementsExternalizable{ privateStringname;privateintage;publicWanger(){ // 必须提供一个无参构造方法}publicStringgetName(){ returnname;}publicvoidsetName(Stringname){ this.name =name;}publicintgetAge(){ returnage;}publicvoidsetAge(intage){ this.age =age;}@OverridepublicStringtoString(){ return"Wanger{ "+"name="+name +",age="+age +"}";}@OverridepublicvoidwriteExternal(ObjectOutputout)throwsIOException{ out.writeObject(name);out.writeInt(age);}@OverridepublicvoidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException{ name =(String)in.readObject();age =in.readInt();}}
publicclassTest{ publicstaticvoidmain(String[]args){ // 初始化Wangerwanger =newWanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try(ObjectOutputStreamoos =newObjectOutputStream(newFileOutputStream("chenmo"));){ oos.writeObject(wanger);}catch(IOExceptione){ e.printStackTrace();}// 从文件中读出对象try(ObjectInputStreamois =newObjectInputStream(newFileInputStream(newFile("chenmo")));){ Wangerwanger1 =(Wanger)ois.readObject();System.out.println(wanger1);}catch(IOException|ClassNotFoundExceptione){ e.printStackTrace();}}}

输出结果:

Wanger{ name=王二,age=18}Wanger{ name=王二,age=18}

5.2 区别与使用场景

  1. 无参构造方法

    Serializable:不需要无参构造方法。

    Externalizable:必须提供一个无参构造方法。在反序列化时,会调用该无参构造方法创建对象,然后再将被保存对象的字段值复制过去。

  2. 方法实现

    Serializable:不需要实现任何方法,序列化和反序列化过程由 Java 自动处理。

    Externalizable:需要实现 writeExternal 和 readExternal 方法,手动控制序列化和反序列化的过程。

  3. 控制能力

    Serializable:自动进行序列化和反序列化,无法对序列化过程进行细粒度控制。

    Externalizable:提供了更高的控制能力,可以在序列化和反序列化过程中对对象进行自定义处理,如对敏感信息进行加密和解密。

  4. 性能

    Serializable:由于是自动进行的,性能相对较低。

    Externalizable:由于是手动控制的,性能相对较高,但需要更多的编码工作。

6 serialVersionUID 的作用及选择

6.1 serialVersionUID的作用

serialVersionUID被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID与被序列化类中的 serialVersionUID进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。

6.2 生成 serialVersionUID的方式

当一个类实现了 Serializable接口后,IDE 通常会提醒该类最好产生一个序列化 ID。生成 serialVersionUID的方式有以下几种:

  1. 默认版本的序列化 ID

    privatestaticfinallongserialVersionUID =1L;
  2. 随机生成的不重复的序列化 ID

    privatestaticfinallongserialVersionUID =-2095916884810199532L;
  3. 使用 @SuppressWarnings("serial")注解

    @SuppressWarnings("serial")

6.3 选择 serialVersionUID的方式

6.3.1 使用随机生成的序列化 ID

首先,我们采用第二种办法,在被序列化类中添加一个随机生成的序列化 ID。

classWangerimplementsSerializable{ privatestaticfinallongserialVersionUID =-2095916884810199532L;privateStringname;privateintage;// 其他代码忽略}

然后,序列化一个 Wanger对象到文件中。

// 初始化Wangerwanger =newWanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try(ObjectOutputStreamoos =newObjectOutputStream(newFileOutputStream("chenmo"));){ oos.writeObject(wanger);}catch(IOExceptione){ e.printStackTrace();}

这时候,我们悄悄地把 Wanger类的序列化 ID 修改一下。

// private static final long serialVersionUID = -2095916884810199532L;privatestaticfinallongserialVersionUID =-2095916884810199533L;

准备反序列化。

try(ObjectInputStreamois =newObjectInputStream(newFileInputStream(newFile("chenmo")));){ Wangerwanger =(Wanger)ois.readObject();System.out.println(wanger);}catch(IOException|ClassNotFoundExceptione){ e.printStackTrace();}

结果会抛出异常:

java.io.InvalidClassException:local classincompatible:stream classdesc serialVersionUID =-2095916884810199532,local classserialVersionUID =-2095916884810199533

异常堆栈信息告诉我们,从持久化文件里面读取到的序列化 ID 和本地的序列化 ID 不一致,无法反序列化。

6.3.2 使用 @SuppressWarnings("serial")注解

假如我们采用第三种方法,为 Wanger类添加 @SuppressWarnings("serial")注解。

@SuppressWarnings("serial")classWangerimplementsSerializable{ // 省略其他代码}

再来一次反序列化,结果依然报错。

java.io.InvalidClassException:local classincompatible:stream classdesc serialVersionUID =-2095916884810199532,local classserialVersionUID =-3818877437117647968

异常堆栈信息告诉我们,本地的序列化 ID 为 -3818877437117647968,和持久化文件里面读取到的序列化 ID 仍然不一致,无法反序列化。这说明使用 @SuppressWarnings("serial")注解时,该注解会为被序列化类自动生成一个随机的序列化 ID。

6.3.3 使用默认的序列化 ID

如果没有特殊需求,采用默认的序列化 ID(1L)就可以,这样可以确保代码一致时反序列化成功。

classWangerimplementsSerializable{ privatestaticfinallongserialVersionUID =1L;// 省略其他代码}

6.4 小结

serialVersionUID是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会比较字节流中的 serialVersionUID和类中的 serialVersionUID,如果不一致,则会抛出 InvalidClassException

生成 serialVersionUID的方式有三种:

  1. 默认版本的序列化 IDprivate static final long serialVersionUID = 1L;
  2. 随机生成的不重复的序列化 IDprivate static final long serialVersionUID = -2095916884810199532L;
  3. 使用 @SuppressWarnings("serial")注解@SuppressWarnings("serial")

如果没有特殊需求,推荐使用默认的序列化 ID(1L),这样可以确保代码一致时反序列化成功。

7 总结

通过深入研究 Java 序列化机制,我们不仅理解了 Serializable接口的作用,还掌握了 Externalizable接口的使用方法,以及 serialVersionUID的重要性。这些知识将帮助我们在实际开发中更好地处理对象的序列化和反序列化问题,提升代码的健壮性和可维护性。

8 思维导图

在这里插入图片描述

9 参考链接

Java Serializable 接口:明明就一个空的接口嘛

2025-06-24 12:07:43

相关新闻

清华大学新闻中心版权所有,清华大学新闻网编辑部维护,电子信箱: news@tsinghua.edu.cn
Copyright 2001-2020 news.tsinghua.edu.cn. All rights reserved.