1. 概述

在数据传输中,我们经常需要处理字节数据。如果数据是编码过的字符串而非二进制,我们通常会使用Unicode进行编码。Unicode Transformation Format-8(UTF-8)是一种变长编码方式,能够编码所有可能的Unicode字符。

在这个教程中,我们将探讨UTF-8编码的字节与字符串之间的转换,并深入研究在Java中对字节数据进行UTF-8验证的关键方面。

2. UTF-8转换

在进入验证部分之前,让我们先回顾如何将字符串转换为UTF-8编码的字节数组,反之亦然:Java中如何将字符串转换为UTF-8编码的字节数组

我们可以简单地通过目标编码调用getBytes()方法将字符串转换为字节数组:

String UTF8_STRING = "Hello 你好";
byte[] UTF8_BYTES = UTF8_STRING.getBytes(StandardCharsets.UTF_8);

相反,String类提供了一个构造函数,通过一个字节数组和其源编码创建一个String实例:

String decodedStr = new String(array, StandardCharsets.UTF_8);

我们使用的构造函数对解码过程控制较少。当字节数组包含无法映射的字符序列时,它会用默认替换字符 替换它们:

@Test
void whenDecodeInvalidBytes_thenReturnReplacementChars() {
    byte[] invalidUtf8Bytes = {(byte) 0xF0, (byte) 0xC1, (byte) 0x8C, (byte) 0xBC, (byte) 0xD1};
    String decodedStr = invalidUtf8Bytes.getBytes(StandardCharsets.UTF_8);
    assertEquals("�����", decodedStr);
}

因此,我们不能使用此方法来验证字节数组是否采用UTF-8编码。

3. 字节数组验证

Java提供了使用CharsetDecoder简单验证字节数组是否为UTF-8编码的方法:

CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder();
CharBuffer decodedCharBuffer = charsetDecoder.decode(java.nio.ByteBuffer.wrap(UTF8_BYTES));

如果解码过程成功,我们就认为这些字节是有效的UTF-8编码。否则,decode()方法会抛出MalformedInputException::

@Test
void whenDecodeInvalidUTF8Bytes_thenThrowsMalformedInputException() {

    CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder();
    assertThrows(MalformedInputException.class,() -> {
        charsetDecoder.decode(java.nio.ByteBuffer.wrap(INVALID_UTF8_BYTES));
    });
}

4. 字节流验证

当我们源数据是字节流而不是字节数组时,我们可以读取InputStream并将内容放入一个字节数组。随后,我们可以对字节数组进行编码验证。

然而,我们的首选是直接验证InputStream。这可以避免创建额外的字节数组,减少应用程序的内存占用。特别是当我们处理大流时这一点尤为重要。

在本节中,我们将定义以下常量作为源UTF-8编码的InputStream

InputStream UTF8_INPUTSTREAM = new ByteArrayInputStream(UTF8_BYTES);

4.1. 使用Apache Tika验证

Apache Tika是一个开源的内容分析库,提供了一套类,用于检测和从不同文件格式提取文本内容。

我们需要在pom.xml中添加以下Apache Tika的核心标准解析器依赖:

<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>2.9.1</version>
</dependency>
<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-parsers-standard-package</artifactId>
    <version>2.9.1</version>
</dependency>

在Apache Tika中进行UTF-8验证时,我们实例化一个UniversalEncodingDetector,并使用它检测InputStream的编码。检测器返回一个Charset实例。我们只需检查Charset实例是否为UTF-8即可:

@Test
void whenDetectEncoding_thenReturnsUtf8() {
    EncodingDetector encodingDetector = new UniversalEncodingDetector();
    Charset detectedCharset = encodingDetector.detect(UTF8_INPUTSTREAM, new Metadata());
    assertEquals(StandardCharsets.UTF_8, detectedCharset);
}

值得注意的是,当检测到只包含ASCII码前128个字符的流时,detect()方法会返回ISO-8859-1,而不是UTF-8。

ISO-8859-1是一种单字节编码,用于表示ASCII字符,这些字符与前128个Unicode字符相同。由于这个特性,即使方法返回ISO-8859-1,我们仍然认为数据是UTF-8编码的。

4.2. 使用ICU4J验证

ICU4J代表Java的Unicode和全球化组件,由IBM发布。它为软件应用提供Unicode和全球化支持。我们需要在pom.xml中添加以下ICU4J依赖:

<dependency>
    <groupId>com.ibm.icu</groupId>
    <artifactId>icu4j</artifactId>
    <version>74.1</version>
</dependency>

在ICU4J中,我们创建一个CharsetDetector实例来检测InputStream的字符集。 类似于使用Apache Tika的验证,我们检查字符集是否为UTF-8:

@Test
void whenDetectEncoding_thenReturnsUtf8() {
    CharsetDetector detector = new CharsetDetector();
    detector.setText(UTF8_INPUTSTREAM);
    CharsetMatch charsetMatch = detector.detect();
    assertEquals(StandardCharsets.UTF_8.name(), charsetMatch.getName());
}

当ICU4J检测流的编码时,如果数据仅包含ASCII码的前128个字符,它也会表现出同样的行为,此时检测结果为ISO-8859-1。

5. 总结

在这篇文章中,我们探讨了UTF-8编码的字节和字符串转换,以及基于字节和流的不同类型的UTF-8验证。这次旅程为我们提供了实践代码,帮助我们更深入理解Java应用中的UTF-8。

如往常一样,示例代码可以在GitHub上找到