字符集的概念实际上包含两个方面,一个是字符的集合,一个是编码方案。通常来说,一个字符集不仅仅定义字符集合,它还为每个符号定义一个二进制编码。例如当我们提到GB2312或者ASCII的时候,它隐式地指明了编码方案是GB2312或者ASCII。
但是Unicode字符集例外,它存在着几种不同的编码方式,例如:
其中UTF-8和UTF-16采用可变长度编码,UTF-32固定采用固定长度编码;
维基百科对Unicode的描述如下
Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位可以用来映射字符;Unicode的编码空间可划分为17个平面,每个平面包括65,536(即2^16) 个码位。17个平面的码位可表示为U+xx0000到U+xxFFFF,其中xx表示平面,从0x00到0x10。第一个平面称为基本多语言平面,其它平面称为辅助平面.基本多语言平面内的U+D800到U+DFFF之间的码位是永久保留的,不会映射到任何Unicode字符。
UTF-8使用1至4个字节为每个字符编码:
对于UTF-8编码中的任意字节B,
- 如果B的第一位为0,则B独立的表示一个字符(ASCII码);
例如,希伯来语字母aleph(א)的Unicode代码是U+05D0,按照以下方法改成UTF-8:
码位范围为U+0000到U+FFFF,包含了最常见的字符,UTF-16将这个范围内的码位编码为2个字节,数值等于对应的Unicode码位即0x0000至0xFFFF。
码位范围为U+10000到U+10FFFF,UTF-16将这个范围内的码位编码为4个字节,称为代理对(surrogate pair)。
具体的编码方式如下:
综上所述,前导代理、后尾代理和基本语言平面的码位,三者互不重叠,因此可以通过检查一个码元就可以判定给定字符的下一个字符的起始码元,这意味着UTF-16是自同步的。
例如U+10437编码:
UTF-16编码存在三种编码格式:
UTF-16BE:
Big Endian,最低位地址存放高位字节
UTF-16LE:
Little Endian,最高位地址存放高位字节
UTF-16:
高字节在前还是低字节在前有流中的前两个字节确定,FEFF表示Big Endian,FFFE表示Little Endian;
public class TestUTF {
public static void main(String[] args) throws Exception {
String str = "中";
//------------编码
//Java里使用的是UTF-16BE方式来存储数据的
System.out.println(Integer.toHexString(str.charAt(0)).toUpperCase());//4E2D
/*
* 进行编码时,因为 UTF-16 编码方式本身未指定字节顺序标记,所以默认使用 Big Endian 字节
* 顺序编码,并将 Big Endian 字节顺序标记写入到流中,所以流前面多了 FE FF 二字节的高字节
* 顺序标记
*/
System.out.println(byteToHex(str.getBytes("utf-16")));//FE FF 4E 2D
/*
* 进行编码时,UTF-16BE 和 UTF-16LE charset 不会将字节顺序标记写入到流中
* 即它们所编出的码每个字符只占二个字节,要注意的是解码时要使用同样的编码
* 方式,不然会出现问题乱码
*/
System.out.println(byteToHex(str.getBytes("utf-16BE")));//4E 2D
System.out.println(byteToHex(str.getBytes("utf-16LE")));//2D 4E
//使用 utf-16BE 对高字节序进行解码,忽略字节顺序标记,即不会将流前二字节内容看作字节序标记
System.out.println(new String(new byte[]{0x4E, 0x2D}, "utf-16BE"));// 中
//使用 utf-16LE 对低字节序进行解码,忽略字节顺序标记,即不会将流前二字节内容看作字节序标记
System.out.println(new String(new byte[]{0x2D, 0x4E}, "utf-16LE"));// 中
//------------解码
/*
* 使用 utf-16 进行解码时,会根据流前两字节内部来确定是低还是高字节顺序,如果流的前两字节
* 内部不是 高字节序 FE FF,也不是低字节序 FF FE时,则默认使用 高字节序 方式来解码
*/
//因为0x4E,0x2D为“中”字的高字节表示,所以前面需要加上 FE FF 字节顺序标记来指示它
System.out.println(new String(new byte[]{(byte) 0xFE, (byte) 0xFF, 0x4E, 0x2D}, "utf-16"));//中
//因为0x2D,0x4E为“中”字的低字节表示,所以前面需要加上 FF FE 字节顺序标记来指示它
System.out.println(new String(new byte[]{(byte) 0xFF, (byte) 0xFE, 0x2D, 0x4E,}, "utf-16"));//中
//使用默认 高字节顺序 方式来解码,
System.out.println(new String(new byte[]{0x4E, 0x2D}, "utf-16"));//中
//因为 0x2D,0x4E 为“中”的低字节序,但 utf-16 默认却是以 高字节序来解的,所以出现乱码
System.out.println(new String(new byte[]{0x2D, 0x4E,}, "utf-16"));//?
}
public static String byteToHex(byte[] bt) {
StringBuilder sb = new StringBuilder(4);
for (int b : bt) {
sb.append(Integer.toHexString(b&0xff).toUpperCase());
sb.append(" ");
}
return sb.toString();
}
}
Windows平台,在UTF-8文件的开首,很多时候都放置一个U+FEFF字符(UTF-8以EF,BB,BF代表),以表明这个文本文件是以UTF-8编码
采用4个字节进行编码,就空间而已,其效率最差;另外其不像UTF-16,可以很容易的判断出下一个字符的开始位置,因此并不如其它Unicode编码用得广泛;
Java虚拟机规范中明确说明了java的char类型使用的编码方案是UTF-16,而我们知道char类型由2个字节存储,这两个字节实际上存储的就是UTF-16编码下的码元;而通过前文可以知道,对于辅助平面字符,需要由4个字节来进行表述;因此我们通过charAt或length方法返回的码元或码元数量只是对于基本语言平面字符正确;正确的处理方式如下:
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
}
可以看到,此时返回结果为int类型,而不是char,因为char由2个字节表示,而辅助平面字符需要4个字节才能表示。因此Java中如果参数是char,则说明不支持辅助平面字符;如果为int,则支持基本平面和辅助平面字符;具体的方法可以参加Character类;
如果所述,我们知道Java是采用UTF-16 BigEndian存储字符的,那么如果跨语言调用,比如JNI对字符是如何处理的呢?
JNI中提供了函数GetStringUTFChars函数,将字符从UTF-16转化为UTF-8:
JNI_ENTRY(const char*, jni_GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy))
JNIWrapper("GetStringUTFChars");
#ifndef USDT2
DTRACE_PROBE3(hotspot_jni, GetStringUTFChars__entry, env, string, isCopy);
#else /* USDT2 */
HOTSPOT_JNI_GETSTRINGUTFCHARS_ENTRY(
env, string, (uintptr_t *) isCopy);
#endif /* USDT2 */
oop java_string = JNIHandles::resolve_non_null(string);
size_t length = java_lang_String::utf8_length(java_string);
char* result = AllocateHeap(length + 1, "GetStringUTFChars");
java_lang_String::as_utf8_string(java_string, result, (int) length + 1);
if (isCopy != NULL) *isCopy = JNI_TRUE;
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, GetStringUTFChars__return, result);
#else /* USDT2 */
HOTSPOT_JNI_GETSTRINGUTFCHARS_RETURN(
result);
#endif /* USDT2 */
return result;
JNI_END
可以看到它是通过java_lang_String::as_utf8_string方法进行转换的:
char* java_lang_String::as_utf8_string(oop java_string, char* buf, int buflen) {
typeArrayOop value = java_lang_String::value(java_string);
int offset = java_lang_String::offset(java_string);
int length = java_lang_String::length(java_string);
jchar* position = (length == 0) ? NULL : value->char_at_addr(offset);
return UNICODE::as_utf8(position, length, buf, buflen);
}
char* UNICODE::as_utf8(jchar* base, int length, char* buf, int buflen) {
u_char* p = (u_char*)buf;
u_char* end = (u_char*)buf + buflen;
for (int index = 0; index < length; index++) {
jchar c = base[index];
if (p + utf8_size(c) >= end) break; // string is truncated
p = utf8_write(p, base[index]);
}
*p = '\0';
return buf;
}
static u_char* utf8_write(u_char* base, jchar ch) {
if ((ch != 0) && (ch <=0x7f)) {//对于基本语言平面字符,UTF-16编码的数值和UTF-8相同,比如字符"a",UTF-16 BE编码为"0x0061",UTF-8为0x61
base[0] = (u_char) ch;
return base + 1;
}
//对于UTF-16编码,0xFFFF范围内的编码和Unicode编码相同;对于UTF-8,0x80-0x7FF范围内第一个字节由110开始,接着单字节由10开始,共1920个码位,占用2个字节
if (ch <= 0x7FF) {
/* 11 bits or less. */
unsigned char high_five = ch >> 6;
unsigned char low_six = ch & 0x3F;
base[0] = high_five | 0xC0; /* 110xxxxx */
base[1] = low_six | 0x80; /* 10xxxxxx */
return base + 2;
}
//对于UTF-8,0x900-0xD7FF,0xE000-0xFFFF范围内第一个字节由1110开始,接着的字节由10开始;占用3个字节;可以看到,此处并不支持辅助平面字符;
/* possibly full 16 bits. */
char high_four = ch >> 12;
char mid_six = (ch >> 6) & 0x3F;
char low_six = ch & 0x3f;
base[0] = high_four | 0xE0; /* 1110xxxx */
base[1] = mid_six | 0x80; /* 10xxxxxx */
base[2] = low_six | 0x80; /* 10xxxxxx */
return base + 3;
}
int UNICODE::utf8_size(jchar c) {
if ((0x0001 <= c) && (c <= 0x007F)) return 1;//US-ASCII, UTF-8占用一个字节
if (c <= 0x07FF) return 2;//UTF-8编码占用两个字节
return 3;//UTF-8编码占用三个字节
}