今天聊聊 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;
  
    @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();
    }

1. 进入ObjectOutputStream的构造函数

public ObjectOutputStream(OutputStream out) 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 (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof 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 (obj instanceof Class) {
                    writeClass((Class) obj, unshared);
                    return;
                } else if (obj instanceof ObjectStreamClass) {
                    writeClassDesc((ObjectStreamClass) obj, unshared);
                    return;
                }
            }

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

这次方法中我们可以看到:

  1. 调用writeUTF()方法将对象所属类的名字写入。

  2. 调用writeLong()方法将类的序列号serialVersionUID写入。

  3. 判断被序列化对象所属类的流类型flag,写入底层字节容器中(占两个字节)。

  4. 写入对象中的所有字段,以及对应的属性

所以直到这个方法的执行,一个对象及其对应类的所有属性和属性值才被序列化。当上述流程完成之后,回到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);
    }

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

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

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实现原理的文章,还是觉得有点难度的,但是这对于源码分析能力还是有点提升的。在这个过程中最好多打断点,多调试。

参考资料:

序列化和反序列化

https://www.cnblogs.com/sinceret/p/10285807.html

序列化和反序列化的详解

https://blog.csdn.net/tree_ifconfig/article/details/82766587

Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解释

https://blog.csdn.net/u013870094/article/details/82765907

Java中序列化实现原理研究

https://blog.csdn.net/weixin_39723544/article/details/80527550

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

                


-END-

如果看到这里,说明你喜欢这篇文章,请转发、点赞。同时标星(置顶)本公众号可以第一时间接受到博文推送。

1. 41 道 Spring Boot 面试题,帮你整理好了!

2. Zookeeper 入门文章

3.  女程序员做了个梦。。。

4. Spring 和 Spring Boot 之间到底有啥区别?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值