第一部分 编程基础与二进制~~第2章 理解数据背后的二进制

536 阅读12分钟

2.1 整数的二进制表示与位运算

2.1.3 十六进制

二进制写起来太长,为了简化写法,可以将4个二进制位简化为一个0 ~ 15的数, 10 ~ 15用字符A ~ F表示,这种表示方法为十六进制,如表2-2所示。

image.png

可以用十六进制直接写常量数字,在数字前面加0x即可。比如十进制123,用十六进制表示是0x7B, 即123 = 7*16 + 11。给整数赋值或者进行运算的时候,都可以直接使用十六进制,比如:

    int a = 0x7B;

Java7 之前不支持直接写二进制常量。比如,想写二进制形式的11001, Java7之前不能直接写,可以在前面补0,补足8位,为00011001,然后用十六进制表示,即0x19。Java7开始支持二进制常量,在前面加0b或0B即可,比如:

    int a = 0b11001;

在Java中,可以方便地使用Integer和Long的方法查看整数的二进制和十六进制表示,例如:

    System.out.println(Integer.toBinaryString(a)); //二进制
    System.out.println(Integer.toHexString(a)); //十六进制
    System.out.println(Long.toBinaryString(a)); //二进制
    System.out.println(Long.toHexString(a)); //十六进制

2.1.4 位运算

理解了二进制表示,我们来看二进制级别的操作:位运算。Java7之前不能单独表示一个位,但可以用byte表示8位,用十六进制写二进制常量。比如,0010表示成十六进制是0x2, 110110表示成十六进制是0x36。

位运算有移位运算和逻辑运算。移位有以下几种。

  1. 左移:操作符为<<, 向左移动,右边的低位补0,高位的就舍弃掉了,将二进制看作整数,左移1位就相当于乘以2。
  2. 无符号右移:操作符为>>>,向右移动,右边的舍弃掉,左边补0.
  3. 有符号右移;操作符为>>, 向右移动,右边的舍弃掉,左边补什么取决于原来最高位是什么,原来是1就补1,原来是0就补0,将二进制看作整数,右移1位相当于除以2。

例如:

    int a = 4; //100
    a = a >> 2; //001, 等于1
    a = a << 3; //1000, 变为8

逻辑运算有以下几种。

  • 按位与 &: 两位都为1才是1。
  • 按位或 |: 只要有一位为1,就为1。
  • 按位取反 ~: 1变0, 0 变1。
  • 按位异或^: 相异为真 1 ,相同为假 0。

大部分都比较简单,如下所示,具体就不赘述了。

    int a = ...;
    a = a & 0x1   //返回0或1,就是a最右边一位的值
    a = a | 0x1   //不管a原来最右边一位是什么,都将设为1

与运算 【与】运算符号为 & ,运算法则为遇0得0。也就是说只要有0,结果即为0。

举例:1001 & 1100

    1 0 0 1       &     1 1 0 0     ————     1 0 0 0

或运算 【或】运算符号为 | ,就是一个竖线,运算法则为遇1得1。也就是说,只要有1,结果就为1。

举例:1100 | 1010

    1 1 0 0       |     1 0 1 0     ————     1 1 1 0

非运算 【非】预算符号为 ~,就是一个波浪线,运算法则为按位取反,也就是遇1取0,遇0取1,即 ~1 = 0 , ~0 = 1;

举例:1001 & 1001

    1 0 1 1        ~     ————     0 1 0 0

异或运算 【异或】运算符号为 ^,就是一个乘方符号,运算法则为相同取0,不同取1。异或运算,关键在异上面,异为1,否则为0。

举例:1001 & 1001

    1 0 1 1      ^     1 0 0 1     ————     0 0 1 0

2.2 小数的二进制表示

2.3 字符的编码与乱码

本节讨论与语言无关的字符和文本的编码以及乱码。我们在处理文件、浏览网页、编写程序时,时不时会碰到乱码的情况。乱码几乎总是令人心烦,让人困惑,通过阅读本节,相信你就可以自信从容地面对乱码,进而恢复乱码了。

编码和乱码听起来比较复杂,但其实并不复杂,请耐心阅读,让我们逐步来探讨。我们先介绍各种编码,然后介绍编码转换,分析乱码出现的原因,最后介绍如何从乱码中恢复。编码有两大类:一类是非Unicode编码;另一类是Unicode编码。我们先介绍非Unicode编码。

2.3.1 常见非Unicode编码

下面我们看一些主要的非Unicode编码,包括ASCII、ISO8859-1、Windows-1252、GB2312、GBK、GB18030和Big5。

1.ASCII

世界上虽然有各种各样的字符,但计算机发明之初没有考虑那么多,基本上只考虑了美国的需求。美国大概只需要128个字符,所以就规定了128个字符的二进制表示方法。这个方法是一个标准,称之为ASCII编码,全称是American Standard Code for Information Interchange,即美国信息互换标准代码。

128个字符用7位刚好可以表示,计算机存储的最小单位是byte, 即8位,ASCII码中最高位设置为0,用剩下的7位表示字符。这7位可以看作数字0 ~ 127, ASCII码规定了从0 ~ 127的每个数字代表什么含义。

我们先来看数字32~126的含义,如图2-1所示,除了中文之外,我们平常用的字符基本都涵盖了,键盘上的字符大部分也都涵盖了。

image.png

数字32 ~ 126表示的字符都是可打印字符,0~31和127表示一些不可以打印的字符,这些字符一般用于控制的目的,这些字符中大部分都是不常用的,表2-4列出了其中相对常用的字符。

image.png

ASCII码对美国是够用了,但对其他国家而言却是不够的,于是,各个国家的各种计算机厂商就发明了各种各样的编码方式以表示自己国家的字符,为了保持与ASCII码的兼容性,一般都是将最高位设置为1。也就是说,当最高位为0时,表示ASCII码,当为1时就是各个国家自己的字符。在这些扩展的编码中,在西欧国家中流行的是ISO 8859-1和Windows-1252, 在中国是GB2312、GBK、GB18030和Big5, 我们逐个介绍这些编码。

2. ISO 8859-1

2.3.2 Unicode编码

以上我们介绍了中文和西欧的字符与编码,但世界上还有很多其他国家的字符,每个国家的各种计算机厂商都对自己常用的字符进行编码,在编码的时候基本忽略了其他国家的字符和编码,甚至忽略了同一国家的其他计算机厂商,这样造成的结果就是,出现了太多的编码,且互相不兼容。

世界上所有的字符能不能统一编码呢?可以,这就是Unicode。

Unicode做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000 ~ 0x10FFFF,包括110多万。但大部分常用字符都在0x0000 ~ 0xFFFF之间,即65536个数字之内。每个字符都有一个Unicode编号,这个编号一般写成十六进制,在前面加U+。大部分总问的编号范围为U+4E00 ~ U+9FFF,例如,“马”的Unicode是U+9A6C。

简单理解,Unicode主要做了这么一件事,就是给所有字符分配了唯一数字编号。它并没有规定这个编号怎么对应到二进制表示,这是与上面介绍的其他编码不同的,其他编码都既规定了能表示哪些字符,又规定了每个字符对应的二进制是什么,而Unicode本身只规定每个字符的数字编号是多少。

那编号怎么对应到二进制表示呢?有多种方案,主要有UTF-32、UTF-16和UTF-8。

1.UTF-32

这个最简单,就是字符编号的整数二进制形式,4个字节。

但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian, BE),否则,就叫“小端”(Little Endian, LE)。对应的编码方式分别是UTF-32BE和UTF-32E。

可以看出,每个字符都用4个字节表示,非常浪费空间,实际采用的也比较少。

2.UTF-16

UTF-16使用变长字节表示:

1)对于编号在U+0000 ~ U+FFFF的字符(常用字符集),直接用两个字节表示。需要说明的是,U+D800 ~ U+DBFF的编号其实是没有意义的。

2)字符值在U+10000 ~ U+10FFFF的字符(也叫增补字符集),需要用4个字节表示。前两个字节叫高代理项, 范围是U+D800 ~ U+DBFF; 后两个字节叫低代理项,范围是U+DC00 ~ U+DFFF。数字编号和这个二进制表示之间有一个转换算法,本书就不介绍了。

区分是两个字节还是4个字节表示一个字符就看前两个字节的编号范围,如果是U+D800 ~ U+DBFF,就是4个字节,否则就是两个字节。

UTF-16也有和UTF-32一样的字节序问题,如果高位存放在前面就叫大端(BE), 编码就叫UTF-16BE, 否则就叫小端,编码就叫UTF-16LE。

UTF-16常用于系统内部编码,UTF-16比UTF-32节省了很多空间,但是任何一个字符都至少需要两个字节表示,对于美国和西欧国家而言,还是很浪费的。

3.UTF-8

UTF-8使用变长字节表示,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为1 ~ 4不等。

具体来说,各个Unicode编号范围对应的二进制格式如表2-6所示。

表2-6 UTF-8编码的编号范围与对应的二进制格式

image.png

表2-6中的x表示可以用的二进制位,而每个字节开头的1或0是固定的。

小于128的,编码与ASCII码一样,最高位为0。其他编号的第一个字节有特殊含义,最高位有几个连续的1就表示用几个字节表示,而其他字节都以10开头。

对于一个Unicode编号,具体怎么编码呢?首先将其看作整数

2.3.2 编码转换

有了Unicode之后,每一个字符就有了多种不兼容的编码格式,比如说“马”这个字符,它的各种编码方式对应的十六进制如表2-7所示。

image.png

这几种格式之间可以借助Unicode编号进行编码转换。可以认为:每种编码都有一个映射表,存储其特有的字符编码和Unicode编号之间的对应关系,这个映射表是一个简化的说法,实际上可能是

2.3.4 乱码的原因

2.3.5 从乱码中恢复

“乱”主要是因为发生了一次错误的编码转换,所谓恢复,是指要恢复两个关键信息:一个是原来的二进制编码方式A;另一个是错误解读的编码方式B

2.4 char的真正含义

通过前面小节,我们应该对字符和文本的编码和乱码有了一个清晰的认识,但前面小节基本是与编程语言无关的,我们还是不知道怎么在程序中处理字符和文本。本节讨论在Java中进行字符处理的基础char, Java中还有Character、String、StringBuilder等类用于文本处理,它们的基础都是char,我们在第7章再介绍这些类。

char看上去是很简单,正如我们在1.2节所说,char用于表示一个字符,这个字符可以是中文字符,也可以是英文字符。赋值时把常量字符用单引号括起来,例如:

    char c = 'A';
    char z = '马';

但为什么字符类型也可以进行算术运算和比较呢?它的本质到底是什么呢?

在Java内部进行字符处理时,采用的都是Unicode,具体编码格式是UTF-16BE。简单回顾一下,UTF-16使用两个或4个字节表示一个字符,Unicode编号范围在65536以内的占两个字节,超出范围的占4个字节,BE就是先输出高位字节,再输出低位字节,这与整数的内存表示是一致的。

char本质上是一个固定占用两个字节的无符号正整数,这个正整数对应于Unicode编号,用于表示那个Unicode编号对应的字符。 由于固定占用两个字节,char只能表示Unicode编号在65536以内的字符,而不能表示超过范围的字符。那超出范围的字符怎么表示呢?使用两个char。类Character、String有一些相关的方法,我们到第7章再介绍。

在这个认识的基础上,我们再来看下char的一些行为。

char有多种赋值方式:

    1. char c = 'A';
    2. char c = '马';
    3. char c = 39532;
    4. char c = 0x9a6c;
    5. char c = '\u9a6c';

第1种赋值方式是最常见的,将一个能用ASCII码表示的字符赋给一个字符变量。第2种赋值方式也很常见,但这里是个中文字符,需要注意的是,直接写字符常量的时候应该注意文件的编码,比如,GBK编码的代码文件按UTF-8打开,字符会变成乱码,