我们知道在计算机里面存储的最小单位为bit,而8个bit构成了一个字节(byte)。字节是我们能用编程语言控制读写的最小单位。如果我们想一个bit一个bit写的话只能去对每个比特位进行缓存然后左移相加,当凑齐8个时再按照一个字节进行写入。
字符和字节是什么关系
字符,也会是我们常见的文字符号,比如A、B、C、1、2、3、你、好、啊,这些字符构成了我们的文字,而他们的组合又构成了我们的语言。当然如果狭义的来说,字符还包括很多看不见的转义字符。
字节是计算机存储的单位,而字符是人的单位。人的单位和计算机的单位又不是完全等同的,当我们想把字符在计算机上表示时,就必须要做一些转换,而这个转换规则,就是我们常说的编码。而我们常见的乱码原因其实都是源于写入编码和读取编码不一致。
在HTML的meta标签里有个charset属性,其作用就是设置读取的编码,如果不设置,则会按照操作系统默认的编码进行读取,在windows下为GBK,Linux下为UTF-8。下面一个最基本的网页,我们以UTF-8编码进行保存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
这是一段中文文字
</body>
</html>
在浏览器打开没问题,
当我们把
<meta charset="UTF-8">
这一行删掉再打开时,由于没设置编码,故浏览器会采用默认的GBK编码进行读取,所以就会出现下面的乱码。
编码的历史
最早的ASCII码
你可能经常听到什么GBK、UTF-8、ASCII、ISO-8859-1......这些乱七八糟的编码名词,心中可能会有疑问为什么会有这么多的编码,只存在一个不好吗,这样也就不存在那些恶心的乱码问题了。其实,这些东西表面上看起来很乱,如果我们从历史的角度上缕一缕,就会明白为什么会有这么多的编码了。
我们知道,计算机早期基本上都是美国人在研究和使用。为了表示方便,他们就想能否用一系列数字来代替那些字符。于是就出来ASCII就出来了,在ASCII码里,0-32有不表示字符,他们有自己特殊的含义,比如7代表响铃、10代表换行。从33到126每一个分别代表一个特定的字符,比如57代表阿拉伯数字9、85代表英文字母U。这样当计算机遇到这些数字时就会显示对应代表的字符。
ASCII码采用一个字节来编码一个字符,也就是计算机每读到一个字节,就会去ASCII码表里查找它对应的字节,然后把它给显示出来。
各种扩展ASCII编码百花齐放时代
随着世界的发展,计算机也逐渐传到各个国家,这时就存在一个问题了,如何在计算机上表示本国的文字。毕竟计算机毕竟是人家美国发明的,他们刚开始设计的时候也没有考虑太多,所以其他国家的文字如何在计算机上显示就成了一个问题了。就以中文为例,ascii码是采用一个字节即八位来表示一个字符的,所以理论上最多能存2^8即256种字符,就算这样,也没法完全表示所有的中文字符啊,就拿比较常用的《新华字典》来说,其里面也有10000多个汉字。而且如果我们要重写ascii码的话,那么在中文机器上又无法正确显示英文字符了。所以现在面临着两个问题
- 如何表示这些中文字符
- 在表示这些字符的时候如何做到与ascii码兼容
但这丝毫难不倒我们聪明的中国人民,既然一个字节表示不了,那我两个总可以了吧,我们知道两个字节也就是16位,最多能表示2^16即65536个字符,对付常见的中文字符是足够了。但是另一个问题该怎么解决呢,这时,我们聪明的中国人民又想到了,既然前127位被你用了,那我就从127后面开始编码不就行了,这样当读取的字节值小于127时,我们就把它当成ASCII码来处理,当读取到的值大于127时,我们就再把它后面一位字节读取下,然后把这两个字节翻译成对应的汉字。然后最早的中文编码GB2312就出来了,后来微软对GB2312进行扩展,同时向下兼容GB2312,制定了GBK编码,最早出现于Windows 95简体中文版中,这也是我们现在windows计算机中文默认使用的编码。后来国家又发布了GB 18030编码标准,对GB2312进行了一些补充。在这里需要补充的是GBK是微软的标准并不是国家标准。
下面我们用代码演示具体编码
下面一段代码用来获取字符串s1用GB2312编码后的字符数组并输出
@Test
public void testGbk() throws UnsupportedEncodingException
{
String s1 = "中国ABC";
byte[] bytes1 = s1.getBytes("GB2312");
for(byte b: bytes1)
{
System.out.print(b + " ");
}
}
得到下面输出结果
这里由于byte在java里默认是有符号类型的,所以最高位的1被当成符号位了输出了负数。具体解释下就是ascii码在0-127之间,所以对应的8位比特最高为永远为0,最大的127对应的8位比特为0111 1111,而GB2312是从128开始的,所以对应的第一个bit位位一,而在计算机中,第一位是当作符号位处理的,中文编码是从128开始的,这时第一位永远是1,所以会输出负值。( 具体怎么转换请搜索原码、反码、补码关键词)
我们对输出结果进行下改进,以便更直观的看到编码结果
@Test
public void testGbk() throws UnsupportedEncodingException
{
String s1 = "中国ABC";
byte[] bytes1 = s1.getBytes("GB2312");
for(byte b: bytes1)
{
System.out.print(Integer.toHexString(b & 0xff) + " ");
}
}
这里我们把它强转成int并通过& oxff屏蔽掉变成int后多出来的位,最后以16进制输出。结果如下
通过刚才以及现在的输出我们可以推断
d6 d0
是中
的GB2312码,b9 fa
是国
的GB2312码,而41 42 43
刚好对应ASCII码的41 42 43
。所以在GB2312码里中文字符占两个字节,英文字符占一个字节。我们把编码换成GBK,得到下面输出
跟刚才的一样。GBK是兼容GB2312的,可以把GB2312看成GBK的子集。
就这样,各个国家也都开始像中国这样通过扩展ASCII码来制定自己的编码,于是出现了一堆编码,比如繁体字的BIG5,日文的Shift_JIS,韩文的EUC_KR......但这些编码面临着一个很大的问题,它们之间互相都不兼容。
统一标准的Unicode
刚才提到了各个国家都通通的去制定自己的文字编码,但是这些文字编码又互相不兼容,所以当一份文字通过互联网在不同国家传递时很容易出现乱码问题。
后来出现了一个叫国际标准化组织ISO和Unicode的协会,他们像设计一个字符集,可以把全世界的文字都包含进去,以图统一编码。而这个字符集就是Unicode字符集。注意Unicode是一个字符集,不是某一个编码。我们知道中文是不包括拉丁文的,同样拉丁文也不包括俄罗斯文,所以Unicode就是一个即包含了中文,又包含了拉丁文、日文、俄文......的字符集,我们常用的emoji表情也是Unicode里面的。总之你可以在里面找到任何一个国家的字符。
而在Unicode里每一个字符都有对应的Unicode码,比如U+4E2D
代表中
, U+56fd
代表 国
。但是这只代表了符号的二进制码,而将它存入计算机依然需要进行编码。而Unicode对应的编码规则有很多,比如我们常见的UTF-8
,java里使用的 UTF-16BE
编码。而不同Unicode编码会有很大差别。
@Test
public void testUTF8() throws UnsupportedEncodingException
{
String s1 = "中国ABC";
byte[] bytes1 = s1.getBytes("UTF-8");
for(byte b: bytes1)
{
System.out.print(Integer.toHexString(b & 0xff) + " ");
}
}
得到如下结果
image.png
我们可以看到UTF-8编码中中文占三个字节,英文占1个字节
如果采用UTF-16BE
,会得到下面结果
在UTF-16BE编码中,不管是中文还是英文,都统统占用两个字节
可以发现UTF-8兼容ASCII码,所以用的比较多。
在unicode里有许多特殊转义字符,利用它我们可以做出许多特殊的效果。比如之前传的比较火的千万不要动到黑点,手机会卡死机 <> erehhcuot
这句话,其实就是Unicode特殊转义字符。在Unicode里有两个字符表示书写方向,这两个字符宽度位0,所以不会显示出来,而在小圆点旁边写入了几千个这样的字符,而对应这些字符,系统需要花点时间来进行渲染,所以但你点击的时候,手机会出现卡顿。
乱码问题(二)常见乱码情况分析与解决方法
网友评论