20200227 java中的字符,字节和编码
java中的字符,字节和编码
字符与编码的发展
从计算机对多国语言的支持角度看,大致可以分为三个阶段:
系统内码 | 说明 | 系统 | |
---|---|---|---|
阶段一 | ASCII | 计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。 | 英文 DOS |
阶段二 | ANSI编码 (本地化) | 为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。 不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。 | 中文 DOS,中文 Windows 95/98,日文 Windows 95/98 |
阶段三 | UNICODE (国际化) | 为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。 | Windows NT/2000/XP,Linux,Java |
字符串在内存中的存放方法:
- 在 ASCII 阶段,单字节字符串使用一个字节存放一个字符(SBCS)。比如,”Bob123” 在内存中为7个字节
- 在使用 ANSI 编码支持多种语言阶段,每个字符使用一个字节或多个字节来表示(MBCS),因此,这种方式存放的字符也被称作多字节字符。比如,”中文123” 在中文 Windows 95 内存中为7个字节,每个汉字占2个字节,每个英文和数字字符占1个字节
- 在 UNICODE 被采用之后,计算机存放字符串时,改为存放每个字符在 UNICODE 字符集中的序号。目前计算机一般使用 2 个字节(16 位)来存放一个序号(DBCS),因此,这种方式存放的字符也被称作宽字节字符。比如,字符串 “中文123” 在 Windows 2000 下,内存中实际存放的是 5 个序号,一共占 10 个字节。
Ascii及中国的编码
Ascii(American Standard Code for Information Interchange),美国信息互换标准代码,官方的ASCII码表。
Ascii为八位一个字节,一共可以组合出256(2的8次方)种不同的状态。
一开始美国人把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上00x10, 终端就换行,例好遇上0x1b, 打印机就打印反白的字,或者终端就用彩色显示字母。这些0x20以下的字节状态称为”控制码”。
接着把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。
后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的。他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称”扩展字符集”。
我国创造了两个字节编码的“GB2312”以及之后的同样是两个字节的“GBK”。之后“GBK”扩展为“GB18030”,这些编码标准被通称为 “DBCS”(Double Byte Charecter Set 双字节字符集)。
DBSC系列的标准的最大特点是,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,如果一个字节小于127那么按照Ascii的编码,如果大于127那么其和后面的字符组成一个汉字字符。
然后各国也是按照自己的需求弄了一套自己可用的编码。很愉快大家都能用了,接着发现不对啊,各个国家之间电脑需要交流,你的资源到我这里编程乱码,我的资源你也用不了。
于是, ISO (国际标谁化组织)决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码。他们打算叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE”。
对于ascii里的那些“半角”字符(即前面排到128个字符),UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于”半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。
字符,字节,字符串
理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:
概念描述 | 举例 | |
---|---|---|
字符 | 人们使用的记号,抽象意义上的一个符号。 | ‘1’, ‘中’, ‘a’, ‘$’, ‘¥’, …… |
字节 | 计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。 | 0x01, 0x45, 0xFA, …… |
ANSI 字符串 | 在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串。 | “中文123” (占7字节) |
UNICODE 字符串 | 在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串。 | L”中文123” (占10字节) |
由于不同 ANSI 编码所规定的标准是不相同的,因此,对于一个给定的多字节字符串,我们必须知道它采用的是哪一种编码规则,才能够知道它包含了哪些“字符”。而对于UNICODE 字符串来说,不管在什么环境下,它所代表的“字符”内容总是不变的。
字符集与编码
各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的“字符”。比如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:
- 使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。
- 规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。
各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。
“UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。
常用的编码简介
简单介绍一下常用的编码规则,为后边的章节做一个准备。在这里,我们根据编码规则的特点,把所有的编码分成三类:
分类 | 编码标准 | 说明 |
---|---|---|
单字节字符编码 | ISO-8859-1 | 最简单的编码规则,每一个字节直接作为一个 UNICODE 字符。比如,[0xD6, 0xD0] 这两个字节,通过 iso-8859-1 转化为字符串时,将直接得到 [0x00D6, 0x00D0] 两个 UNICODE 字符,即 “ÖД。 反之,将 UNICODE 字符串通过 iso-8859-1 转化为字节串时,只能正常转化 0~255 范围的字符。 |
ANSI 编码 | GB2312, BIG5, Shift_JIS, ISO-8859-2 …… | 把 UNICODE 字符串通过 ANSI 编码转化为“字节串”时,根据各自编码的规定,一个 UNICODE 字符可能转化成一个字节或多个字节。 反之,将字节串转化成字符串时,也可能多个字节转化成一个字符。比如,[0xD6, 0xD0] 这两个字节,通过 GB2312 转化为字符串时,将得到 [0x4E2D] 一个字符,即 ‘中’ 字。 “ANSI 编码”的特点: 1. 这些“ANSI 编码标准”都只能处理各自语言范围之内的 UNICODE 字符。 2. “UNICODE 字符”与“转换出来的字节”之间的关系是人为规定的。 |
UNICODE 编码 | UTF-8, UTF-16, UnicodeBig …… | 与“ANSI 编码”类似的,把字符串通过 UNICODE 编码转化成“字节串”时,一个 UNICODE 字符可能转化成一个字节或多个字节。 与“ANSI 编码”不同的是: 1. 这些“UNICODE 编码”能够处理所有的 UNICODE 字符。 2. “UNICODE 字符”与“转换出来的字节”之间是可以通过计算得到的。 |
我们实际上没有必要去深究每一种编码具体把某一个字符编码成了哪几个字节,我们只需要知道“编码”的概念就是把“字符”转化成“字节”就可以了。对于“UNICODE 编码”,由于它们是可以通过计算得到的,因此,在特殊的场合,我们可以去了解某一种“UNICODE 编码”是怎样的规则。
Unicode及UTF
UNICODE 是用两个字节来表示为一个字符,他总共可以组合出65535不同的字符,这大概已经可以覆盖世界上所有文化的符号。UNICODE 如何在网络上传输也是一个必须考虑的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从UNICODE到UTF时并不是直接的对应,而是要过一些算法和规则来转换。而且网络传输字符编码也涉及到大端小端的问题。
Java中的字符与字节
类型或操作 | Java |
---|---|
字符 | char |
字节 | byte |
ANSI 字符串 | byte[] |
UNICODE 字符串 | String |
字节串→字符串 | string = new String(bytes, “encoding”) |
字符串→字节串 | bytes = string.getBytes(“encoding”) |
java中的编码
前面说了,java使用到编码是UNICODE。怎么具体“体会到”这种编码呢?我们可以用java中的转义符 \
。
一、我们直接使用“\”来转化数字为字符的话,后面的数字应为八进制。
而且只能转化一个字节大小,即255个字符,如下:
- 八进制转义序列:\ + 八进制数;范围’\000’’\377’(对应十进制0255)
- \0:空字符
有人问了Unicode不是两个字节吗,为什么这里一个字节就可以,其实java在这里会把它转化为两个字节按Unicode转换。记住是Unicode,不要因为一个字节就以为是ASCII编码,如下代码:
System.out.println('\367'); //这里输出的是 ÷
//八进制367转化为10进制为247
System.out.println((int) '÷');//输出十进制:247
//序号247在Ascii和Unicode对应的字符如下:
二、 Unicode转义字符:\u + 4个十六进制数字;对应十进制范围是0~65535
- \u0000:空字符
- \u0000-\uFFFF:我们电脑出现的每个字符都包含在这其中
三、 特殊字符:就3个
因为在Java中 双引号"
、引号'
、反斜杆\
都有特定的含义,双引号要包住字符串,引号要包住字符,反斜杠是转义符,所以我们通过在加一个转义符\
让他们真正代表他们自己。
\"
:双引号\'
:单引号\\
:反斜线
四、控制字符:5个
转义符\加固定字符在java中有五个,表示一定的控制操作
\r
:回车,return 到当前行的最左边。\n
:换行,向下移动一行,并不移动左右。\f
:走纸换页\t
:横向跳格\b
:退格
Linux中\n
表示回车+换行;
Windows中\r\n
表示回车+换行。
中文的半角和全角
在DBCS系列的编码里面,把连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。所以如果这些例如字母使用一个字节编码就是“半角”,两个字节就是“全角”。其实样子看起来还是有区别的:
几种误解,以及乱码产生的原因和解决办法
容易产生的误解
对编码的误解 | |
---|---|
误解一 | 在将“字节串”转化成“UNICODE 字符串”时,比如在读取文本文件时,或者通过网络传输文本时,容易将“字节串”简单地作为单字节字符串,采用每“一个字节”就是“一个字符”的方法进行转化。而实际上,在非英文的环境中,应该将“字节串”作为 ANSI 字符串,采用适当的编码来得到 UNICODE 字符串,有可能“多个字节”才能得到“一个字符”。 通常,一直在英文环境下做开发的程序员们,容易有这种误解。 |
误解二 | 在 DOS,Windows 98 等非 UNICODE 环境下,字符串都是以 ANSI 编码的字节形式存在的。这种以字节形式存在的字符串,必须知道是哪种编码才能被正确地使用。这使我们形成了一个惯性思维:“字符串的编码”。 当 UNICODE 被支持后,Java 中的 String 是以字符的“序号”来存储的,不是以“某种编码的字节”来存储的,因此已经不存在“字符串的编码”这个概念了。只有在“字符串”与“字节串”转化时,或者,将一个“字节串”当成一个 ANSI 字符串时,才有编码的概念。 不少的人都有这个误解。 |
在这里,我们可以看到,其中所讲的“误解一”,即采用每“一个字节”就是“一个字符”的转化方法,实际上也就等同于采用 iso-8859-1 进行转化。因此,我们常常使用 bytes = string.getBytes(“iso-8859-1”)
来进行逆向操作,得到原始的“字节串”。然后再使用正确的 ANSI 编码,比如 string = new String(bytes, “GB2312”)
,来得到正确的“UNICODE 字符串”。
非 UNICODE 程序在不同语言环境间移植时的乱码
非 UNICODE 程序中的字符串,都是以某种 ANSI 编码形式存在的。如果程序运行时的语言环境与开发时的语言环境不同,将会导致 ANSI 字符串的显示失败。
比如,在日文环境下开发的非 UNICODE 的日文程序界面,拿到中文环境下运行时,界面上将显示乱码。如果这个日文程序界面改为采用 UNICODE 来记录字符串,那么当在中文环境下运行时,界面上将可以显示正常的日文。
由于客观原因,有时候我们必须在中文操作系统下运行非 UNICODE 的日文软件,这时我们可以采用一些工具,比如,南极星,AppLocale 等,暂时的模拟不同的语言环境。
网页提交字符串
当页面中的表单提交字符串时,首先把字符串按照当前页面的编码,转化成字节串。然后再将每个字节转化成 “%XX” 的格式提交到 Web 服务器。比如,一个编码为 GB2312 的页面,提交 “中” 这个字符串时,提交给服务器的内容为 “%D6%D0”。
在服务器端,Web 服务器把收到的 “%D6%D0” 转化成 [0xD6, 0xD0] 两个字节,然后再根据 GB2312 编码规则得到 “中” 字。
在 Tomcat 服务器中,request.getParameter()
得到乱码时,常常是因为前面提到的“误解一”造成的。默认情况下,当提交 “%D6%D0” 给 Tomcat 服务器时,request.getParameter()
将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,而不是返回一个 “中” 字符。因此,我们需要使用 bytes = string.getBytes(“iso-8859-1”)
得到原始的字节串,再用 string = new String(bytes, “GB2312”)
重新得到正确的字符串 “中”。
从数据库读取字符串
通过数据库客户端(比如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。
如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的“误解一”造成的。解决的办法还是通过 string = new String(string.getBytes(“iso-8859-1”), “GB2312”)
的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。
几种错误理解的纠正
误解:“ISO-8859-1 是国际编码?”
非也。iso-8859-1 只是单字节字符集中最简单的一种,也就是“字节编号”与“UNICODE 字符编号”一致的那种编码规则。当我们要把一个“字节串”转化成“字符串”,而又不知道它是哪一种 ANSI 编码时,先暂时地把“每一个字节”作为“一个字符”进行转化,不会造成信息丢失。然后再使用 bytes = string.getBytes(“iso-8859-1”)
的方法可恢复到原始的字节串。
误解:“Java 中,怎样知道某个字符串的内码?”
Java 中,字符串类 java.lang.String
处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。
例子
// -------------------- Charset 和 StandardCharsets --------------------------- //
System.out.println(Charset.defaultCharset()); // UTF-8
System.out.println(Charset.availableCharsets());
System.out.println(Charset.isSupported("utf8")); // true
System.out.println(StandardCharsets.UTF_8); // UTF-8
System.out.println("中文123".getBytes(StandardCharsets.UTF_8).length); // 9
System.out.println("中文123".getBytes(Charset.forName("gbk")).length); // 7
// ---------------------- ÷ ------------------------- //
int x = 0367; // 八进制表示法
char c = '\367'; // 八进制转义序列
System.out.println((char) x); // ÷
System.out.println(c); // ÷
//八进制367转化为10进制为247
System.out.println('÷');// ÷
System.out.println((int) '÷');// 247
System.out.println((char) 247); // ÷
// ---------------------- 0 ------------------------- //
System.out.println('\0'); //
System.out.println((int) '\0'); // 0
System.out.println((char) 0); //
// ----------------------------------------------- //
//八进制367对应十六进制00F7,所以下面两个输出都是 ÷
System.out.println('\u00f7'); // ÷
System.out.println(Integer.toBinaryString('÷')); // 二进制 11110111
System.out.println(Integer.toOctalString('÷')); // 八进制 367
System.out.println(Integer.toHexString('÷')); // 十六进制 f7
System.out.println(Integer.toString('÷')); // 十进制 247
System.out.println(Integer.toString('÷', 4)); // n进制 3313
// -------------------- 特殊字符 --------------------------- //
System.out.println('\"'); // "
System.out.println('\''); // '
System.out.println('\\'); // \
// -------------------- 控制字符 --------------------------- //
System.out.println("aa\rbb"); // bb
System.out.println("cc\r\ndd"); // cc换行dd
System.out.println("12\r34\n56\f78\t90\b12"); // 34换行5678 912
// ----------------------------------------------- //
byte[] bytes = "中".getBytes(Charset.forName("gbk"));
for (byte aByte : bytes) {
System.out.println(Integer.toHexString(aByte));
}
// ffffffd6
// ffffffd0
byte bytes1 = 0xffffffd6;
byte bytes2 = 0xffffffd0;
byte[] bytes3 = new byte[]{bytes1, bytes2};
System.out.println(new String(bytes3, Charset.forName("gbk"))); // 中
}