以我的經驗,當開發人員第一次遇到java.nio.ByteBuffer時,會引起混亂和細微的錯誤,因為如何正確使用它尚不明顯。 在我對API文檔感到滿意之前,需要反復閱讀API文檔和一些經驗以實現一些微妙之處。 這篇文章是關于如何正確使用它們的短暫崩潰,希望可以為其他人節省一些麻煩。
由于所有這些都是基于推斷(而不是基于明確的文檔),并且是基于經驗,因此我不能斷言這些信息必定是權威的。 我歡迎您提出反饋意見,以指出錯誤或其他觀點。 我也歡迎提出其他陷阱/最佳做法的建議。
我確實假定讀者將閱讀與本文相關的API文檔。 我不會窮盡所有您可以使用ByteBuffer進行的操作。
ByteBuffer抽象
可以將ByteBuffer看作提供了一些(未定義的)底層字節存儲的視圖 。 字節緩沖區的兩種最常見的具體類型是由字節數組支持的字節緩沖區和由直接(脫離堆,本機)字節緩沖區支持的字節緩沖區。 在兩種情況下,都可以使用相同的接口讀取和寫入緩沖區的內容。
ByteBuffer的API的某些部分特定于某些類型的字節緩沖區。 例如,字節緩沖區可以是只讀的 ,將用法限制為方法的子集。 array()方法僅適用于由字節數組支持的字節緩沖區(可以使用hasArray()進行測試),并且通常僅在完全知道自己在做什么的情況下使用 。 一個常見的錯誤是使用array()將ByteBuffer“轉換”為字節數組。 這不僅僅適用于字節數組支持的緩沖區,而且很容易成為錯誤的來源,因為根據緩沖區的創建方式,返回數組的開頭可能與字節緩沖區的開頭相對應, 也可能不對應。 結果往往是一個細微的錯誤,其中代碼的行為根據字節緩沖區和創建它的代碼的實現細節而有所不同。
ByteBuffer可以通過調用repeat()復制自身。 這實際上并不復制基礎字節 ,而只是創建一個指向相同基礎存儲的新ByteBuffer實例。 可以使用slice()創建表示另一個ByteBuffer的子集的ByteBuffer。
與字節數組的主要區別
- ByteBuffer具有關于hashCode() / equals()的值語義,因此可以更方便地在容器中使用。
- ByteBuffer通過實例化新的ByteBuffer,提供了將字節緩沖區的子集作為值傳遞而不復制字節的功能。
- NIO API大量使用了ByteBuffer:s。
- ByteBuffer中的字節可能駐留在Java堆之外。
- ByteBuffer的狀態超出了字節本身,這有利于進行相對的I / O操作(但有一些警告,請參見下文)。
- ByteBuffer提供了用于讀取和寫入各種原始類型(如整數和long)的方法(并且可以按不同的字節順序進行操作)。
ByteBuffer的關鍵屬性
ByteBuffer的以下三個屬性至關重要(我在每個屬性上引用了API文檔):
- 緩沖區的容量是它包含的元素數量。 緩沖區的容量永遠不會為負,也不會改變。
- 緩沖區的限制是不應讀取或寫入的第一個元素的索引。 緩沖區的限制永遠不會為負,也永遠不會大于緩沖區的容量。
- 緩沖區的位置是下一個要讀取或寫入的元素的索引。 緩沖區的位置永遠不會為負,也不會大于其限制。
這是一個示例ByteBuffer的可視化示例,在示例中,ByteBuffer由字節數組支持,并且ByteBuffer的值是單詞“ test”(單擊以放大):
該ByteBuffer等于(在equals()的意義上) 等于其在[ position , limit )之間內容相同的任何其他ByteBuffer。
假設上面顯示的字節緩沖區是bb ,我們這樣做:
final ByteBuffer other = bb.duplicate();
other.position(bb.position() + 4);
現在,我們將有兩個ByteBuffer實例都引用相同的基礎字節數組,但是它們的內容將有所不同( 其他將為空):
字節緩沖區的緩沖區/流對偶
有兩種訪問字節緩沖區內容的方法- 絕對訪問和相對訪問。 例如,假設我有一個ByteBuffer,我知道它包含兩個整數。 為了使用絕對定位提取整數,可以這樣做:
int first = bb.getInt(0)
int second = bb.getInt(4)
或者,可以使用相對定位提取它們:
int first = bb.getInt();
int second = bb.getInt();
第二種選擇通常很方便,但是以對緩沖區產生副作用 (即更改它)為代價。 不是內容本身,而是ByteBuffers視圖可以查看該內容。
這樣,如果將ByteBuffer用作流,則其行為類似于流。
最佳做法和陷阱
flip()緩沖區
如果要通過重復寫入來構建ByteBuffer,然后想將其贈送,則必須記住將它翻轉() 。 例如,這是一種將字節數組復制到ByteBuffer的方法,并假設使用默認編碼(請注意,此處使用的ByteBuffer.wrap()創建一個包裝指定字節數組的ByteBuffer,而不是復制其中的內容放入新的ByteBuffer中):
public static ByteBuffer fromByteArray(byte[] bytes) {final ByteBuffer ret = ByteBuffer.wrap(new byte[bytes.length]);ret.put(bytes);ret.flip();return ret;
}
如果我們不翻轉它,則返回的ByteBuffer 將為空,因為該位置等于limit 。
不要消耗緩沖區
除非特別打算這樣做,否則在讀取字節緩沖區時請注意不要“消耗”它。 例如,考慮采用默認編碼方式,將ByteBuffer轉換為String的此方法:
public static String toString(ByteBuffer bb) {final byte[] bytes = new byte[bb.remaining()];bb.duplicate().get(bytes);return new String(bytes);
}
不幸的是,沒有提供進行字節數組的絕對定位讀取的方法(但確實存在用于基元的絕對定位讀取)。
注意在讀取字節時使用了plicate() 。 如果我們不這樣做,該函數將對輸入ByteBuffer產生副作用 。 這樣做的代價是僅為了一次調用get()的目的就額外分配了一個新的ByteBuffer。 您可以在get()之前記錄ByteBuffer的位置,然后再將其還原,但這存在線程安全性問題(請參閱下一節)。
值得注意的是,這僅在您嘗試將ByteBuffer:s視為值時適用。 如果您正在編寫旨在對ByteBuffer產生副作用的代碼,將它們更像流一樣對待,那么您當然打算這樣做,并且本節不適用。
不要改變緩沖區
在不是特定于特定用例的通用代碼的情況下,(在我看來)對于執行(抽象)只讀操作(例如讀取字節緩沖區)的方法來說是一種好習慣。 ,不更改其輸入。 這是比“不要消耗ByteByffer”更強的要求。 以上一節中的示例為例,但嘗試避免額外分配ByteBuffer:
public static String toString(ByteBuffer bb) {final byte[] bytes = new byte[bb.remaining()];bb.mark(); // NOT RECOMMENDED, don't do thisbb.get(bytes);bb.reset(); // NOT RECOMMENDED, don't do thisreturn new String(bytes);
}
在這種情況下,我們在調用get()之前記錄ByteBuffer的狀態,然后再進行恢復(請參閱API文檔中的mark()和reset() )。 這種方法有兩個問題。 第一個問題是上面的函數沒有組成 。 一個ByteBuffer僅具有一個“標記”,并且您的(非常通用,不具有上下文意識) toString()方法不能安全地假定調用者并未出于自身目的嘗試使用mark()和reset() 。 例如,假設以下調用者正在反序列化一個長度為前綴的字符串:
bb.mark();
int length = bb.getInt();
... sanity check length
final String str = ByteBufferUtils.toString(bb);
... do something
bb.reset(); // OOPS - reset() will now point 4 bytes off, because toString() modified the mark
(順便說一句,這是一個非常人為且奇怪的示例,因為我發現很難提出一個使用mark() / reset()的實際代碼示例,該代碼通常在處理流中的緩沖區時使用,像派系一樣,也感覺需要在所述緩沖區的其余部分上調用toString() 。我很想聽聽人們在這里提出了什么解決方案。例如,可以想象一個清晰的代碼庫中的策略在類似于toString()的面向值的上下文中允許mark() / reset() –但是即使您這樣做了(它可能會無意中違反了它的味道),您仍然會遭受后面提到的突變問題。)
讓我們看一下避免這種問題的toString()的替代版本:
public static String toString(ByteBuffer bb) {final byte[] bytes = new byte[bb.remaining()];bb.get(bytes);bb.position(bb.position() - bytes.length); // NOT RECOMMENDED, don't do thisreturn new String(bytes);
}
在這種情況下,我們不修改標記,因此我們進行撰寫。 但是,我們仍然致力于改變輸入的“罪行”。 在多線程情況下,這是一個問題。 除非抽象隱含了該內容(例如,使用流或以類似流的方式使用ByteBuffer時),否則您不希望閱讀暗示其變化的內容。 如果要傳遞的ByteBuffer視為一個值,將其放入容器中,共享它們,等等–除非保證兩個線程永遠不會同時使用同一個ByteBuffer,否則對它們進行突變將引入細微的錯誤。 通常,此類錯誤的結果是奇怪的值損壞或意外的BufferOverFlowException:s。
不受此影響的版本出現在上面的“不要使用緩沖區”部分,該部分使用duplicate()構造一個臨時的ByteBuffer實例,可以在其上安全調用get() 。
compareTo()受字節簽名的約束
Java中的字節是有符號的 ,這與通常期望的相反。 但是,容易錯過的是,這也會影響ByteBuffer.compareTo() 。 該方法的Java API文檔顯示為:
“通過按字典順序比較剩余字節的序列來比較兩個字節緩沖區,而不考慮每個序列在其相應緩沖區中的開始位置。”
快速閱讀可能會使人相信結果通常是您期望的,但是當然,鑒于Java中字節的定義,情況并非如此。 結果是,包含最高位設置的值的字節緩沖區的順序將與您期望的有所不同。
Google出色的Guava庫具有UnsignedBytes幫助器 ,可減輕您的痛苦。
array()通常是使用錯誤的方法
通常,不要隨便使用array() 。 為了正確使用它,您要么必須知道字節緩沖區是數組支持的事實 ,要么必須使用 hasArray() 對其進行測試,并且在兩種情況下都有兩個單獨的代碼路徑。 此外,在使用它時, 必須使用arrayOffset()以確定ByteBuffer的第零個位置與字節數組相對應。
在典型的應用程序代碼中,除非您真的知道自己在做什么并且特別需要它,否則您將不會使用array() 。 也就是說,在某些情況下它很有用。 例如,假設您實現的是UnsignedBytes.compare()的ByteBuffer版本(同樣來自Guava )–您可能希望優化其中一個或兩個參數都支持數組的情況,以避免不必要的復制和頻繁調用。緩沖區。 對于這種通用且可能大量使用的方法,這種優化是有意義的。
參考: Java ByteBuffer –我們的JCG合作伙伴 Peter Schuller在(mod:world:scode)博客上的速成班 。
翻譯自: https://www.javacodegeeks.com/2012/12/the-java-bytebuffer-a-crash-course.html