读书笔记:Java 编程的逻辑(二)

编程基础与二进制

第2章 理解数据背后的二进制

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

2.1.1 正整数的二进制表示
  • 正整数的二进制中,每个位置只能是 0 或 1。位权从右到左,第一位为 1,然后依次乘以 2,即第二位为 2,第三位为 4,以此类推。
2.1.2 负整数的二进制表示
  • 二进制使用最高位表示符号位,用 1 表示负数,用 0 表示正数。
    • 整数有 4 种类型 byte、short、int、long,分别占 1、2、4、8 个字节,即分别占 8、16、32、64 位,每种类型的符号位都是其最左边的一位
    • 下面假定类型是 byte,即从右到左的第 8 位表示符号位。
      • 负数表示不是简单地将最高位变为 1,而是采用补码表示法,即在原码表示的基础上取反然后加 1,取反就是将 0 变为 1,1 变为 0。
      • 负数的二进制表示就是对应的正数的补码表示。
  • 给定一个负数的二进制表示,要想知道它的十进制值,可以采用相同的补码运算
    • 比如:10010010,首先取反,变为 01101101,然后加 1,结果为 01101110,它的十进制值为 110,所以原值就是 -110。
    • 直觉上,应该是先减 1,然后再取反,但计算机只能做加法,而补码的一个良好特性就是,对负数的补码表示做补码运算就可以得到其对应正数的原码,正如十进制运算中负负得正一样。
    • 对于 byte 类型,正数最大表示是 01111111,即 127,负数最小表示(绝对值最大)是 10000000,即 -128,表示范围就是 -128~127。其他类型的整数也类似,负数能多表示一个数。
    • 负整数为什么要采用这种奇怪的表示形式呢?原因是,只有这种形式,计算机才能实现正确的加减法。
  • 理解了二进制加减法,我们就能理解为什么正数的运算结果可能出现负数了。当计算结果超出表示范围的时候,最高位往往是1,然后就会被看作负数。
2.1.3 十六进制
  • 二进制写起来太长,为了简化写法,可以将 4 个二进制位简化为一个 0~15 的数,10~15 用字符 A~F 表示,这种表示方法称为十六进制。
  • 可以用十六进制直接写常量数字,在数字前面加 0x 即可。
  • 在 Java 中,可以方便地使用 Integer 和 Long 的方法查看整数的二进制和十六进制表示:
    int a = 24;
    // 二进制,结果为 11000
    System.out.println(Integer.toBinaryString(a)); 
    // 十六进制,结果为 18
    System.out.println(Integer.toHexString(a));
    // 二进制,结果为 11000
    System.out.println(Long.toBinaryString(a));
    // 十六进制,结果为 18
    System.out.println(Long.toHexString(a));
    
2.1.4 位运算
  • 理解了二进制表示,我们来看二进制级别的操作:位运算。
  • 位运算有移位运算和逻辑运算。
    • 移位有以下几种。
      • 1)左移:操作符为 <<,向左移动,右边的低位补 0,高位的就舍弃掉了,将二进制看作整数,左移 1 位就相当于乘以 2
      • 2)无符号右移:操作符为 >>>,向右移动,右边的舍弃掉,左边补 0
      • 3)有符号右移:操作符为 >>,向右移动,右边的舍弃掉,左边补什么取决于原来最高位是什么,原来是 1 就补 1,原来是 0 就补 0,将二进制看作整数,右移 1 位相当于除以 2
    • 逻辑运算有以下几种。
      • 按位与 &:两位都为 1 才为 1。
      • 按位或 |:只要有一位为 1,就为 1。
      • 按位取反 ~:1 变为 0, 0 变为 1。
      • 按位异或 ^:相异为真,相同为假

2.2 小数的二进制表示

  • 实际上,即使在一些非常基本的小数运算中,计算的结果也是不精确的。
2.2.1 小数计算为什么会出错
  • 实际上,不是运算本身会出错,而是计算机根本就不能精确地表示很多数。计算机是用一种二进制格式存储小数的,只能精确表示那些可以表述为 2 的多少次方和的数
  • 为什么计算机中不能用我们熟悉的十进制呢?在最底层,计算机使用的电子元器件只能表示两个状态,通常是低压和高压,对应 0 和 1,使用二进制容易基于这些电子元器件构建硬件设备和进行运算。如果非要使用十进制,则这些硬件就会复杂很多,并且效率低下。
2.2.2 二进制表示
  • float 和 double 被称为浮点数据类型,小数运算被称为浮点运算。
  • 为什么要叫浮点数呢?这是由于小数的二进制表示中,表示那个小数点的时候,点不是固定的,而是浮动的。
  • 二进制中为表示小数,也采用科学表示法,形如 m× (2^e)。
    • m 称为尾数,e 称为指数。
    • 指数可以为正,也可以为负,负的指数表示那些接近 0 的比较小的数。
    • 在二进制中,单独表示尾数部分和指数部分,另外还有一个符号位表示正负。
  • 几乎所有的硬件和编程语言表示小数的二进制格式都是一样的。
    • 这种格式是一个标准,叫做 IEEE 754 标准,它定义了两种格式:一种是 32 位的,对应于 Java 的 float;另一种是 64 位的,对应于 Java 的 double。
    • 32 位格式中,1 位表示符号,23 位表示尾数,8 位表示指数。
    • 64 位格式中,1 位表示符号,52 位表示尾数,11 位表示指数。
    • 在两种格式中,除了表示正常的数,标准还规定了一些特殊的二进制形式表示一些特殊的值,比如负无穷、正无穷、0、NaN(非数值,比如0乘以无穷大)。

2.3 字符的编码与乱码

  • 编码有两大类:一类是非 Unicode 编码;另一类是 Unicode 编码。
2.3.1 常见非 Unicode 编码
  • ASCII
    • 美国大概只需要 128 个字符,所以就规定了 128 个字符的二进制表示方法。这个方法是一个标准,称为 ASCII 编码,全称是 American Standard Codefor InformationInterchange,即美国信息互换标准代码。
    • 128 个字符用 7 位刚好可以表示,计算机存储的最小单位是 byte,即 8 位,ASCII 码中最高位设置为 0,用剩下的 7 位表示字符。
    • 这 7 位可以看作数字 0~127, ASCII 码规定了从 0~127 的每个数字代表什么含义。
  • ASCII 码是基础,使用一个字节表示,最高位设为 0,其他 7 位表示 128 个字符。
    • 其他编码都是兼容 ASCII 的,最高位使用 1 来进行区分。
    • 西欧主要使用 Windows-1252,使用一个字节,增加了额外 128 个字符。
    • 我国内地的三个主要编码 GB2312、GBK、GB18030 有时间先后关系,表示的字符数越来越多,且后面的兼容前面的,GB2312 和 GBK 都是用两个字节表示,而 GB18030 则使用两个或四个字节表示。
    • 我国香港特别行政区和我国台湾地区的主要编码是 Big5。
    • 如果文本里的字符都是 ASCII 码字符,那么采用以上所说的任一编码方式都是一样的。但如果有高位为 1 的字符,除了 GB2312、GBK、GB18030 外,其他编码都是不兼容的。
2.3.2 Unicode 编码
  • Unicode 做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从 0x000000~0x10FFFF,包括 110 多万。但大部分常用字符都在 0x0000~0xFFFF 之间,即 65536 个数字之内。每个字符都有一个 Unicode 编号,这个编号一般写成十六进制,在前面加 U+。
  • 简单理解,Unicode 主要做了这么一件事,就是给所有字符分配了唯一数字编号。它并没有规定这个编号怎么对应到二进制表示,那编号怎么对应到二进制表示呢?有多种方案,主要有 UTF-32、UTF-16 和 UTF-8。
    • UTF-32
      • 字符编号的整数二进制形式,4 个字节。
      • 但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian,BE),否则,就叫“小端”(Little Endian, LE)。
      • 每个字符都用 4 个字节表示,非常浪费空间,实际采用的也比较少。
    • UTF-16
      • UTF-16 使用变长字节表示。
      • 对于编号在 U+0000~U+FFFF 的字符(常用字符集),直接用两个字节表示。
      • 字符值在 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 节省了很多空间,但是任何一个字符都至少需要两个字节表示,对于美国和西欧国家而言,还是很浪费的。
    • UTF-8
      • UTF-8 使用变长字节表示,每个字符使用的字节个数与其 Unicode 编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为 1~4 不等。
      • 小于 128 的,编码与 ASCII 码一样,最高位为 0。其他编号的第一个字节有特殊含义,最高位有几个连续的 1 就表示用几个字节表示,而其他字节都以 10 开头。
      • 对于一个 Unicode 编号,具体怎么编码呢?首先将其看作整数,转化为二进制形式(去掉高位的 0),然后将二进制位从右向左依次填入对应的二进制格式x中,填完后,如果对应的二进制格式还有没填的 x,则设为 0。
      • 和 UTF-32/UTF-16 不同,UTF-8 是兼容 ASCII 的,对大部分中文而言,一个中文字符需要用三个字节表示。
2.3.3 编码转换
  • 有了 Unicode 之后,每一个字符就有了多种不兼容的编码方式。这几种格式之间可以借助 Unicode 编号进行编码转换。可以认为:每种编码都有一个映射表,存储其特有的字符编码和 Unicode 编号之间的对应关系,这个映射表是一个简化的说法,实际上可能是一个映射或转换方法。
  • 编码转换的具体过程可以是:一个字符从 A 编码转到 B 编码,先找到字符的 A 编码格式,通过 A 的映射表找到其 Unicode 编号,然后通过 Unicode 编号再查B的映射表,找到字符的B编码格式。
2.3.4 乱码的原因
  • 乱码有两种常见原因:一种比较简单,就是简单的解析错误;另外一种比较复杂,在错误解析的基础上进行了编码转换。
  • 解析错误
    • 切换查看编码的方式并没有改变数据的二进制本身,而只是改变了解析数据的方式,从而改变了数据看起来的样子,这与前面提到的编码转换正好相反。
    • 很多时候,做这样一个编码查看方式的切换就可以解决乱码的问题,但有的时候这样是不够的。
  • 错误的解析和编码转换
    • 如果怎么改变查看方式都不对,那很有可能就不仅仅是解析二进制的方式不对,而是文本在错误解析的基础上还进行了编码转换。
    • 这种情况其实很常见,计算机程序为了便于统一处理,经常会将所有编码转换为一种方式,比如 UTF-8,在转换的时候,需要知道原来的编码是什么,但可能会搞错,而一旦搞错并进行了转换,就会出现这种乱码。这种情况下,无论怎么切换查看编码方式都是不行的。
2.3.5 从乱码中恢复
  • “乱”主要是因为发生了一次错误的编码转换,所谓恢复,是指要恢复两个关键信息:一个是原来的二进制编码方式 A;另一个是错误解读的编码方式 B。
  • 恢复的基本思路是尝试进行逆向操作,假定按一种编码转换方式 B 获取乱码的二进制格式,然后再假定一种编码解读方式 A 解读这个二进制,查看其看上去的形式,这要尝试多种编码,如果能找到看着正常的字符形式,应该就可以恢复。
  • Java 中处理字符串的类有 String,String 中有我们需要的两个重要方法。
    • 1)public byte[] getBytes(String charsetName),这个方法可以获取一个字符串的给定编码格式的二进制形式。
    • 2)public String(byte bytes[], String charsetName),这个构造方法以给定的二进制数组 bytes 按照编码格式 charsetName 解读为一个字符串。

2.4 char 的真正含义

  • char 本质上是一个固定占用两个字节的无符号正整数,这个正整数对应于 Unicode 编号,用于表示那个 Unicode 编号对应的字符。由于固定占用两个字节,char 只能表示 Unicode 编号在 65536 以内的字符,而不能表示超出范围的字符。那超出范围的字符怎么表示呢?使用两个 char。
  • 由于 char 本质上是一个整数,所以可以进行整数能做的一些运算,在进行运算时会被看作 int,但由于 char 占两个字节,运算结果不能直接赋值给 char 类型,需要进行强制类型转换,这和 byte、short 参与整数运算是类似的。char 类型的比较就是其 Unicode 编号的比较。
  • char 的加减运算就是按其 Unicode 编号进行运算,一般对字符做加减运算没什么意义,但 ASCII 码字符是有意义的。比如大小写转换,大写 A~Z 的编号是 65~90,小写 a~z 的编号是 97~122,正好相差 32,所以大写转小写只需加 32,而小写转大写只需减 32。加减运算的另一个应用是加密和解密,将字符进行某种可逆的数学运算可以做加解密。
  • char 的位运算可以看作是对应整数的位运算,只是它是无符号数,也就是说,有符号右移 >>和无符号右移 >>> 的结果是一样的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值