Google Protobuf ByteString 與 Byte[]
1. 簡介
在 Java 中使用 Google 的協定緩衝區 (Protobuf)時,我們不可避免地會遇到處理二進位資料的需求。這通常會導致我們在標準byte[]
和 Protobuf 自訂的ByteString
類別之間做出選擇。雖然兩者都表示位元組序列,但它們在設計和預期用途上存在根本差異。
在本文中,我們將探討這兩種類型的特點,透過程式碼範例強調它們的主要區別,並提供何時使用每種類型以獲得最佳效能和可維護性的指導。
2.定義Maven依賴項
首先,我們需要在專案中包含protobuf-java依賴項:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.31.1</version>
</dependency>
此相依性提供對ByteString
類別和必要的 Protobuf API 的存取。
3. 理解byte[]
byte[]
是 Java 的核心資料結構,用來表示原始位元組序列。它的主要特性是可變性。這使得我們可以在創建後直接修改其元素,這對於構建緩衝區以從流中讀取資料等任務至關重要。
讓我們透過一個簡單的範例測試來說明它的可變性質。我們將定義一個byte
數組,然後替換其中的一個元素:
@Test
public void givenByteArray_whenModified_thenChangesPersist() {
// Here, we'll initialize a mutable buffer
byte[] data = new byte[4];
// We'll read data into the buffer
ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[] { 0x01, 0x02, 0x03, 0x04 });
try {
inputStream.read(data);
} catch (IOException e) {
e.printStackTrace();
}
// Note, the first byte is 1
assertEquals(1, data[0]);
// We can directly modify the first byte
data[0] = 0x05;
// The modification is persisted
assertEquals(5, data[0]);
}
如上面的測試所示, byte[]
可以就地更改,這使得它成為我們需要操作緩衝區內容的場景的靈活選擇。
4. 理解ByteString
ByteString
是 Protobuf 函式庫提供的一個用來處理位元組序列的類別。與byte[]
不同, ByteString
是不可變的。一旦創建,其內容就無法更改,這與 Java 中的String
類別的工作方式類似。
這種不變性提供了多種優勢,例如線程安全,因為不可變物件本質上是安全的,可以在多個線程之間共享而無需同步。
此外,由於substring()
和concat()
等操作經過高度最佳化,效率也得到了提升。這些方法通常不會複製所有數據,而是創建共享原始數據引用的新ByteString
對象,這在記憶體和效能方面都更加有效率。
讓我們看看ByteString
的不變性:
@Test
public void givenByteString_whenCreated_thenIsImmutable() {
// We'll create an immutable ByteString from a mutable byte array
byte[] originalArray = new byte[] { 0x01, 0x02, 0x03, 0x04 };
ByteString byteString = ByteString.copyFrom(originalArray);
// The value of the first byte is 1
assertEquals(1, byteString.byteAt(0));
// We'll try to modify the original array
originalArray[0] = 0x05;
// The ByteString's contents remain unchanged
assertEquals(1, byteString.byteAt(0));
}
測試確認,即使來源byte[]
被修改, ByteString
仍然保持不變。這種行為是 Protobuf 可靠性的關鍵。
5. 主要區別
byte[]
和ByteString
的對比性質導致了影響我們設計決策的關鍵差異。
5.1. 可變性與不變性
這是最根本的差別。 byte byte[]
是可變的,這使得它非常適合需要就地修改的數據,例如記憶體緩衝區或在流處理期間。
相比之下, ByteString
是不可變的,這確保了資料完整性和線程安全性。這使得它成為持久化或共享資料的完美選擇,尤其是在訊息格式的上下文中。
5.2. 性能
對於簡單的讀取/寫入操作,效能相似。然而, ByteString
在諸如連接之類的更複雜操作中才真正體現了其效率。
要連接兩個byte[]
數組,我們必須創建一個新的、更大的數組並複製所有數據,這可能是一個開銷很大的操作。 ByteString 的ByteString
concat()
方法經過高度優化,通常會建立一個引用兩個原始物件的新實例,而無需執行完整的資料複製,從而顯著減少了記憶體分配。
5.3. API 和 Protobuf 集成
byte[]
的 API 非常精簡,因此大多數複雜的操作都需要自訂邏輯。而ByteString
則提供了針對二進位資料自訂的豐富 API,包括startsWith()
、 substring()
和indexOf()
等方法。
最重要的是, ByteString
是 Protobuf 訊息中bytes
組欄位的原生類型。它確保了無縫且高效的序列化和反序列化。我們可以透過查看一個簡單的 Protobuf 定義來了解這一點:
message UserData {
string name = 1;
bytes profile_image = 2;
}
產生的 Java 類別將把profile_image
欄位表示為ByteString
,而不是byte[]
。這種整合是 Protobuf 設計的核心部分。
6. 型別之間的轉換
在常見場景下,我們與標準 Java API 進行互通時,經常需要在byte[]
和ByteString
之間進行轉換。
6.1. byte[]
轉換為ByteString
要將 byte[] 轉換為ByteString
,我們使用靜態ByteString.copyFrom()
方法。此操作會建立一個新的ByteString
並複製數據,以確保新實例的不變性:
@Test
public void givenByteArray_whenCopiedToByteString_thenDataIsCopied() {
// We'll start with a mutable byte array
byte[] byteArray = new byte[] { 0x01, 0x02, 0x03 };
// Create a new ByteString from it
ByteString byteString = ByteString.copyFrom(byteArray);
// We'll assert that the data is the same
assertEquals(byteArray[0], byteString.byteAt(0));
// Here, we change the original array
byteArray[0] = 0x05;
// Note, the ByteString remains unchanged, confirming the copy
assertEquals(1, byteString.byteAt(0));
assertNotSame(byteArray, byteString.toByteArray());
}
6.2. ByteString
到byte[]
另一個方向的轉換使用toByteArray()
方法。此方法傳回一個新的byte[]
實例,其中包含ByteString
資料的副本:
@Test
public void givenByteString_whenConvertedToByteArray_thenDataIsCopied() {
// We'll start with an immutable ByteString
ByteString byteString = ByteString.copyFromUtf8("Baeldung");
// Create a mutable byte array from it
byte[] byteArray = byteString.toByteArray();
// Here, the byte array now has a copy of the data
assertEquals('B', (char) byteArray[0]);
// We'll change the new array
byteArray[0] = 'X';
// Note, the original ByteString remains unchanged
assertEquals('B', (char) byteString.byteAt(0));
assertNotSame(byteArray, byteString.toByteArray());
}
必須注意的是,這兩種轉換都涉及完整的**資料複製,這可能會為大位元組序列帶來開銷。**
7. 結論
在本文中,我們首先探討了byte[]
和ByteString
之間的根本區別,首先從byte[]
的可變性質及其在低階流操作中的使用入手。我們也研究了效能和 API 的關鍵差異,最後了解如何在兩種類型之間進行轉換。
最終,它們之間的選擇歸結為一個簡單的原則:我們使用byte[]
作為可變的通用緩衝區,並使用ByteString
作為 Protobuf 訊息中所有二進位資料的預設值。
與往常一樣,該實作的原始程式碼可在 GitHub 上取得。