Java 语言中一个字符占几个字节?

Java中理论说是一个字符(汉字 字母)占用两个字节。 但是在UTF-8的时候 new String("字").getBytes().length 返回的是3 表示3个字节 小白求回答。。
关注者
130
被浏览
21991

4 个回答

题主要区分清楚内码(internal encoding)和外码(external encoding)就好了。
内码是程序内部使用的字符编码,特别是某种语言实现其char或String类型在内存里用的内部编码;
外码是程序与外部交互时外部使用的字符编码。“外部”相对“内部”而言;不是char或String在内存里用的内部编码的地方都可以认为是“外部”。例如,外部可以是序列化之后的char或String,或者外部的文件、命令行参数之类的。

Java语言规范规定,Java的char类型是UTF-16的code unit,也就是一定是16位(2字节);
char, whose values are 16-bit unsigned integers representing UTF-16 code units (§3.1).
然后字符串是UTF-16 code unit的序列:
The Java programming language represents text in sequences of 16-bit code units, using the UTF-16 encoding.
这样,Java规定了字符的内码要用UTF-16编码。或者至少要让用户无法感知到String内部采用了非UTF-16的编码。
“感知”可以是多方面的,例如随机访问某个下标的code unit(String.charAt())应该是O(1)操作,这只有使用UTF-16或者别的“定长”编码才可以做到。注意我这里说的“定长”特指code unit定长,而不是说code point定长。

String.getBytes()是一个用于将String的内码转换为指定的外码的方法。无参数版使用平台的默认编码作为外码,有参数版使用参数指定的编码作为外码;将String的内容用外码编码好,结果放在一个新byte[]返回。

题主的例子里,显然外码是UTF-8,那么调用了String.getBytes()之后得到的byte[]只能表明该外码的性质,而无法碰触到String内码的任何特质。

另举一例:
Java标准库实现的对char与String的序列化规定使用UTF-8作为外码。Java的Class文件中的字符串常量与符号名字也都规定用UTF-8编码。这大概是当时设计者为了平衡运行时的时间效率(采用定长编码的UTF-16)与外部存储的空间效率(采用变长的UTF-8编码)而做的取舍。

题外话1:
可惜UTF-16在Java设计之初还是真的定长编码,后来Unicode涵盖的字符变多了之后UTF-16变成了坑爹的变长编码(一个完整的“字符”是一个code point;一个code point可以对应1到2个code unit;一个code unit是16位),Java也只好跟进。
为了实现UTF-16的变长编码语义,Java规定char仍然只能是一个16位的code point,也就是说Java的char类型不一定能表示一个UTF-16的“字符”——只有只需1个code unit的code point才可以完整的存在char里。但String作为char的序列,可以包含由两个code unit组成的“surrogate pair”来表示需要2个code unit表示的UTF-16 code point。为此Java的标准库新加了一套用于访问code point的API,而这套API就表现出了UTF-16的变长特性。

题外话2:
前面我说Java的内码时说得比较松,留下了“不总是使用UTF-16作为内码,但是用户无法感知区别”的余地。
在Sun JDK6中有一个“压缩字符串”(-XX:+UseCompressedString)的功能。启用后,String内部存储字符串内容可能用byte[],也可能用char[];当整个字符串所有字符都在ASCII编码范围内时,就使用byte[](ASCII序列)来存储,此时字符串就处于“压缩”状态;反之,只要有任何一个字符超出了ASCII的编码范围,就退回到用char[](UTF-16序列)来存储。
ASCII编码也是一种定长编码,而且其涵盖的字符是UTF-16的真子集;用户在对一个“压缩”的字符串访问其内容时(例如String.charAt()),只需对ASCII字符做无符号扩展就可以得到对应的UTF-16 code unit。这样用户也就无法感知到Java String的内码不是UTF-16的情况。

Sun JDK6对“压缩字符串”的实现不够理想,实现太复杂而效果未如预期的好,所以没有包含在OpenJDK6、Oracle JDK7/OpenJDK7里。
现在Oracle在重新审视“压缩字符串”功能,有可能在JDK9重新实现出来。

题外话3:
同样规定使用UTF-16作为内码的JavaScript语言,其实现广泛应用了“压缩字符串”的思想。现在主流的JavaScript引擎都会尽可能用ASCII内码的字符串,不过用户能接触的API只能看到UTF-16 code unit。

更多了解可参考我的这个字符集编码与乱码专题:字符集编码与乱码 - 肖国栋的i自留地,里面有更详细的介绍,包括很多图示及代码的例子。

-----------------------------------------------------------------

首先,你所谓的“字符”具体指什么呢?

如果你说的“字符”就是指 Java 中的 char,那好,那它就是 16 位,2 字节。

如果你说的“字符”是指我们用眼睛看到的那些“抽象的字符”,那么,谈论它占几个字节是没有意义的。

具体地讲,脱离具体的编码谈某个字符占几个字节是没有意义的

就好比有一个抽象的整数“42”,你说它占几个字节?这得具体看你是用 byte,short,int,还是 long 来存它。 用 byte 存就占一字节,用 short 存就占两字节,int 通常是四字节,long 通常八字节。 当然,如果你用 byte,受限于它有限的位数,有些数它是存不了的,比如 256 就无法放在一个 byte 里了。

字符是同样的道理,如果你想谈“占几个字节”,就要先把编码说清楚。

同一个字符在不同的编码下可能占不同的字节

就以你举的“”字为例,“”在 GBK 编码下占 2 字节,在 UTF-16 编码下也占 2 字节,在 UTF-8 编码下占 3 字节,在 UTF-32 编码下占 4 字节。

不同的字符在同一个编码下也可能占不同的字节

”在 UTF-8 编码下占3字节,而“A”在 UTF-8 编码下占 1 字节。(因为 UTF-8 是变长编码)

而 Java 中的 char 本质上是 UTF-16 编码。而 UTF-16 实际上也是一个变长编码(2 字节或 4字节)。

如果一个抽象的字符在 UTF-16 编码下占 4 字节,显然它是不能放到 char 中的。换言之, char 中只能放 UTF-16 编码下只占 2 字节的那些字符。

而 getBytes 实际是做编码转换,你应该显式传入一个参数来指定编码,否则它会使用缺省编码来转换。

你说“ new String("字").getBytes().length 返回的是3 ”,这说明缺省编码是 UTF-8. 如果你显式地传入一个参数,比如这样“ new String("字").getBytes("GBK").length ”,那么返回就是 2.

你可以在启动 JVM 时设置一个缺省编码,

假设你的类叫 Main,那么在命令行中用 java 执行这个类时可以通过 file.encoding 参数设置一个缺省编码。 比如这样:java -Dfile.encoding=GBK Main 这时,你再执行不带参数的 getBytes() 方法时,new String("字").getBytes().length 返回的就是 2 了,因为现在缺省编码变成 GBK 了。 当然,如果这时你显式地指定编码,new String("字").getBytes("UTF-8").length 返回的则依旧是 3.

否则,会使用所在操作系统环境下的缺省编码。

通常,Windows 系统下是 GBK,Linux 和 Mac 是 UTF-8. 但有一点要注意,在 Windows 下使用 IDE 来运行时,比如 Eclipse,如果你的工程的缺省编码是 UTF-8,在 IDE 中运行你的程序时,会加上上述的 -Dfile.encoding=UTF-8 参数,这时,即便你在 Windows 下,缺省编码也是 UTF-8,而不是 GBK。

由于受启动参数及所在操作系统环境的影响,不带参数的 getBytes 方法通常是不建议使用的,最好是显式地指定参数以此获得稳定的预期行为。