Java序列化 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;
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章