Ohhnews

分类导航

$ cd ..
Baeldung原文

Google Protobuf ByteString 与 Byte 数组:深入比较

#protobuf#bytestring#字节数组#java#二进制数据

[LOADING...]

1. 引言

在 Java 中使用 Google 的 Protocol Buffers (Protobuf) 时,我们不可避免地会遇到处理二进制数据的需求。这通常会导致在标准的 byte[] 和 Protobuf 的自定义 ByteString 类之间进行选择。虽然它们都表示字节序列,但在设计和预期用途上存在根本差异。

在本文中,我们将探讨这两种类型的特性,通过代码示例突出它们的主要区别,并提供关于何时使用它们以获得最佳性能和可维护性的指导。

2. 定义 Maven 依赖项

首先,我们需要在项目中引入 protobuf-java 依赖项:

$ 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 数组,然后替换其中的一个元素:

$ java
@Test
public void givenByteArray_whenModified_thenChangesPersist() {
    // 这里,我们将初始化一个可变缓冲区
    byte[] data = new byte[4];
        
    // 我们将数据读入缓冲区
    ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[] { 0x01, 0x02, 0x03, 0x04 });
    try {
        inputStream.read(data);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 注意,第一个字节是 1
    assertEquals(1, data[0]);
    // 我们可以直接修改第一个字节
    data[0] = 0x05;
        
    // 修改已持久化
    assertEquals(5, data[0]);
}

如上述测试所示,byte[] 可以原地修改,这使其成为需要操作缓冲区内容的场景的灵活选择。

4. 理解 ByteString

ByteString 是 Protobuf 库提供的一个类,用于处理字节序列。byte[] 不同,ByteString 是不可变的。 一旦创建,其内容就不能更改,这类似于 Java 中 String 类的工作方式。

这种不可变性提供了几个优点,例如线程安全,因为不可变对象本质上可以安全地在多个线程之间共享而无需同步。

此外,效率更高,因为像 substring()concat() 这样的操作是高度优化的。 这些方法通常不是复制所有数据,而是创建新的 ByteString 对象,这些对象共享对原始数据的引用,这在内存和性能方面都效率更高。

让我们看看 ByteString 的不可变性:

$ java
@Test
public void givenByteString_whenCreated_thenIsImmutable() {
    // 我们将从一个可变的字节数组创建一个不可变的 ByteString
    byte[] originalArray = new byte[] { 0x01, 0x02, 0x03, 0x04 };
    ByteString byteString = ByteString.copyFrom(originalArray);
        
    // 第一个字节的值是 1
    assertEquals(1, byteString.byteAt(0));
        
    // 我们将尝试修改原始数组
    originalArray[0] = 0x05;
        
     // ByteString 的内容保持不变
     assertEquals(1, byteString.byteAt(0));
}

该测试证实,即使源 byte[] 被修改,ByteString 仍然保持不变。这种行为是其在 Protobuf 中可靠性的关键。

5. 主要区别

byte[]ByteString 的对比特性导致了影响我们设计决策的关键差异。

5.1. 可变性与不可变性

这是最根本的区别。byte[] 是可变的,这使其非常适合需要原地修改的数据,例如内存中的缓冲区或流处理期间的数据。

相比之下,ByteString 是不可变的,这确保了数据完整性和线程安全。 这使其成为持久性或共享数据的完美选择,尤其是在消息格式的上下文中。

5.2. 性能

对于简单的读/写操作,性能相似。然而,ByteString 在更复杂的操作(如连接)中展现出其真正的效率。

要连接两个 byte[] 数组,我们必须创建一个新的、更大的数组并复制所有数据,这可能是一个昂贵的操作。ByteStringconcat() 方法经过高度优化,通常会创建一个引用两个原始对象的新实例,而无需执行完整的数据复制,这显著减少了内存分配。

5.3. API 和 Protobuf 集成

byte[] 的 API 很少,因此大多数复杂操作都需要自定义逻辑。另一方面,ByteString 为二进制数据提供了丰富的 API,包括 startsWith()substring()indexOf() 等方法。

最重要的是,ByteString 是 Protobuf 消息中 bytes 字段的原生类型。 它确保了无缝高效的序列化和反序列化。我们可以通过一个简单的 Protobuf 定义来看出这一点:

$ protobuf
message UserData {
  string name = 1;
  bytes profile_image = 2;
}

生成的 Java 类会将 profile_image 字段表示为 ByteString,而不是 byte[]。这种集成是 Protobuf 设计的核心部分。

6. 类型之间的转换

在常见场景中,当我们与标准 Java API 交互时,通常需要在这两种类型之间进行转换。

6.1. byte[]ByteString

要将 byte[] 转换为 ByteString,我们使用静态方法 ByteString.copyFrom() 此操作会创建一个新的 ByteString 并复制数据,确保新实例的不可变性:

$ java
@Test
public void givenByteArray_whenCopiedToByteString_thenDataIsCopied() {
    // 我们将从一个可变的字节数组开始
    byte[] byteArray = new byte[] { 0x01, 0x02, 0x03 };
        
    // 从中创建一个新的 ByteString
    ByteString byteString = ByteString.copyFrom(byteArray);
    // 我们将断言数据是相同的
    assertEquals(byteArray[0], byteString.byteAt(0));
        
    // 这里,我们更改原始数组
    byteArray[0] = 0x05;
    // 注意,ByteString 保持不变,证实了复制
    assertEquals(1, byteString.byteAt(0));
    assertNotSame(byteArray, byteString.toByteArray());
}

6.2. ByteStringbyte[]

反向转换使用 toByteArray() 方法。 此方法返回一个新的 byte[] 实例,其中包含 ByteString 数据的副本:

$ java
@Test
public void givenByteString_whenConvertedToByteArray_thenDataIsCopied() {
    // 我们将从一个不可变的 ByteString 开始
    ByteString byteString = ByteString.copyFromUtf8("Baeldung");
        
    // 从中创建一个可变的字节数组
    byte[] byteArray = byteString.toByteArray();
    // 这里,字节数组现在有了数据的副本
    assertEquals('B', (char) byteArray[0]);
        
    // 我们将更改新数组
    byteArray[0] = 'X';
    // 注意,原始 ByteString 保持不变
    assertEquals('B', (char) byteString.byteAt(0));
    assertNotSame(byteArray, byteString.toByteArray());
}

值得注意的是,这两种转换都涉及完整的数据复制,这对于大型字节序列可能会引入开销。

7. 结论

在本文中,我们首先探讨了 byte[]ByteString 之间的根本区别,从 byte[] 的可变性及其在低级流操作中的使用开始。我们还研究了性能和 API 的主要差异,最后了解了如何在两种类型之间进行转换。

最终,它们之间的选择归结为一个简单的原则:我们将 byte[] 用于可变的通用缓冲区,并将 ByteString 用作 Protobuf 消息中所有二进制数据的默认类型。

像往常一样,实现的源代码可在 GitHub 上获取。