java 字节序列化_一文带你全面了解Java 序列化

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

什么是序列化和反序列化?为什么要序列化?怎么实现序列化?序列化的原理是什么呢?transient关键字序列化时应注意什么?如果你也有这些疑问,不妨看看本文?

1.什么是序列化和反序列化?

Java序列化是指把Java对象转换为字节序列的过程;Java反序列化是指把字节序列恢复为Java对象的过程;

2. 为什么要序列化?

其实我们的对象不只是存储在内存中,它还需要在传输网络中进行传输,并且保存起来之后下次再加载出来,这时候就需要序列化技术。

一般Java对象的生命周期比Java虚拟机端,而实际开发中如果需要JVM停止后能够继续持有对象,则需要用到序列化技术将对象持久化到磁盘或数据库。在多个项目进行RPC调用时,需要在网络上传输JavaBean对象,而网络上只允许二进制形式的数据进行传输,这时则需要用到序列化技术。Java的序列化技术就是把对象转换成一串由二进制字节组成的数组,然后将这二进制数据保存在磁盘或传输网络。而后需要用到这对象时,磁盘或者网络接收者可以通过反序列化得到此对象,达到对象持久化的目的。

3. 怎么实现序列化?

序列化的过程一般会是这样的:

将对象实例相关的类元数据输出递归地输出类的超类描述,直到没有超类类元数据输出之后,开始从最顶层的超类输出对象实例的实际数据值从上至下递归输出实例的数据所以,如果父类已经序列化了,子类继承之后也可以进行序列化。

实现第一步,则需要的先将对象实例相关的类标记为需要序列化。

实现序列化的要求:目标对象实现Serializable接口

我们先创建一个NY类,实现Serializable接口,并生成一个版本号:

public class NY implements Serializable {private static final long serialVersionUID = 8891488565683643643L;private String name;private String blogName;@Overridepublic 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

再执行一次反序列化方法,运行结果如下:

Exceptionin thread"main" java.io.InvalidClassException: NY; localclass incompatible:stream classdesc serialVersionUID =8891488565683643642, localclass serialVersionUID =8891488565683643643......

至于怎么让idea生成serialVersionUID,则需要在idea设置中改个配置即可:

1350ddb08a4ef5b14806ba184ffe0de9.png

之后再使用"Alt+Enter"键即可调出下图选项:

d453202ea3a48a23cc965004370f9411.png

序列化的原理是什么呢?

既然知道了序列化是怎么使用的,那么序列化的原理是怎么样的呢?

我们用上面的例子来作为探寻序列化原理的入口:

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(); }

1. 进入ObjectOutputStream的构造函数:

public ObjectOutputStream(OutputStreamout) throws IOException{verifySubclass(); 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);}

这个方法是将序列化文件的魔数和版本写入序列化文件头:

final static short STREAM_MAGIC = (short)0xaced;final static short STREAM_VERSION =5;

2. 在 writeObject() 方法进行具体的序列化写入操作:

public final void writeObject(Object obj) throws IOException{if (enableOverride) { writeObjectOverride(obj); return; } try { 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 { int h; if ((obj = subs.lookup(obj)) ==null) { writeNull(); return; }else if (!unshared && (h = handles.lookup(obj)) !=-1) { writeHandle(h); return; }else if (objinstanceof Class) { writeClass((Class) obj, unshared); return; }else if (objinstanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } Object orig = obj; Class> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { Class> repCl; 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 (obj != orig) { subs.assign(orig, obj); if (obj ==null) { writeNull(); return; }else if (!unshared && (h = handles.lookup(obj)) !=-1) { writeHandle(h); return; }else if (objinstanceof Class) { writeClass((Class) obj, unshared); return; }else if (objinstanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } } if (objinstanceof String) { writeString((String) obj, unshared); }else if (cl.isArray()) { writeArray(obj, desc, unshared); }else if (objinstanceof Enum) { writeEnum((Enum>) obj, desc, unshared); }else if (objinstanceof 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(); 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) { 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);}final static byte TC_NULL = (byte)0x70;

可以看到,在writeNull()中,会将表示NULL的标识写入序列中。

那么如果desc不为null时,一般执行writeNonProxyDesc()方法:

private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)throws IOException{bout.writeByte(TC_CLASSDESC);handles.assign(unshared ? null : desc);if (protocol == PROTOCOL_VERSION_1) {desc.writeNonProxy(this);} else {writeClassDescriptor(desc);}Class> cl = desc.forClass();bout.setBlockDataMode(true);if (cl != null && isCustomSubclass()) {ReflectUtil.checkPackageAccess(cl);}annotateClass(cl);bout.setBlockDataMode(false);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);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) {flags |= ObjectStreamConstants.SC_SERIALIZABLE;}if (hasWriteObjectData) {flags |= ObjectStreamConstants.SC_WRITE_METHOD;}if (isEnum) {flags |= ObjectStreamConstants.SC_ENUM;}out.writeByte(flags);out.writeShort(fields.length);for (int i = 0; i < fields.length; i++) {ObjectStreamField f = fields[i];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{ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();for (int i = 0; i < slots.length; i++) {ObjectStreamClass slotDesc = slots[i].desc;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]; } 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); 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(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); } annotateClass(cl); bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA);‘ writeClassDesc(desc.getSuperDesc(),false); }

至此,我们可以知道,整个序列化的过程其实就是一个递归写入的过程。

将上面的过程进行简化,可以总结为这幅图:

77f819045658286ba0e724d51e0d578c.png

transient关键字

在有些时候,我们并不想将一些敏感信息序列化,如密码等,这个时候就需要transient关键字来标注属性为非序列化属性。

transient关键字的使用

将上面的NY类中的name属性稍作修改:

private transientString name;

当我们再次运行SerializeNY类中的main()方法时,运行结果如下:

NY 对象序列化成功!NY 对象反序列化成功NY{name='null', blogName='NYfor2020'}

我们可以看到,name属性为null,说明反序列化时根本没有从文件中获取到信息。

ransient关键字的特点

变量一旦被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修饰,我也会序列化";@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(content);}@Overridepublic 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实现原理的文章,还是觉得有点难度的,但是这对于源码分析能力还是有点提升的。在这个过程中最好多打断点,多调试。

作者: NYfor2020,作者已通过

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值