【问题标题】:Why is Java String.length inconsistent across platforms with unicode characters?为什么 Java String.length 跨平台与 unicode 字符不一致?
【发布时间】:2019-10-07 10:05:12
【问题描述】:

根据Java documentation for String.length

公共整数长度()

返回此字符串的长度。

长度等于字符串中Unicode代码单元的个数。

指定者:

接口 CharSequence 中的长度

返回:

序列的长度 此对象表示的字符数。

但是我不明白为什么下面的程序 HelloUnicode.java 在不同的平台上会产生不同的结果。按照我的理解,Unicode码单元的个数应该是一样的,因为Java supposedly always represents strings in UTF-16

public class HelloWorld {

    public static void main(String[] args) {
        String myString = "I have a ???? in my string";
        System.out.println("String: " + myString);
        System.out.println("Bytes: " + bytesToHex(myString.getBytes()));
        System.out.println("String Length: " + myString.length());
        System.out.println("Byte Length: " + myString.getBytes().length);
        System.out.println("Substring 9 - 13: " + myString.substring(9, 13));
        System.out.println("Substring Bytes: " + bytesToHex(myString.substring(9, 13).getBytes()));
    }

    // Code from https://stackoverflow.com/a/9855338/4019986
    private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

}

这个程序在我的 Windows 机器上的输出是:

String: I have a ???? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 26
Byte Length: 26
Substring 9 - 13: ????
Substring Bytes: F09F9982

我的 CentOS 7 机器上的输出是:

String: I have a ???? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 24
Byte Length: 26
Substring 9 - 13: ???? i
Substring Bytes: F09F99822069

我都使用 Java 1.8 运行。相同的字节长度,不同的字符串长度。为什么?

更新

通过替换“????”在带有“\uD83D\uDE42”的字符串中,我得到以下结果:

窗户:

String: I have a ? in my string
Bytes: 4920686176652061203F20696E206D7920737472696E67
String Length: 24
Byte Length: 23
Substring 9 - 13: ? i
Substring Bytes: 3F2069

CentOS:

String: I have a ???? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 24
Byte Length: 26
Substring 9 - 13: ???? i
Substring Bytes: F09F99822069

为什么 "\uD83D\uDE42" 在 Windows 机器上最终被编码为 0x3F 是我无法理解的......

Java 版本:

窗户:

java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

CentOS:

openjdk version "1.8.0_201"
OpenJDK Runtime Environment (build 1.8.0_201-b09)
OpenJDK 64-Bit Server VM (build 25.201-b09, mixed mode)

更新 2

使用.getBytes("utf-8"),带有“????”嵌入在字符串文字中,这里是输出。

窗户:

String: I have a ???? in my string
Bytes: 492068617665206120C3B0C5B8E284A2E2809A20696E206D7920737472696E67
String Length: 26
Byte Length: 32
Substring 9 - 13: ????
Substring Bytes: C3B0C5B8E284A2E2809A

CentOS:

String: I have a ???? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 24
Byte Length: 26
Substring 9 - 13: ???? i
Substring Bytes: F09F99822069

所以是的,它似乎是系统编码的差异。但这意味着字符串文字在不同平台上的编码方式不同?这听起来在某些情况下可能会出现问题。

另外...在 Windows 中代表笑脸的字节序列 C3B0C5B8E284A2E2809A 来自哪里?这对我来说没有意义。

为了完整起见,使用.getBytes("utf-16"),加上“????”嵌入在字符串文字中,这里是输出。

窗户:

String: I have a ???? in my string
Bytes: FEFF00490020006800610076006500200061002000F001782122201A00200069006E0020006D007900200073007400720069006E0067
String Length: 26
Byte Length: 54
Substring 9 - 13: ????
Substring Bytes: FEFF00F001782122201A

CentOS:

String: I have a ???? in my string
Bytes: FEFF004900200068006100760065002000610020D83DDE4200200069006E0020006D007900200073007400720069006E0067
String Length: 24
Byte Length: 50
Substring 9 - 13: ???? i
Substring Bytes: FEFFD83DDE4200200069

【问题讨论】:

  • 请显示字节数组的确切内容(最好是十六进制)并使用\uD83D\uDE42序列而不是????在代码中
  • @MichalKordas 感谢您的建议。我在对问题的更新中提到了他们。
  • 你能用getBytes("UTF-8")getBytes("UTF-16")吗?还要确保 STDOUT 也使用 UTF-8(或者更好的是,改为使用指定的编码写入文件)。
  • "为什么 "\uD83D\uDE42" 在 Windows 机器上最终被编码为 0x3F 是我无法理解的......" 0x3f 是问号。 Java 在要求输出无效字符时将其放入。所以看起来它只是用 ? 替换了你的笑脸。因为您没有在getBytes 中指定Unicode,所以它默认为平台编码。
  • @Thilo 但它似乎已将 "\uD83D\uDE42" 替换为 "?"在解释字符串文字时,而不是通过getBytes 转换为字节时。似乎 Windows Java 只是不知道如何处理“\uD83D\uDE42”。

标签: java string encoding


【解决方案1】:

您没有考虑到 getBytes() 返回平台默认编码中的字节。这在 windows 和 centOS 上是不同的。

另请参阅How to Find the Default Charset/Encoding in Java?String.getBytes() 上的 API 文档。

【讨论】:

  • 这如何解释不同的长度?
  • 使用转义序列后长度没有区别。以前不同,因为在编译期间使用了平台编码(并且已经在 Windows 上中断了)。
  • 没错,所以我认为这个答案并不能解释问题
  • 它解释了一个不同的方面,这对于全面理解很重要并且包含有价值的学习效果:在第一个示例中,字节是相同的。那么如果字节相同,字符串长度怎么可能不同呢?原因是 getBytes() 中缺少编码。学习:永远不要忽略编码或为讨厌的惊喜做好准备。这对于在日期/时间 API 中省略时区也是有效的。 .
【解决方案2】:

您必须小心指定编码:

  • 当您编译 Java 文件时,它对源文件使用某种编码。我的猜测是,这已经破坏了您在编译时的原始字符串文字。这可以通过使用转义序列来解决。
  • 使用转义序列后,String.length 相同。字符串中的字节也是一样的,但是你打印出来的并没有显示出来。
  • 打印的字节不同,因为您调用了getBytes(),并且再次使用了环境或平台特定的编码。所以它也被破坏了(用问号替换不可编码的表情符号)。您需要调用getBytes("UTF-8") 以独立于平台。

所以回答提出的具体问题:

相同的字节长度,不同的字符串长度。为什么?

因为字符串字面量是由java编译器编码的,而java编译器在不同的系统上往往默认使用不同的编码。这可能会导致每个 Unicode 字符的字符单元数不同,从而导致字符串长度不同。跨平台传递具有相同选项的-encoding 命令行选项将使它们编码一致。

为什么 "\uD83D\uDE42" 在 Windows 机器上最终被编码为 0x3F 是我无法理解的......

它没有在字符串中编码为 0x3F。 0x3f 是问号。当 Java 被要求通过 System.out.printlngetBytes 输出无效字符时,Java 会将其放入其中,当您使用不同编码将文字 UTF-16 表示形式编码为字符串然后尝试将其打印到控制台时,就会出现这种情况getBytes 来自它。

但这意味着字符串文字在不同平台上的编码方式不同?

默认情况下是的。

另外...在 Windows 中表示笑脸的字节序列 C3B0C5B8E284A2E2809A 来自哪里?

这很令人费解。 “?”字符(Unicode 代码点 U+1F642)使用字节序列 F0 9F 99 82 以 UTF-8 编码存储在 Java 源文件中。Java 编译器然后使用平台默认编码 Cp1252 ( Windows-1252),因此它将这些 UTF-8 字节视为 Cp1252 字符,通过将每个字节从 Cp1252 转换为 Unicode 来生成 4 个字符的字符串,从而得到 U+00F0 U+0178 U+2122 U+201A。 getBytes("utf-8") 调用然后通过将这 4 个字符的字符串编码为 utf-8 将它们转换为字节。由于字符串的每个字符都高于十六进制 7F,因此每个字符都转换为 2 个或更多 UTF-8 字节;因此生成的字符串这么长。该字符串的值不重要;这只是使用错误编码的结果。

【讨论】:

  • 关于检查源文件编码的要点是一个我没有想到的好方法,但实际上这两个文件都是用 UTF-8 编码的。唯一的区别是行尾(Windows 使用 CRLF,而 CentOS 当然只使用 LF)。为什么每个平台会以不同的方式解释文字“?”对我来说仍然没有意义......
  • @NanoWizard 我怀疑您仍在 Windows 上使用平台相关的源编码。我刚刚在带有 javac 1.8.0_212 的 Windows 上使用命令 javac -encoding utf-8 &lt;source-file&gt; 使用您的代码剪切并粘贴到保存为 UTF-8 的 IntelliJ 中进行了尝试,并且报告的字符串长度确实为 24,与 CentOS 上相同。确保使用javac -encoding 命令行选项!
  • 更可能是 cp1252,这是美国和“西方”版本的 Windows 的默认设置。 UTF-8 中的 U+1F642 是 F0 9F 99 82,解释为 cp1252 的那些字节是 U+00F0 U+0178 U+2122 U+201A 然后 UTF-8 编码为 C3 B0,C5 B8,E2 84 A2, E2 80 9A。在 cp1250 中,9F 将改为 U+017A 并编码为 C5 BA。
  • 好点@dave_thompson_085。我实际上已经确定 cp1252 是系统编码,然后在添加到这个答案时错误地写了 cp1250。
猜你喜欢
  • 1970-01-01
  • 2011-05-09
  • 1970-01-01
  • 2010-09-07
  • 2015-04-07
  • 2014-01-24
  • 2017-06-26
  • 2011-05-23
  • 1970-01-01
相关资源
最近更新 更多