Java序列化与反序列化(二)源码分析

发布于:2022-12-18 ⋅ 阅读:(678) ⋅ 点赞:(0)

目录

1. Java 序列化接口

2. ObjectOutputStream 源码分析

2.1 ObjectOutputStream 数据结构

2.2 ObjectOutputStream 构造函数

2.3 序列化入口:writeObject

2.4 核心方法:writeObject0

2.5 序列化:writeOrdinaryObject

2.6 类信息序列化:writeClassDesc

2.7 类数据信息序列化:writeSerialData

3. ObjectOutputStream#BlockDataOutputStream

4. ObjectOutputStream#HandleTable

5. ObjectOutputStream#PutField


1. Java 序列化接口

Java 为了方便开发人员将 Java 对象进行序列化及反序列化提供了一套方便的 API 来支持。其中包括以下接口和类:

  • Serializable 和 Externalizable 序列化接口。Serializable 接口没有方法或字段,仅用于标识可序列化的语义,实际上 ObjectOutputStream#writeObject 时通过反射调用 writeObject 方法,如果没有自定义则调用默认的序列化方法。Externalizable 接口该接口中定义了两个扩展的抽象方法:writeExternal 与 readExternal。

  • DataOutput 和 ObjectOutput    DataOutput 提供了对 Java 基本类型 byte、short、int、long、float、double、char、boolean 八种基本类型,以及 String 的操作。ObjectOutput 则在 DataOutput 的基础上提供了对 Object 类型的操作,writeObject 最终还是调用 DataOutput 对基本类型的操作方法。

  • ObjectOutputStream         我们一般使用 ObjectOutputStream#writeObject 方法把一个对象进行持久化。ObjectInputStream#readObject 则从持久化存储中把对象读取出来。

  • ObjectStreamClass 和 ObjectStreamField       ObjectStreamClass 是类的序列化描述符,包含类描述信息,字段的描述信息和 serialVersionUID。可以使用 lookup 方法找到/创建在此 JVM 中加载的具体类的 ObjectStreamClass。而 ObjectStreamField 则保存字段的序列化描述符,包括字段名、字段值等。

2. ObjectOutputStream 源码分析

2.1 ObjectOutputStream 数据结构

private final BlockDataOutputStream bout;   // io流
private final HandleTable handles;          // 序列化对象句柄(编号)映射关系
private final ReplaceTable subs;            // 替换对象的映射关系

private final boolean enableOverride;       // true 则调用writeObjectOverride()来替代writeObject()
private boolean enableReplace;              // true 则调用replaceObject()

         bout 是下层输出流,两个表是用于记录已输出对象的缓存便于之前说的重复输出的时候输出上一个相同内容的位置。

        ObjectOutputStream 属性中不太好理解的是 handles 和 subs 这两个属性。HandleTable 从名称就知道这是一个轻量的 HashMap,保存序列化对象句柄(编号)映射关系,ReplaceTable 保存的是替换对象的映射关系。关于 handles 的作用,举个例子,我们知道 Java 序列化除了保存字段信息外,还保存有类信息,当同一个对象序列化两次时第二次只用保存第一次的编号,这样可以大大减少序列化的大小,剩余的变量用到了再作说明。

2.2 ObjectOutputStream 构造函数

public ObjectOutputStream(OutputStream out) throws IOException {
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    enableOverride = false;
    writeStreamHeader();
    bout.setBlockDataMode(true);
}

        ObjectOutputStream 构建时会创建 BlockDataOutputStream 序列化流 bout,handles 和 subs 大致作用上面提了一下,下面还会有说明。writeStreamHeader 方法是输出序列化流的头信息,用于文件校验,和 .class 文件头的魔数及版本作用一样。如果不需要的话,可以覆盖这个方法,什么也不做,Hadoop 默认的 Java 序列化就是这样做的。

protected void writeStreamHeader() throws IOException {
    bout.writeShort(STREAM_MAGIC);
    bout.writeShort(STREAM_VERSION);
}

2.3 序列化入口:writeObject

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {           // 默认为 flase,由子类复写
        writeObjectOverride(obj);   // 子类实现
        return;
    }
    try {
        writeObject0(obj, false);   // 最核心的方法
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}

总结: writeObject 将所有序列委托给了 writeObject0 完成,如果序列化出现异常调用 writeFatalException 方法。

depth 变量表示 writeObject0 调用的深度,比如序列化 A 对象时调用 writeObject 则 depth++,而 A 对象的字段又是一个对象,此时又会递归调用 writeObject 方法,当 writeObject 方法执行完成时 depth--。因而如果不出异常则 depth 最终会是 0,有异常则在 catch 模块时 depth 不为 0。

private void writeFatalException(IOException ex) throws IOException {
    clear();
    boolean oldMode = bout.setBlockDataMode(false);
    try {
        bout.writeByte(TC_EXCEPTION);   // 异常信息
        writeObject0(ex, false);
        clear();
    } finally {
        bout.setBlockDataMode(oldMode);
    }
}

2.4 核心方法:writeObject0

        writeObject0 比较复杂,大致可分为三个部分:一是判断需不需要序列化;二是判断是否替换了对象;三是终于可以序列化了。

private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // 1. 判断需不需要序列化
        // 2. 判断是否替换了对象
        // 3. 真正可以序列化了
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

下面我们就看一下这三步都做了些什么?

int h;
// 1. 替换后的对象为 null
if ((obj = subs.lookup(obj)) == null) {     
    writeNull();
    return;
// 2. handles存储的是已经序列化的对象句柄,如果找到了,直接写一个句柄就可以了
} else if (!unshared && (h = handles.lookup(obj)) != -1) {  
    writeHandle(h);
    return;
// 3. Class 对象
} else if (obj instanceof Class) {
    writeClass((Class) obj, unshared);
    return;
// 4. ObjectStreamClass 序列化类的描述信息
} else if (obj instanceof ObjectStreamClass) {
    writeClassDesc((ObjectStreamClass) obj, unshared);
    return;
}

总结1: Java 序列化保存了很多与数据无关的数据,如类信息。但 Java 本身也做了一些优化,如 handles 保存了类的句柄,这样重复的类就只用保存一个句柄就可以了。

Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
// 1. 如果要序列化的对象中有 writeReplace 方法,则递归检查最终要输出的对象
for (;;) {
    Class<?> repCl;
    desc = ObjectStreamClass.lookup(cl, true);
    // 如果要序列化的对象中有 writeReplace 方法,则递归检查最终要输出的对象
    if (!desc.hasWriteReplaceMethod() ||
        (obj = desc.invokeWriteReplace(obj)) == null ||
        (repCl = obj.getClass()) == cl) {
        break;
    }
    cl = repCl;
}
// 2. 子类重写 ObjectOutputStream#replaceObject 方法
if (enableReplace) {
    Object rep = replaceObject(obj);
    if (rep != obj && rep != null) {
        cl = rep.getClass();
        desc = ObjectStreamClass.lookup(cl, true);
    }
    obj = rep;
}

// 3. 既然要序列化的对象已经被替换了,此时就需要再次做判断,和步骤1类似
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;
    }
}

总结2: 其实就是做了一个拦截,有机会替换要序列化的对象。做了这么多,现在终于可以序列化对象了。

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 {
    throw new NotSerializableException(cl.getName());
}

总结3: 对于 String、Array、Enum 三类对象,序列时做了特殊的处理,实现了 Serializable 接口的普通对象则调用 writeOrdinaryObject 进行序列化,在这个方法中我们可以真正看到数据的序列化。不过在这个方法中我们可以看到如果没有实现 Serializable 接口会抛出 NotSerializableException 异常。

补充:writeObject 和 writeUnshared 区别(了解):

在这之前,我们先了解一下 unshared 这个参数的作用,writeObject 和 writeUnshared 的区别是:后者会重新申请内存空间,让其地址发生改变。看下面这个例子:

oos.writeObject(user1);
int length1 = baos.toByteArray().length;
oos.writeObject(user1);
int length2 = baos.toByteArray().length;
// 1. 同一个对象写两次,长度只增加了 5
Assert.assertEquals(5, length2 - length1);  

oos.writeUnshared(user1);
int length3 = baos.toByteArray().length;

// 2. length1=123; length2=128; length3=140。
//    第三个对象数据重新保存了一次,所以长度增加大于 5,也就是内存地址是非共享的
System.out.println(String.format("length1=%s; length2=%s; length3=%s", length1, length2, length3));

2.5 序列化:writeOrdinaryObject

// String 类型
private void writeString(String str, boolean unshared) throws IOException {
    handles.assign(unshared ? null : str);
    long utflen = bout.getUTFLength(str);
    if (utflen <= 0xFFFF) {     // 长度小于 0xFFFF(65506)
        bout.writeByte(TC_STRING);  // 类型
        bout.writeUTF(str, utflen); // 内容
    } else {                    // 长度大于 0xFFFF(65506)
        bout.writeByte(TC_LONGSTRING);
        bout.writeLongUTF(str, utflen);
    }
}

// Enum 类型
private void writeEnum(Enum<?> en, ObjectStreamClass desc, 
        boolean unshared) throws IOException {
    bout.writeByte(TC_ENUM);                // 1. 类型
    ObjectStreamClass sdesc = desc.getSuperDesc();  // 2. 类信息
    writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
    handles.assign(unshared ? null : en);
    writeString(en.name(), false);          // 3. 枚举类的名称
}

// 实现了 Serializable 接口的序列化
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc,
        boolean unshared) throws IOException {    
    desc.checkSerialize();

    bout.writeByte(TC_OBJECT);      // 1. 类型
    writeClassDesc(desc, false);    // 2. 类信息
    handles.assign(unshared ? null : obj);
    if (desc.isExternalizable() && !desc.isProxy()) {
        writeExternalData((Externalizable) obj);    // 3.1 实现 Externalizable 接口的类
    } else {
        writeSerialData(obj, desc); // 3.2 实现 Serializable 接口的类,数据序列化
    }
}

总结: 可以看到 Java 序列化保存了三部分的数据:一是类型信息序列化 bout.writeByte(TC_OBJECT);二是类信息序列化 writeClassDesc();三是类数据信息序列化 writeSerialData()。到这里终于可以看到 io 序列化流的操作了。

writeOrdinaryObject 这个方法主要是在 Externalizable 和 Serializable 的接口出现分支,如果实现了 Externalizable 接口并且类描述符非动态代理,则执行 writeExternalData,否则执行 writeSerialData。同时,这个方法会写类描述信息。

2.6 类信息序列化:writeClassDesc

// 递归调用 writeClassDesc 直到父类没有实现 Serializable,也就是说会保存父类的信息
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);
    }
}

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);             // 保存类信息,本质上也是调用 desc.writeNonProxy(this)
    }

    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);     // 递归调用
}

总结: 序列化时会先递归调用 writeClassDesc 方法,将实现 Serializable 接口的父类信息也会同时序列化。类信息都保存在 ObjectStreamClass 类中,同时也可以通过 ObjectStreamClass#getFields 获取所有要序列的字段信息 ObjectStreamField。

2.7 类数据信息序列化: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;
        // 1. 自定义 writeObject 方法
        if (slotDesc.hasWriteObjectMethod()) {
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;

            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);  // 调用自定义序列化 writeObject 方法
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
            }
            curPut = oldPut;
        // 2. 默认序列化
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

总结: writeSerialData 首先获取需要序列化的类(desc.getClassDataLayout()),遍历进行序列化。对于重写 writeObject 方法则通过反射调用该方法,否则使用默认的序列化方式。

private class Animal { ... }
private class Person extends Animal implements Serializable { ... }
private class User extends Person { ... }

上述情况下,User 序列化时通过 ObjectStreamClass#lookup(User.class) 获取其序列化类信息,getClassDataLayout 方法则获取要序列化的类 User 和 Person。关于 ObjectStreamClass 会在下面讲解。

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

    // 1. Java 原生类型序列化
    int primDataSize = desc.getPrimDataSize();      // 1.1 获取原生类型字段的长度
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    desc.getPrimFieldValues(obj, primVals);         // 1.2 获取原生类型字段的值
    bout.write(primVals, 0, primDataSize, false);   // 1.3 原生类型序列化

    // 2. Java 对象类型序列化,递归调用 writeObject0 方法
    ObjectStreamField[] fields = desc.getFields(false);     // 2.1 获取所有序列化的字段
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    desc.getObjFieldValues(obj, objVals);                   // 2.2 获取所有序列化字段的值
    for (int i = 0; i < objVals.length; i++) {              // 2.3 递归完成序列化
        writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());            
    }
}

总结: defaultWriteFields 原生类型直接序列化,而非原生类型则需要递归调用 writeObject0 来序列化。

3. ObjectOutputStream#BlockDataOutputStream

BlockDataOutputStream 是一个内部类,它继承了 OutputStream 并实现了 DataOutput 接口,缓冲输出流有两种模式:在默认模式下,输出数据和 DataOutputStream 使用同样模式;在块数据模式下,使用一个缓冲区来缓存数据到达最大长度或者手动刷新时将内容写入下层输入流,这点和 BufferedOutputStream 类似。不同之处在于,块模式在写数据之前,要先写入一个头部来表示当前块的长度。
        从内部变量和构造函数中可以看出,缓冲区的大小是固定且不可修改的,其中包含了一个下层输入流和一个数据输出流以及是否采用块模式的标识,在构造时默认不采用块数据模式。

    /** maximum data block length 最大数据块长度1K*/
        private static final int MAX_BLOCK_SIZE = 1024;
        /** maximum data block header length 最大数据块头部长度*/
        private static final int MAX_HEADER_SIZE = 5;
        /** (tunable) length of char buffer (for writing strings) 字符缓冲区的可变长度,用于写字符串*/
        private static final int CHAR_BUF_SIZE = 256;

        /** buffer for writing general/block data 用于写一般/块数据的缓冲区*/
        private final byte[] buf = new byte[MAX_BLOCK_SIZE];
        /** buffer for writing block data headers 用于写块数据头部的缓冲区*/
        private final byte[] hbuf = new byte[MAX_HEADER_SIZE];
        /** char buffer for fast string writes 用于写快速字符串的缓冲区*/
        private final char[] cbuf = new char[CHAR_BUF_SIZE];

        /** block data mode 块数据模式*/
        private boolean blkmode = false;
        /** current offset into buf buf中的当前偏移量*/
        private int pos = 0;

        /** underlying output stream 下层输出流*/
        private final OutputStream out;
        /** loopback stream (for data writes that span data blocks) 回路流用于写跨越数据块的数据*/
        private final DataOutputStream dout;

        /**
         * 在给定的下层流上创建一个BlockDataOutputStream,块数据模式默认关闭
         */
        BlockDataOutputStream(OutputStream out) {
            this.out = out;
            dout = new DataOutputStream(this);
        }

        setBlockDataMode可以改变当前的数据模式,从块数据模式切换到非块数据模式时,要讲缓冲区内的数据写入到下层输入流中。getBlockDataMode可以查询当前的数据模式。 

 /**
         * 设置块数据模式为给出的模式true是开启,false是关闭,并返回之前的模式值。
         * 如果新的模式和旧的一样,什么都不做。
         * 如果新的模式和旧的模式不同,所有的缓冲区数据要在转换到新模式之前刷新。
         */
        boolean setBlockDataMode(boolean mode) throws IOException {
            if (blkmode == mode) {
                return blkmode;
            }
            drain();//将缓冲区内的数据全部写入下层输入流
            blkmode = mode;
            return !blkmode;
        }

        /**
         * 当前流为块数据模式返回true,否则返回false
         */
        boolean getBlockDataMode() {
            return blkmode;
        }

        drain这个方法在多个方法中被调用,作用是将缓冲区内的数据全部写入下层输入流,但不会刷新下层输入流,在写入实际数据前要先用writeBlockHeader写入块头部,头部包含1字节标志位和1字节或4字节的长度大小。

   void drain() throws IOException {
            if (pos == 0) {
                return;//pos为0说明当前缓冲区为空
            }
            if (blkmode) {
                writeBlockHeader(pos);//块数据模式下要先写入头部
            }
            out.write(buf, 0, pos);//写入缓冲区数据
            pos = 0;//缓冲区被清空
        }

        /**
         * 写入块数据头部。数据块小于256字节会增加2字节头部前缀,其他会增加5字节头部。
         * 第一字节是标识长度范围,因为255字节以内可以用1字节来表示长度,4字节可以表示int范围内的最大整数
         */
        private void writeBlockHeader(int len) throws IOException {
            if (len <= 0xFF) {
                hbuf[0] = TC_BLOCKDATA;
                hbuf[1] = (byte) len;
                out.write(hbuf, 0, 2);
            } else {
                hbuf[0] = TC_BLOCKDATALONG;
                Bits.putInt(hbuf, 1, len);
                out.write(hbuf, 0, 5);
            }
        }

        下面的方法等价于他们在OutputStream中的对应方法,除了他们参与在块数据模式下写入数据到数据块中的部分有所不同。写入都需要先检查缓冲区有没有达到上限,达到时需要先刷新,然后再将数据复制到缓冲区。刷新和关闭操作都不难理解。 

        public void write(int b) throws IOException {
            if (pos >= MAX_BLOCK_SIZE) {
                drain();//达到块数据上限时,将缓冲区内的数据全部写入下层流
            }
            buf[pos++] = (byte) b;//存储b到buf中
        }

        public void write(byte[] b) throws IOException {
            write(b, 0, b.length, false);
        }

        public void write(byte[] b, int off, int len) throws IOException {
            write(b, off, len, false);
        }
        /**
         * 将指定的字节段从数组中写出。如果copy是true,复制值到一个中间缓冲区在将它们写入下层流之前,来避免暴露一个对原字节数组的引用
         */
        void write(byte[] b, int off, int len, boolean copy)
            throws IOException
        {
            if (!(copy || blkmode)) {// 非copy也非块数据模式直接写入下层输入流
                drain();
                out.write(b, off, len);
                return;
            }

            while (len > 0) {
                if (pos >= MAX_BLOCK_SIZE) {
                    drain();
                }
                if (len >= MAX_BLOCK_SIZE && !copy && pos == 0) {
                    // 长度大于缓冲区非copy模式且缓冲区为空直接写,避免不必要的复制
                    writeBlockHeader(MAX_BLOCK_SIZE);
                    out.write(b, off, MAX_BLOCK_SIZE);
                    off += MAX_BLOCK_SIZE;
                    len -= MAX_BLOCK_SIZE;
                } else {
                    //剩余内容在缓冲区内放得下或者缓冲区不为空或者是copy模式,则将数据复制到缓冲区
                    int wlen = Math.min(len, MAX_BLOCK_SIZE - pos);
                    System.arraycopy(b, off, buf, pos, wlen);
                    pos += wlen;
                    off += wlen;
                    len -= wlen;
                }
            }
        }
        
        /**
         * 将缓冲区数据刷新到下层流,同时会刷新下层流
         */
        public void flush() throws IOException {
            drain();
            out.flush();
        }

        /**
         * 刷新之后关闭下层输出流
         */
        public void close() throws IOException {
            flush();
            out.close();
        }

         上面的方法等价于他们在DataOutputStream中的对应方法,除了他们参与在块数据模式下写入数据到数据块中部分有所不同。基本上逻辑都是先检查空间是否足够,不足的话先刷新缓冲区,然后将数据存储到缓冲区中。

4. ObjectOutputStream#HandleTable

        HandleTable 也是一个内部类,这是一个轻量的 hash 表,它的作用是缓存写过的共享 class 便于下次查找,内部含有 3 个数组,spine、next 和 objs。objs 存储的是对象也就是 class,spine 是 hash 桶,next 是冲突链表,每有一个新的元素插入需要计算它的 hash 值然后用 spine 的大小取模,找到它的链表,新对象会被插入到链表的头部,它在 objs 和 next 中对应的数据是根据加入的序号顺序存储,spine 存储它的 handle 值也就是在另外两个数组中的下标。

// HandleTable 保存对象及其句柄的映射关系
private static class HandleTable {
    private int[] spine;    // 1. maps hash value -> candidate handle value
    private int[] next;     // 2. maps handle value -> next candidate handle value
    private Object[] objs;  // 3. maps handle value -> associated object

    // 插入对象
    int assign(Object obj) {
        // 省略扩容代码 ...
        insert(obj, size);
        return size++;
    }
    private void insert(Object obj, int handle) {
        int index = hash(obj) % spine.length;
        objs[handle] = obj;             // 对象表,通过 `句柄 -> 对象` 查找
        next[handle] = spine[index];    // 冲突链表,保存上一个冲突的 hash 对应的 handle
        spine[index] = handle;          // hash桶表,保存当前 hash 对应的 handle
    }

    // 查找对象的句柄
    int lookup(Object obj) {
        if (size == 0) {
            return -1;
        }
        int index = hash(obj) % spine.length;
        for (int i = spine[index]; i >= 0; i = next[i]) {
            if (objs[i] == obj) {   // 查找时完成相等,非 equals 方法
                return i;
            }
        }
        return -1;
    }
}

总结: HandleTable 保存对象及其句柄的映射关系,如果对象已经序列化了,则在 HandleTable#lookup 返回的结果就不是 -1,此时只用保存对象的句柄就可以了,不需要重新保存一次类的信息,减小了序列化后的大小。

ReplaceTable 使用的是 HandleTable,表示可替换对象的关系表,和 HandleTable 功能类似,也是为了避免重复序列化。

private static class ReplaceTable {
    private final HandleTable htab; // 1. maps object -> index
    private Object[] reps;          // 2. maps index -> replacement object

    // 插入和查找
    void assign(Object obj, Object rep) {
        int index = htab.assign(obj);
        reps[index] = rep;
    }
    Object lookup(Object obj) {
        int index = htab.lookup(obj);
        return (index >= 0) ? reps[index] : obj;
    }
}

5. ObjectOutputStream#PutField

PutField 也是一个内部类,可以通过它动态修改序列化的字段。

// 自定义序列化规则,调用 writeFields 进行序列化
private void writeObject(ObjectOutputStream out) throws Exception {
    ObjectOutputStream.PutField putFields = out.putFields();
    putFields.put("password", password + "-1");
    out.writeFields();  // 这个方法只是调用了 putFields#writeFields
}

总结: put 方法修改内容后,调用 writeFields 进行序列化。我们看一下 PutField 这个类的实现。

// Bits 是一个工具类,将 java 原生类型写入指定的 buffer 中
public void put(String name, int val) {
    Bits.putInt(primVals, getFieldOffset(name, Integer.TYPE), val);
}
// 直接替换了原对象
public void put(String name, Object val) {
    objVals[getFieldOffset(name, Object.class)] = val;
}

void writeFields() throws IOException {
    // 1. 原生类型序列化
    bout.write(primVals, 0, primVals.length, false);

    // 2. 非原生类型序列化
    ObjectStreamField[] fields = desc.getFields(false);
    int numPrimFields = fields.length - objVals.length;
    for (int i = 0; i < objVals.length; i++) {
        writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
    }
}

总结: PutField 通过 put 方法修改属性后,还是调用 writeObject0 进行了对象的序列化。

参考:

                  Java 序列化和反序列化(二)Serializable 源码分析 - 1 - binarylei - 博客园 (cnblogs.com)

                  Java序列化 ObjectOutputStream源码解析-阿里云开发者社区 (aliyun.com)                

本文含有隐藏内容,请 开通VIP 后查看