在 Java 编程中,序列化是一个非常重要的概念,它允许我们将对象转换为字节流,以便于存储或传输。反序列化则是将字节流转换回对象的过程。尽管我们通常只需要让类实现 Serializable
接口即可实现序列化,但深入了解其背后的机制和细节,对于提升我们的编程技能和理解 Java 的内部工作原理非常有帮助。
1 理论基础
Java 序列化是在 JDK 1.1 中引入的特性,用于将 Java 对象转换为字节数组,便于存储或传输。反序列化则是将字节数组转换回 Java 对象的过程。
序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。
要序列化的对象必须实现 Serializable
接口,否则会抛出 NotSerializableException
异常。
Serializable 接口定义
publicinterfaceSerializable{ }
Serializable
接口是一个空接口,没有任何方法。它的作用仅仅是作为一个标识,告诉 Java 虚拟机该类的对象是可以被序列化的。
2 实战演练
首先,我们创建一个简单的类 Wanger
,包含两个字段 name
和 age
,并实现 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 +"}";}}
接下来,我们创建一个测试类,通过 ObjectOutputStream
将 Wanger
对象序列化到文件中,再通过 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++){ try{ writeObject0(objVals[i],fields[numPrimFields +i].isUnshared());}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];}bin.readFully(primVals,0,primDataSize,false);if(obj !=null){ 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++){ ObjectStreamFieldf =fields[numPrimFields +i];objVals[i]=readObject0(Object.class,f.isUnshared());if(f.getField()!=null){ handles.markDependency(objHandle,passHandle);}}if(obj !=null){ 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();}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}
从结果中可以看出:
static
字段 pre
在反序列化后保持了修改后的值,说明 static
字段不会被序列化。transient
字段 meizi
在反序列化后为 null
,说明 transient
字段也不会被序列化。
4.2 源码探究
如果想要深究源码的话,你可以在 ObjectStreamClass 中发现下面这样的代码:
privatestaticObjectStreamField[]getDefaultSerialFields(Class<?>cl){ Field[]clFields =cl.getDeclaredFields();ArrayList<ObjectStreamField>list =newArrayList<>();intmask =Modifier.STATIC |Modifier.TRANSIENT;for(inti =0;i <clFields.length;i++){ Fieldfield =clFields[i];intmods =field.getModifiers();if((mods &mask)==0){ ObjectStreamFieldosf =newObjectStreamField(field.getName(),field.getType(),!Serializable.class.isAssignableFrom(cl));list.add(osf);}}intsize =list.size();return(size ==0)?NO_FIELDS :list.toArray(newObjectStreamField[size]);}
其中,Modifier.STATIC | Modifier.TRANSIENT
表示这两个修饰符标记的字段就没有被放入到序列化的字段中。
5 Externalizable 接口
除了 Serializable
接口,Java 还提供了 Externalizable
接口,它提供了更高的序列化控制能力。
Externalizable
接口的类需要手动实现writeExternal
和 readExternal
方法,以便在序列化和反序列化过程中对对象进行自定义的处理。
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 区别与使用场景
无参构造方法
Serializable
:不需要无参构造方法。
Externalizable
:必须提供一个无参构造方法。在反序列化时,会调用该无参构造方法创建对象,然后再将被保存对象的字段值复制过去。
方法实现
Serializable
:不需要实现任何方法,序列化和反序列化过程由 Java 自动处理。
Externalizable
:需要实现 writeExternal 和 readExternal 方法,手动控制序列化和反序列化的过程。
控制能力
Serializable
:自动进行序列化和反序列化,无法对序列化过程进行细粒度控制。
Externalizable
:提供了更高的控制能力,可以在序列化和反序列化过程中对对象进行自定义处理,如对敏感信息进行加密和解密。
性能
Serializable
:由于是自动进行的,性能相对较低。
Externalizable
:由于是手动控制的,性能相对较高,但需要更多的编码工作。
6 serialVersionUID 的作用及选择
6.1 serialVersionUID
的作用
serialVersionUID
被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID
与被序列化类中的 serialVersionUID
进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。
6.2 生成 serialVersionUID
的方式
当一个类实现了 Serializable
接口后,IDE 通常会提醒该类最好产生一个序列化 ID。生成 serialVersionUID
的方式有以下几种:
默认版本的序列化 ID:
privatestaticfinallongserialVersionUID =1L;
随机生成的不重复的序列化 ID:
privatestaticfinallongserialVersionUID =-2095916884810199532L;
使用 @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 修改一下。
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
的方式有三种:
- 默认版本的序列化 ID:
private static final long serialVersionUID = 1L;
- 随机生成的不重复的序列化 ID:
private static final long serialVersionUID = -2095916884810199532L;
- 使用
@SuppressWarnings("serial")
注解:@SuppressWarnings("serial")
如果没有特殊需求,推荐使用默认的序列化 ID(1L),这样可以确保代码一致时反序列化成功。
7 总结
通过深入研究 Java 序列化机制,我们不仅理解了 Serializable
接口的作用,还掌握了 Externalizable
接口的使用方法,以及 serialVersionUID
的重要性。这些知识将帮助我们在实际开发中更好地处理对象的序列化和反序列化问题,提升代码的健壮性和可维护性。
8 思维导图

9 参考链接
Java Serializable 接口:明明就一个空的接口嘛