将字节数组编码为字符串。

10

嗨,

我想将任意字节数据转换为字符串。我的问题是,使用UTF-8编码转换字节数据是否“安全”:

String s1 = new String(data, "UTF-8");

或使用 base64:

String s2 = Base64.encodeToString(data, false); //migbase64

我只是担心使用第一种方法会有负面影响。我的意思是两种变体都可以完美地工作,但是s1可以包含UTF-8字符集中的任何字符,而s2仅使用“可读”字符。我只是不确定是否真的需要使用base64。基本上,我只需要创建一个字符串,将其发送到网络上再接收回来。(在我的情况下没有其他方法 :/)

问题只关于 负面影响,而不是可行性!


1
你真的需要将数据转换为java.lang.String吗?为什么不能直接处理字节序列呢? - Ray Toal
这是由于技术原因:D 我只说“Minecraft” :/ - maxammann
我不知道Minecraft需要角色!不过,关于你如何丢失数据的问题,原因是无效的UTF-8序列将被编码为替换字符。我不确定下面的回答是否正确。 - Ray Toal
是的,差不多就是这样,但还是谢谢你:D 如果你想要在Minecraft中保存数据到物品中,你必须使用字符串:/ - maxammann
3个回答

23

你应该绝对使用base64或可能的hex。(两者都可以,但是base64更紧凑但对人类来说更难读。)

你声称"两种变体都完美工作",但事实并非如此。如果你使用第一种方法,而data不是有效的UTF-8序列,那么就会丢失数据。你并不尝试将UTF-8编码的文本转换为字符串,因此不要编写尝试这样做的代码。

使用ISO-8859-1作为编码将保留所有数据——但在很多情况下,返回的字符串将无法轻松地传输到其他协议中。例如,它很可能包含无法打印的控制字符。

只有在你拥有固有文本数据(以编码形式指定为第二个参数)时,才使用String(byte[], String)构造函数。对于任何其他类型的数据——音乐、视频、图像、加密或压缩数据,只是举例——你应该使用一种把传入数据视为"任意二进制数据"的方法,并找到它的文本编码... 这正是base64和hex所做的。


1
@PeterLawrey:我完全不理解你的第一句话,也不知道它与第二句话有什么关联... - Jon Skeet
2
@p000ison UTF-8在每个组合中并不使用所有可能的字节值,这意味着有些组合是无效的。有些组合会产生与其他组合相同的 char,这意味着无法确定原始的byte[]是什么。 - Peter Lawrey
2
@PeterLawrey:我认为UTF-8不允许一个字符有多个有效编码。根据维基百科:“标准规定,代码点的正确编码只使用最少数量的字节来保存代码点的有效位。更长的编码称为过长编码,不是代码点的有效UTF-8表示。” - Jon Skeet
Java的UTF-8解码器将0b11000000、0b100000000视为两个字符。 - Peter Lawrey
1
@PeterLawrey:你是不是指第二个应该是0b10000000?据我所知,这是一个过长的编码。Java将其解码为U+FFFD U+FFFD,其中U+FFFD是替换字符 - 实际上是拒绝它的正确方式。根据你对byte[]“有效的UTF-8编码”的描述,我不认为它符合这个要求。 - Jon Skeet
显示剩余3条评论

6
您可以将一个字节存储在字符串中,但这不是一个好主意。您不能使用UTF-8,因为它会处理字节,但更快、更有效的方法是使用ISO-8859-1编码或纯8位。最简单的方法是使用。
String s1 = new String(data, 0);

或者

String s1 = new String(data, "ISO-8859-1");

来自维基百科上的UTF-8, 正如Jon Skeet所指出的,这些编码在标准下是无效的。在Java中,它们的行为各不相同。DataInputStream在前三个版本中将它们视为相同的,而在接下来的两个版本中会抛出异常。字符集解码器会默默地将它们视为单独的字符。

00000000 is \0
11000000 10000000 is \0
11100000 10000000 10000000 is \0
11110000 10000000 10000000 10000000 is \0
11111000 10000000 10000000 10000000 10000000 is \0
11111100 10000000 10000000 10000000 10000000 10000000 is \0

这意味着如果您在字符串中看到\0,您无法确定原始byte[]值是什么。 DataOutputStream使用第二个选项来与C兼容,因为C将\0视为终止符。
BTW,DataOutputStream不知道代码点,因此会使用UTF-16编码高代码点字符,然后使用UTF-8编码。
0xFE和0xFF不能出现在字符中。 0x11000000+的值只能出现在字符的开头,而不能出现在多字节字符内部。

1
谢谢,现在一切都清楚了,但我希望我能接受两个答案 :D - maxammann
那个我不熟悉的 0 和标准方法 ISO-8859-1 有什么区别?前者是后者的简写吗? - WestCoastProjects
@javadba ISO-8859-1 会将不支持的字符编码为 ?,而如果你只取低8位,可能会得到一个随机的字符。 - Peter Lawrey

3

通过Java确认了被接受的答案。重申一下,UTF-8、UTF-16不能保存所有的字节值,而ISO-8859-1可以保存所有的字节值。但是如果编码后的字节需要在JVM之外传输,建议使用Base64。

@Test
public void testBase64() {
    final byte[] original = enumerate();
    final String encoded = Base64.encodeBase64String( original );
    final byte[] decoded = Base64.decodeBase64( encoded );
    assertTrue( "Base64 preserves bytes", Arrays.equals( original, decoded ) );
}

@Test
public void testIso8859() {
    final byte[] original = enumerate();
    String s = new String( original, StandardCharsets.ISO_8859_1 );
    final byte[] decoded = s.getBytes( StandardCharsets.ISO_8859_1 );
    assertTrue( "ISO-8859-1 preserves bytes", Arrays.equals( original, decoded ) );
}

@Test
public void testUtf16() {
    final byte[] original = enumerate();
    String s = new String( original, StandardCharsets.UTF_16 );
    final byte[] decoded = s.getBytes( StandardCharsets.UTF_16 );
    assertFalse( "UTF-16 does not preserve bytes", Arrays.equals( original, decoded ) );
}

@Test
public void testUtf8() {
    final byte[] original = enumerate();
    String s = new String( original, StandardCharsets.UTF_8 );
    final byte[] decoded = s.getBytes( StandardCharsets.UTF_8 );
    assertFalse( "UTF-8 does not preserve bytes", Arrays.equals( original, decoded ) );
}

@Test
public void testEnumerate() {
    final Set<Byte> byteSet = new HashSet<>();
    final byte[] bytes = enumerate();
    for ( byte b : bytes ) {
        byteSet.add( b );
    }
    assertEquals( "Expecting 256 distinct values of byte.", 256, byteSet.size() );
}

/**
 * Enumerates all the byte values.
 */
private byte[] enumerate() {
    final int length = Byte.MAX_VALUE - Byte.MIN_VALUE + 1;
    final byte[] bytes = new byte[length];
    for ( int i = 0; i < length; i++ ) {
        bytes[i] = (byte)(i + Byte.MIN_VALUE);
    }
    return bytes;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接