天天看點

Java序列化 ObjectOutputStream源碼解析概述ObjectOutputStream

概述

衆所周知,Java原生的序列化方法可以分為兩種:

  1. 實作Serializable接口
  2. 實作Externalizable接口

其實還有一種,可以完全自己實作轉為二進制内容,用Unsafe寫到記憶體裡面,然後寫入檔案

Serializable

可以使用ObjectStream預設實作的writeObject和readObject方法并且可以通過transit關鍵字來使得變量不被序列化,開發簡單

除了輸出協定和包名類名外,會額外輸出類的變量資訊

有緩存機制,對于重複對象會直接輸出所在位置,是以類較大且重複内容多時反而效率高,但會消耗額外記憶體空間

如果父類沒有無參構造函數則不會序列化父類

Externalizable

必須完全由自己來實作序列化規則是以可以直接控制哪些變量需要序列化,是以開發工作量較大

可以自己決定輸出内容,隻會固定輸出協定和包名類名,較為簡潔,對于小對象的序列化Externalizable會快一些

必須有無參構造函數否則編譯會出錯

​ 但是,普遍實際項目開發中對于原生序列化的使用非常少,我覺得這裡面的主要原因還是出在原生的對象流本身設計上一些是否安全的判斷過多,加上緩沖區本身大小隻有1K有點小,很明顯一個16K的對象一次寫入硬碟是比1K*16次快很多。尤其是大多數情況下重複對象判斷就是在浪費時間,比如一個網站的一條使用者資訊,根本不會有幾個重複字段。是以在很多網上的性能測試案例中,Serializable

​ 因為對象流篇幅過長,加上很多内容是系統安全或者是分隔符标志之類的東西,下面就隻挑重點來說。

ObjectOutputStream

先看一眼内部變量一大堆,光看注釋根本不知道是幹嗎用的。大緻分類一下,内部類Caches用于安全審計緩存。一面一塊是用于輸出的部分,bout是下層輸出流,兩個表是用于記錄已輸出對象的緩存便于之前說的重複輸出的時候輸出上一個相同内容的位置。接下來兩個是writeObject()/writeExternal()上行調用記錄上下文用的。debugInfoStack用于存儲錯誤資訊。

private static class Caches {
        /** cache of subclass security audit results 子類安全審計結果緩存*/
        static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits =
            new ConcurrentHashMap<>();

        /** queue for WeakReferences to audited subclasses 對審計子類弱引用的隊列*/
        static final ReferenceQueue<Class<?>> subclassAuditsQueue =
            new ReferenceQueue<>();
    }

    /** filter stream for handling block data conversion 解決塊資料轉換的過濾流*/
    private final BlockDataOutputStream bout;
    /** obj -> wire handle map obj->線性句柄映射*/
    private final HandleTable handles;
    /** obj -> replacement obj map obj->替代obj映射*/
    private final ReplaceTable subs;
    /** stream protocol version 流協定版本*/
    private int protocol = PROTOCOL_VERSION_2;
    /** recursion depth 遞歸深度*/
    private int depth;

    /** buffer for writing primitive field values 寫基本資料類型字段值緩沖區*/
    private byte[] primVals;

    /** if true, invoke writeObjectOverride() instead of writeObject() 如果為true,調用writeObjectOverride()來替代writeObject()*/
    private final boolean enableOverride;
    /** if true, invoke replaceObject() 如果為true,調用replaceObject()*/
    private boolean enableReplace;

    //下面的值隻在上行調用writeObject()/writeExternal()時有效
    /**
     * 上行調用類定義的writeObject方法時的上下文,持有目前被序列化的對象和目前對象描述符。在非writeObject上行調用時為null
     */
    private SerialCallbackContext curContext;
    /** current PutField object 目前PutField對象*/
    private PutFieldImpl curPut;

    /** custom storage for debug trace info 正常存儲用于debug追蹤資訊*/
    private final DebugTraceInfoStack debugInfoStack;           

構造函數有兩個,第一個是自身的構造需要提供一個輸出流,第二個實際上是提供給子類用的,建立一個自身相關内部變量全為空的對象輸出流。但是,兩個構造器都會進行安全檢查,檢查序列化的類是否重寫了安全敏感方法,如果違反了規則會抛出異常。正常的構造類還會直接輸出頭部資訊,包括對象輸出流的魔數和協定版本資訊,是以即使隻建立一個對象輸出流就會輸出頭部資訊。

/**
     * 建立一個ObjectOutputStream寫到指定的OutputStream。這個構造器寫序列化流頭部到下層流中,
     * 調用者可能希望立即重新整理流來確定接收的ObjectInputStreams構造器不會再讀取頭部時阻塞。
     * 如果一個安全管理器被安裝,這個構造器将會在被直接調用和被子類的構造器間接調用時檢查enableSubclassImplementation序列化許可,
     * 如果這個子類重寫了ObjectOutputStream.putFields或者ObjectOutputStream.writeUnshared方法
     */
    public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        bout = new BlockDataOutputStream(out);//通過下層流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;
        }
    }

    /**
     * 給子類提供一個路徑完全重新實作ObjectOutputStream,不會配置設定任何用于實作ObjectOutputStream的私有資料
     * 如果安裝了一個安全管理器,這個方法會先調用安全管理器的checkPermission方法來檢查序列化許可來確定可以使用子類
     */
    protected ObjectOutputStream() throws IOException, SecurityException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
        bout = null;
        handles = null;
        subs = null;
        enableOverride = true;
        debugInfoStack = null;
    }

    /**
     * 驗證這個執行個體(可能是子類)可以不用違背安全限制被構造:子類不能重寫安全敏感的非final方法,或者其他enableSubclassImplementation序列化許可檢查
     * 這個檢查會增加運作時開支
     */
    private void verifySubclass() {
        Class<?> cl = getClass();
        if (cl == ObjectOutputStream.class) {
            return;//不是子類直接傳回
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm == null) {
            return;//沒有安全管理器直接傳回
        }
        processQueue(Caches.subclassAuditsQueue, Caches.subclassAudits);//從弱引用隊列中出隊所有類,并移除緩存中相同的類
        WeakClassKey key = new WeakClassKey(cl, Caches.subclassAuditsQueue);
        Boolean result = Caches.subclassAudits.get(key);//緩存中是否已有這個類
        if (result == null) {
            result = Boolean.valueOf(auditSubclass(cl));//檢查這個子類是否安全
            Caches.subclassAudits.putIfAbsent(key, result);//将結果存儲到緩存
        }
        if (result.booleanValue()) {
            return;//子類安全直接傳回
        }
        sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);//檢查子類實作許可
    }

    /**
     * 提供writeStreamHeader方法這樣子類可以擴充或者預先考慮它們自己的流頭部。
     * 這個方法寫魔數和版本到流中。
     *
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);//流魔數
        bout.writeShort(STREAM_VERSION);//流版本
    }           

接下來開始關鍵部分,來分析writeObject到底做了什麼,首先看這個方法本身是final方法也就是說即使繼承了ObjectOutputStream也不能重寫這個方法而是重寫writeObjectOverride并且enableOverride=true

public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);//如果流子類重寫了writeObject則調用這裡的方法
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }           

writeObject0這個方法代碼很長,一部分一部分來看,首先我們注意到上面的都是緩存替換部分,第一次進入這個方法是不需要考慮的,直接看到writeOrdinaryObject這裡,因為用于資料化的類是實作了Serializable接口,是以會進入這個分支。

private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        boolean oldMode = bout.setBlockDataMode(false);//将輸出流設定為非塊模式
        depth++;//增加遞歸深度
        try {
            // handle previously written and non-replaceable objects處理之前寫的不可替換對象
            int h;
            if ((obj = subs.lookup(obj)) == null) {
                writeNull();//替代對象映射中這個對象為null時,寫入null代碼
                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;
            }

            // check for replacement object檢查替代對象,要求對象重寫了writeReplace方法
            Object orig = obj;
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                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);//如果不重寫這個方法直接傳回了obj也就是什麼也沒做
                if (rep != obj && rep != null) {
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                obj = rep;
            }

            // if object replaced, run through original checks a second time如果對象被替換,第二次運作原本的檢查,大部分情況下不執行此段
            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;
                }
            }

            // remaining cases剩下的情況
            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);
        }
    }           

writeOrdinaryObject這個方法主要是在Externalizable和Serializable的接口出現分支,如果實作了Externalizable接口并且類描述符非動态代理,則執行writeExternalData,否則執行writeSerialData。同時,這個方法會寫類描述資訊。

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);//如果是share模式把這個對象加入緩存
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }           

writeExternalData和writeSerialData(Object, ObjectStreamClass)這裡有個上下文的操作,目的是保證序列化操作同一時間隻能由一個線程調用。前者直接調用writeExternal,後者如果重寫了writeObject則調用它,否則調用defaultWriteFields。defaultWriteFields會先輸出基本資料類型,對于非基本資料類型的部分會再遞歸調用writeObject0,是以這裡也就會增加遞歸深度depth。

private void writeExternalData(Externalizable obj) throws IOException {
        PutFieldImpl oldPut = curPut;
        curPut = null;

        if (extendedDebugInfo) {
            debugInfoStack.push("writeExternal data");
        }
        SerialCallbackContext oldContext = curContext;//存儲上下文
        try {
            curContext = null;
            if (protocol == PROTOCOL_VERSION_1) {
                obj.writeExternal(this);
            } else {//預設協定是2,是以會使用塊輸出流
                bout.setBlockDataMode(true);
                obj.writeExternal(this);//這裡取決于類的方法怎麼實作
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            }
        } finally {
            curContext = oldContext;//恢複上下文
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }

        curPut = oldPut;
    }

    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()) {//重寫了writeObject方法
                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);//調用writeObject方法
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);//如果沒有重寫writeObject則輸出預設内容
            }
        }
    }
    
    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();
                }
            }
        }
    }           

然後看一下類描述資訊是怎麼寫的,動态代理類和普通類有一些差別,但都是先寫這個類本身的資訊再寫入父類的資訊。

private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException
    {
        int handle;
        if (desc == null) {
            writeNull();//描述符不存在時寫null
        } else if (!unshared && (handle = handles.lookup(desc)) != -1) {
            writeHandle(handle);//共享模式且緩存中已有該類描述符時,寫對應句柄值
        } else if (desc.isProxy()) {
            writeProxyDesc(desc, unshared);//描述符是動态代理類時
        } else {
            writeNonProxyDesc(desc, unshared);//描述符是标準類時
        }
    }
    
    private void writeProxyDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException
    {
        bout.writeByte(TC_PROXYCLASSDESC);
        handles.assign(unshared ? null : desc);//存入緩存
        //擷取類實作的接口,然後寫入接口個數和接口名
        Class<?> cl = desc.forClass();
        Class<?>[] ifaces = cl.getInterfaces();
        bout.writeInt(ifaces.length);
        for (int i = 0; i < ifaces.length; i++) {
            bout.writeUTF(ifaces[i].getName());
        }

        bout.setBlockDataMode(true);
        if (cl != null && isCustomSubclass()) {
            ReflectUtil.checkPackageAccess(cl);
        }
        annotateProxyClass(cl);//裝配動态代理類,子類可以重寫這個方法存儲類資訊到流中,預設什麼也不做
        bout.setBlockDataMode(false);
        bout.writeByte(TC_ENDBLOCKDATA);

        writeClassDesc(desc.getSuperDesc(), false);//寫入父類的描述符
    }
    
    private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException
    {
        bout.writeByte(TC_CLASSDESC);
        handles.assign(unshared ? null : desc);

        if (protocol == PROTOCOL_VERSION_1) {
            // do not invoke class descriptor write hook with old protocol
            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);//寫入父類的描述資訊
    }           

最後看一下幾個寫方法,寫字元串是寫入UTF編碼的二進制流資料。寫枚舉會額外寫入一次枚舉的類描述,然後将枚舉名作為字元串寫入。如果是寫一個數組,先寫入數組長度,然後如果數組是基本資料類型則可以直接寫入,否則需要遞歸調用writeObject0

private void writeString(String str, boolean unshared) throws IOException {
        handles.assign(unshared ? null : str);
        long utflen = bout.getUTFLength(str);//獲得UTF編碼長度
        if (utflen <= 0xFFFF) {
            bout.writeByte(TC_STRING);
            bout.writeUTF(str, utflen);
        } else {
            bout.writeByte(TC_LONGSTRING);
            bout.writeLongUTF(str, utflen);
        }
    }

    private void writeEnum(Enum<?> en,
                           ObjectStreamClass desc,
                           boolean unshared)
        throws IOException
    {
        bout.writeByte(TC_ENUM);
        ObjectStreamClass sdesc = desc.getSuperDesc();
        writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
        handles.assign(unshared ? null : en);
        writeString(en.name(), false);
    }           

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中的對應方法,除了他們參與在塊資料模式下寫入資料到資料塊中部分有所不同。基本上邏輯都是先檢查空間是否足夠,不足的話先重新整理緩沖區,然後将資料存儲到緩沖區中。因為篇幅原因,這裡隻貼幾個方法為例。寫一個字元串時,需要先将字元串中的字元存儲到字元緩沖數組中,然後再轉換成位元組存儲到buf中。

public void writeBoolean(boolean v) throws IOException {
            if (pos >= MAX_BLOCK_SIZE) {
                drain();
            }
            Bits.putBoolean(buf, pos++, v);
        }

        public void writeByte(int v) throws IOException {
            if (pos >= MAX_BLOCK_SIZE) {
                drain();
            }
            buf[pos++] = (byte) v;
        }

        /**
         * 寫入單個字元,塊未滿時存儲到緩沖區,塊滿時調用的是BlockDataOutputStream.write(int v)方法
         */
        public void writeChar(int v) throws IOException {
            if (pos + 2 <= MAX_BLOCK_SIZE) {
                Bits.putChar(buf, pos, (char) v);
                pos += 2;
            } else {
                dout.writeChar(v);
            }
        }

        /**
         * 先将String中的内容複制到字元緩沖區,再将其中的内容轉為位元組複制到塊資料緩沖區
         */
        public void writeBytes(String s) throws IOException {
            int endoff = s.length();
            int cpos = 0;//目前字元串開始位置
            int csize = 0;//目前字元串大小
            for (int off = 0; off < endoff; ) {
                if (cpos >= csize) {
                    cpos = 0;
                    csize = Math.min(endoff - off, CHAR_BUF_SIZE);
                    s.getChars(off, off + csize, cbuf, 0);//将字元串中指定位置的片段複制到字元數組緩沖區
                }
                if (pos >= MAX_BLOCK_SIZE) {
                    drain();
                }
                int n = Math.min(csize - cpos, MAX_BLOCK_SIZE - pos);
                int stop = pos + n;
                while (pos < stop) {
                    buf[pos++] = (byte) cbuf[cpos++];//将字元數組中的内容複制到塊資料緩沖區
                }
                off += n;
            }
        }           

下面的方法寫出連貫的原始資料值。盡管和重複調用對應的原始寫方法結果相同,這些方法對于寫一組原始資料值進行了效率優化。優化的方式是先計算出緩沖區内的剩餘大小,計算可以寫入的個數,然後直接寫入而不是每次寫入之前檢查緩沖區是否有空間,減少判斷次數。寫UTF編碼字元串時,如果能夠提前知道編碼長度,可以省去一次周遊字元串确定大小的過程,因為UTF編碼中單個字元可能是一個1-3個位元組不等。

void writeBooleans(boolean[] v, int off, int len) throws IOException {
            int endoff = off + len;
            while (off < endoff) {
                if (pos >= MAX_BLOCK_SIZE) {
                    drain();
                }
                int stop = Math.min(endoff, off + (MAX_BLOCK_SIZE - pos));
                while (off < stop) {//連續存儲資料到緩沖區,減少了判斷緩沖區是否滿的次數
                    Bits.putBoolean(buf, pos++, v[off++]);
                }
            }
        }

        void writeChars(char[] v, int off, int len) throws IOException {
            int limit = MAX_BLOCK_SIZE - 2;
            int endoff = off + len;
            while (off < endoff) {
                if (pos <= limit) {
                    int avail = (MAX_BLOCK_SIZE - pos) >> 1;//一個字元=2個位元組是以要除以2
                    int stop = Math.min(endoff, off + avail);
                    while (off < stop) {
                        Bits.putChar(buf, pos, v[off++]);
                        pos += 2;
                    }
                } else {
                    dout.writeChar(v[off++]);
                }
            }
        }

        /**
         * 傳回給定字元串在UTF編碼下的位元組長度
         */
        long getUTFLength(String s) {
            int len = s.length();
            long utflen = 0;
            for (int off = 0; off < len; ) {
                int csize = Math.min(len - off, CHAR_BUF_SIZE);
                s.getChars(off, off + csize, cbuf, 0);
                for (int cpos = 0; cpos < csize; cpos++) {
                    char c = cbuf[cpos];
                    if (c >= 0x0001 && c <= 0x007F) {
                        utflen++;
                    } else if (c > 0x07FF) {
                        utflen += 3;
                    } else {
                        utflen += 2;
                    }
                }
                off += csize;
            }
            return utflen;
        }

        /**
         * 寫給定字元串的UTF格式。這個方法用于字元串的UTF編碼長度已知的情況,這樣可以避免提前掃描一遍字元串來确定UTF長度
         */
        void writeUTF(String s, long utflen) throws IOException {
            if (utflen > 0xFFFFL) {
                throw new UTFDataFormatException();
            }
            writeShort((int) utflen);//先寫長度
            if (utflen == (long) s.length()) {
                writeBytes(s);//沒有特殊字元
            } else {
                writeUTFBody(s);//有特殊字元
            }
        }           

HandleTable

HandleTable是一個輕量的hash表,它的作用是緩存寫過的共享class便于下次查找,内部含有3個數組,spine、next和objs。objs存儲的是對象也就是class,spine是hash桶,next是沖突連結清單,每有一個新的元素插入需要計算它的hash值然後用spine的大小取模,找到它的連結清單,新對象會被插入到連結清單的頭部,它在objs和next中對應的資料是根據加入的序号順序存儲,spine存儲它的handle值也就是在另外兩個數組中的下标。

/** number of mappings in table/next available handle 表中映射的個數或者下一個有效的句柄*/
        private int size;
        /** size threshold determining when to expand hash spine 決定什麼時候擴充hash脊柱的大小門檻值*/
        private int threshold;
        /** factor for computing size threshold 計算大小門檻值的因子*/
        private final float loadFactor;
        /** maps hash value -> candidate handle value 映射hash值->候選句柄值*/
        private int[] spine;
        /** maps handle value -> next candidate handle value 映射句柄值->下一個候選句柄值*/
        private int[] next;
        /** maps handle value -> associated object 映射句柄值->關聯的對象*/
        private Object[] objs;

        /**
         * 建立一個新的hash表使用給定的容量和負載因子
         */
        HandleTable(int initialCapacity, float loadFactor) {
            this.loadFactor = loadFactor;
            spine = new int[initialCapacity];
            next = new int[initialCapacity];
            objs = new Object[initialCapacity];
            threshold = (int) (initialCapacity * loadFactor);
            clear();
        }           

assign就是插入操作,它會檢查3個數組大小是否足夠,其中spine是根據next.length*負載因子來決定門檻值的,數組大小擴大是乘以2加1,這個和HashTable時同樣的設計。插入的時候注意到next的值被賦為原本的spine[index]值,說明之前的連結清單頭成為了新結點的後驅,也就是說結點被插傳入連結表頭部。

/**
         * 配置設定下一個有效的句柄給給出的對象并傳回句柄值。句柄從0開始升序被配置設定。相當于put操作
         */
        int assign(Object obj) {
            if (size >= next.length) {
                growEntries();
            }
            if (size >= threshold) {
                growSpine();
            }
            insert(obj, size);
            return size++;
        }

        /**
         * 通過延長條目數組增加hash表容量,next和objs大小變為舊大小*2+1
         */
        private void growEntries() {
            int newLength = (next.length << 1) + 1;//長度=舊長度*2+1
            int[] newNext = new int[newLength];
            System.arraycopy(next, 0, newNext, 0, size);//複制舊數組元素到新數組中
            next = newNext;

            Object[] newObjs = new Object[newLength];
            System.arraycopy(objs, 0, newObjs, 0, size);
            objs = newObjs;
        }

        /**
         * 擴充hash脊柱,等效于增加正常hash表的桶數
         */
        private void growSpine() {
            spine = new int[(spine.length << 1) + 1];//新大小=舊大小*2+1
            threshold = (int) (spine.length * loadFactor);//擴充門檻值=spine大小*負載因子
            Arrays.fill(spine, -1);//spine中全部填充-1
            for (int i = 0; i < size; i++) {
                insert(objs[i], i);
            }
        }

        /**
         * 插入映射對象->句柄到表中,假設表足夠大來容納新的映射
         */
        private void insert(Object obj, int handle) {
            int index = hash(obj) % spine.length;//hash值%spine數組大小
            objs[handle] = obj;//objs順序存儲對象
            next[handle] = spine[index];//next存儲spine[index]原本的handle值,也就是說新的沖突對象插入在連結清單頭部
            spine[index] = handle;//spine中存儲handle大小
        }           

hash值計算就是通過系統函數計算出hash值然後去有符号int的有效位

private int hash(Object obj) {
            return System.identityHashCode(obj) & 0x7FFFFFFF;//取系統計算出的hash值得有效整數值部分
        }           

lookup是查找hash表中是否含有指定對象,這裡相等必須是==,因為class在完整類名相等時就是==

/**
         * 查找并傳回句柄值關聯給與的對象,如果沒有映射傳回-1
         */
        int lookup(Object obj) {
            if (size == 0) {
                return -1;
            }
            int index = hash(obj) % spine.length;//通過hash值尋找在spine數組中的位置
            for (int i = spine[index]; i >= 0; i = next[i]) {
                if (objs[i] == obj) {//周遊spine[index]位置的連結清單,必須是對象==才是相等
                    return i;
                }
            }
            return -1;
        }           

clear是清空hash表,size傳回目前表中映射對數

/**
         * 重置表為初始狀态,next不需要重新指派是因為插入第一個元素時,原本的spine[index]一定是-1,連結清單中不會出現之前存在的值
         */
        void clear() {
            Arrays.fill(spine, -1);
            Arrays.fill(objs, 0, size, null);
            size = 0;
        }

        /**
         * 傳回目前表中的映射數量
         */
        int size() {
            return size;
        }