3. 二进制可以表示的内容
内容导视:
进制转换
原码、补码、反码
浮点数表示法
字符编码
抱歉,最近没空写文章了...内容很杂,有内容导视,可以 Ctrl + F 搜索一下关键字,有谬误的地方,尽管批评。
6.29 去武汉面试,软件测试实习生,之前从未了解过,说是可以培训两个月,于是答应了,也不知道 hr 与技术人员沟通好了没,还只是为了完成业绩,面试官究竟要问什么,有什么要求呢,祝我好运。
往返一次 100 就没了,有些心疼。
还行吧,还以为要进厂或工地...
3.1 进制转换
内容导视:
二进制与十进制互转
二进制与八进制互转
常用进制如下:
二进制:0、1,满 2 进 1
十进制:0 ~ 9,满 10 进 1
八进制:0 ~ 7,满 8 进 1
十六进制:0 ~ F,满 16 进 1
十六进制中,超过 9 的部分与十进制的对应关系,A:10,B:11,C:12,D:13,E:14,F:15。
码 3.1-1 加前缀表明进制
89:十进制 0b100:二进制的前面加上 0b 015 :八进制的前面加上 0 0xFF:十六进制的前面加上 0x
3.1.1 二进制与十进制互转
二进制转十进制
将每位上的数乘以 2 的(所处位数 - 1)次方,求和。
例 1:0b100 转为十进制
0b100 =1 * 2 ^ 2 + 0 * 2 ^ 1 + 0 * 2 ^ 0 = 4 所以 0b100 对应的十进制为 4
例 2:0b10101010 转为十进制
由于 0 乘以什么都是 0,故而省去。
0b10101010 =1 * 2 ^ 7 + 1 * 2 ^ 5 + 1 * 2 ^ 3 + 1 * 2 ^ 1 = 128 + 32 + 8 + 2 = 170 所以 0b10101010 对应的十进制为 170
十进制转二进制
十进制数除以 2 得到商,再将商除以 2,如此反复,直到商为 0,然后将每步得到的余数倒过来,就是对应的二进制。
例 1:5 转为二进制
5/2,商 2,余 1 2/2,商 1,余 0 1/2,商 0,余 1 所以 5 对应的二进制为 0b101
例 2:147 转为二进制
147/2,商 73,余 1 73/2,商 36,余 1 36/2,商 18,余 0 18/2,商 9,余 0 9/2,商 4,余 1 4/2,商 2,余 0 2/2,商 1,余 0 1/2,商 0,余 1 所以 147 对应的二进制为 0b10010011
十进制与八进制互转、十进制与十六进制互转,类似于十进制与二进制互转,只不过把 2 换成 8、16 而已。
例 3:0124 转十进制
0124 =1 * 8 ^ 2 + 2 * 8 ^ 1 + 4 * 8 ^ 0 = 64 + 16 + 4 = 84 所以 0124 对应的十进制为 84
例 4:241 转十六进制
241/16,商 15,余 1 15/16,商 0,余 F 所以 241 对应的十六进制为 0xF1
3.1.2 二进制与八进制互转
二进制转八进制
0b000 ~ 0b111 对应 0 ~ 7,每三位二进制数可以表示 0 ~ 7 之间的数,即一个八进制数。
从右边开始,二进制数每三位一组,转成对应的八进制数,不够三位补 0,拼起来即可:
0b000 0 0b001 1 0b010 2 0b011 3 0b100 4 0b101 5 0b110 6 0b111 7 0b00100010 0b00100111 ... 0b11111177
例 1:0b11101 转为八进制
101:5 011:3 所以 0b11101 对应的八进制数为 035
例 2:0b101111011 转为八进制
011:3 111:7 101:5 所以 0b101111011 对应的八进制数为 0573
八进制转二进制
上述的逆过程,将八进制的每位,转成对应的一个 3 位的二进制数,拼起来即可。
例 1:0241 转为二进制数
0241 2:010 4:100 1:001 所以 0241 对应的二进制是 0b10100001
例 2:02371 转为二进制数
02371 2:010 3:011 7:111 1:001 所以 02371 对应的二进制数为 0b1
同理二进制与十六进制转换,由于 0b0000 ~ 0b1111 对应 0 ~ 15,每 4 位二进制数可以表示 1 个十六进制数。
0b00000x00b00010x10b00100x20b00110x30b01000x40b01010x50b01100x60b01110x70b10000x80b10010x90b10100xA0b10110xB0b11000xC0b11010xD0b11100xE0b11110xF0b000100000x10...0b111111110xFF
例 3:0b111101010 转为十六进制
1010:A1110:E0001:10b111101010 对应的十六进制为 0x1EA
例 4:0xFAD8 转为二进制
F:1111 A:1010 D:1101 8:1000 所以 0xFAD8 对应的二进制为 0b11111
3.2 原码、补码、反码
内容导视:
使用二进制表示正负数
猜测为什么计算机要以补码形式存储
3.2.1 使用二进制表示正负数
采用最高位是符号位的方法来区分正负数,正数的符号位为 0、负数的符号位为 1。
原码是带符号位的二进制码。用 8 位(1 个字节)表示一个整数,3 的原码为 00000011,-3 的原码为 10000011。
其实计算机存储数据时,一律储存二进制的补码形式。
对于正整数来说,原码、反码、补码相同。
对于负整数来说,反码的符号位不变,其它位取反(1、0 互换);补码是反码 + 1。
例 1:-1 的补码
原码:10000001 反码:符号位不变,其它位取反,11111110 补码:反码 + 1,11111111 所以 -1 的补码是 11111111
例 2:补码 10000010 对应的十进制整数
通过符号位可以看出是负数的补码,因为正数三码相同,不可能符号位为 1补码:10000010反码:补码 - 1,10000001原码:11111110原码 1111110 对应的十进制为 254再加上符号位 1,原码 11111110 对应 -254所以补码 10000010 对应的十进制整数为 -254
要表示的十进制数
原码
反码
补码
+0
...
到 128 时,原码:10000000,错误,因为最高位是符号位,这会被误以为是负数,所以 128 超出了 1 个字节能够表示的范围。
要表示的十进制数
原码
反码
补码
-0
-1
-2
-3
-4
...
-126
-127
999999英文怎么读,若超出 8 位,就截去前面的多余位数。如 100000000 是 9 位,截去前面一位为 00000000。
1 个字节 8 位,最多表示 28 = 256 个数,以原码的形式存储数据时,由于 0 的表示方法有两种:00000000、10000000,浪费了一种表示方法,实际表示的数的范围:[-127,127] 一共 255 个。
如果以补码的形式存储数据,0 的表示方法统一了,都是 00000000,还有补码 10000000 未被使用,用于表示 -128,实际表示的数的范围:[-128,127]。
3.2.3 猜测为什么计算机要以补码形式存储
除了以补码形式存储可以多存一个数的原因,方便减法运算转为加法运算也是一个关键的因素吧。
二进制加法:0 + 0 = 0,0 + 1 = 1,1 + 1 = 0 进 1 位。
例 1:1 - 1 = ?
1 - 1 = 1 + (-1)1 的补码为:00000001,-1 的补码为 11111111,0 的补码 00000000以补码形式做加法,得到的补码,再转为整数00000001+ 11111111= 0000 是 0 的补码,所以运算结果为 0
例 2:24 - 4 = ?
24 - 4 = 24 + (-4) 24 的补码为:00011000,-4 的补码为 11111100 00011000 + 11111100 = 00010100 00010100 是正数的补码、反码、原码,转为十进制为 20
使用补码,运算变得更方便。
3.3 浮点数表示法
内容导视:
小数对应的二进制
科学计数法
浮点数表示法
最大值、最小值
特殊值
3.3.1 小数对应的二进制
就这么写:
之前漏掉了小数对应的二进制,现补上。
二进制转十进制
从个位数开始向左计算,个位数乘以 2 的 0 次方,十位数乘以 2 的 1 次方,百位数乘以 2 的 2 次方...
从十分位开始向右计算,十分位乘以 2 的 -1 次方,百分位乘以 2 的 -2 次方...
然后将每个式子的结果相加。
例 1:0b101.11 转为十进制
最高位是百位,从 1 * 2 ^ 2 开始 最低位是百分位,到 1 * 2 ^ -2 结束 0b101.11 =1 * 2 ^ 2 + 0 * 2 ^ 1+ 1 * 2 ^ 0 + 1 * 2 ^ -1 + 1 * 2 ^ -2 = 4 + 1 + 0.5 + 0.25 = 5.75
例 2:0b111.01 转为十进制
0b111.01 =1 * 2 ^ 2 + 1 * 2 ^ 1 + 1 * 2 ^ 0 + 0 * 2 ^ -1 + 1 * 2 ^ -2 = 4 + 2 + 1 + 0 + 0.25 = 7.25
可以看到小数部分都是由 0.5、0.25、0.125、0.0625... 等数组合表示,前面的系数 1 或 0,0.625 = 1 * 0.5 + 0 * 0.25 + 1 * 0.125,所以 0.625 对应的二进制为 0.101。
例 3:0100.4 转为十进制,八进制转为十进制,同上过程,将 2 换成 8。
0100.4 = 1 * 8 ^ 2+ 4 * 8 ^ -1= 64 + 0.5 = 64.5
十进制转二进制
整数部分除以 2 得到商,再将商除以 2,如此反复,直到商为 0,然后将每步得到的余数倒过来,就是对应的二进制。
小数部分乘以 2 得到积,取整数部分,余下的部分再次乘以 2,重复步骤,直到余数为 0,取出的整数合并再添上前缀 0. 就是小数对应的二进制。
再将整数和小数部分对应的二进制合并。
使用二进制表示小数可能会有精度损失,有些小数穷尽二进制也无法表示。
例 1:0.75 转为二进制
例 2:3.4 转为二进制
例 3:123.123 转为二进制
123/2,商 61,余 161/2,商 30,余 130/2,商 15,余 015/2,商 7,余 17/2,商 3,余 13/2,商 1,余 11/2,商 0,余 1所以 123 对应的二进制为 0b1111011
0.123 * 2 = 0.246,取 0,余 0.2460.246 * 2 = 0.492,取 0,余 0.4920.492 * 2 = 0.984,取 0,余 0.9840.984 * 2 = 1.968,取 1,余 0.9680.968 * 2 = 1.936,取 1,余 0.9360.936 * 2 = 1.872,取 1,余 0.8720.872 * 2 = 1.744,取 1,余 0.7440.744 * 2 = 1.488,取 1,余 0.4880.488 * 2 = 0.976,取 0,余 0.976...0.123 对应的二进制为 0b0.000111110...
所以 123.123 对应的二进制为 0b1111011.000111110...
3.3.2 科学计数法
十进制表示法(E)
尾数在 [1.0,10) 之间的表示方法称为 modified normalized form,正是 Java 所使用的。
尾数在 [0.0,1.0) 之间的表示方法称为 true normalized form。
鸡二肉 噗哦因特 乃乃乃乃乃乃 眯利恩(Zero point nine nine nine nine nine nine million)或者 乃汗珠二眼的乃踢乃烧珍 乃汗珠二乃踢乃(Nine hundred ninety-nine thousand nine hundred ninety-nine)希望能帮到你,。
例:
999999 = 9.99999 * 10 ^ 5。
0b10010101 = 0b1.0010101 * 2 ^ 7(对于二进制而言,每乘以 2 就相当于小数点往右移动一位)
Java 中,当小数超出 [-9999999,9999999] 范围时,会使用科学计数法表示。
System.out.println(-10000000.0);// -1.0E7
思考控制台输出什么结果?
System.out.println(500e-2);
答:500e-2 = 500 * 10 ^ -2 = 500 / 10 ^ 2 = 5.0
科学计数法默认被当作 double 类型来处理,所以小数不能掉。
System.out.println(-10000000); System.out.println(500E-7);
第 1 个不是小数,原样输出。第 2 个超过上述所说的范围,那使用科学计数法表示,尾数应在 [1.0,10) 之间,输出 5.0E-5。
十六进制表示(P)
在十六进制表示中,2 为底数。
例 1:0xa.0p-1 转为十进制
0xa.0 转为十进制为 10 0xa.0p-1 = 10 * 2 ^ -1 = 10 / 2 = 5.0
例 2:0x19.0p-2 转为十进制
0x19.0 转十进制 0x19.0 =1 * 16 ^ 1 + 9 * 16 ^ 0 = 16 + 9 = 250x19.0p-2 = 25 * 2 ^ -2 = 25 / 4 = 6.25
例 3:0x0.12p2 转为十进制
0x0.12 转十进制 0x0.12 =1 * 16 ^ -1 + 2 * 16 ^ -2 = 1/16 + 2/256 = 0.07031250x0.12p2 = 0.0703125 * 2 ^ 2 = 0.28125
例 4:0b1.0010101 * 2 ^ 7 转为十进制
有两种方案,一是先求出 0b1.0010101 对应的小数,然后再乘以 2 的 7 次方。
二是将小数点向右移动 7 位,得到 0b10010101,然后在求 0b10010101 对应的整数。
0b1.0010101 * 2 ^ 7 = 0b10010101 = 1 + 4 + 16 + 128 = 149
0b1.0010101 = 0b1 + 0b0.0010101 = 1 + 0.125 + 0.03125 + 0.0078125 = 1.1640625 1.1640625 * 2 ^ 7 = 1.1640625 * 128 = 149
0b1.00101010 = 0x1.2a = 1 + 2/16 + 10/256 = 1 + 0.125 + 0.0390625 = 1.1640625 1.1640625 * 2 ^ 7 = 149
3.3.3 浮点数表示法
并不是直接将小数对应的二进制直接存储,这样表示的数的范围太小,也无法保证精度。
如 123456789.123456 的二进制为 11111.10101101。
使用 4 个字节保存此二进制,只能保存前 32 位:0b11111.0 = 123456789.0,小数全部丢失。
浮点数如何表示小数?
分为四部分表示:符号、尾数、基数和指数。
例:0b1010.11101 * 2 ^ 10
符号:+
尾数:1010.11101
基数:2
指数:10。
浮点数有两种类型:单精度浮点数(float:32bit)和双精度浮点数(double:64bit)。
3.3.3-1 浮点数存储格式
类型
总长度
符号
阶码
尾数
float
32 bit
double
64 bit
符号
nine hundred and ninety-nie thousand,nine hundred and ninety-nie
1 表示负,0 表示正。
阶码
也称指数位,因为指数有正、负,为了避免使用符号位,方便比较、排序,采用了 The Biased exponent(有偏指数)。[1]
IEEE754 规定,2 ^(e-1) - 1 的值是 0(e 是阶码部分的位数),小于这个值表示负数,大于这个值表示正数。因此,对于单精度浮点数而言,127 是 0;双精度浮点数中 1023 是 0。
假设指数为 -5,使用单精度浮点数表示时,应存储 -5 + 127 = 122 的二进制 0111 1010。
指数为 1023,使用双精度浮点数表示时,应存储 1023 + 1023 = 2046 的二进制 11111111110。
8 位可以表示的值:00000000 ~ 11111111 即 0 ~ 255,其中 00000000 和 11111111 被用作特殊情况,所以可用范围只有 1 ~ 254,用来表示负数,如果取 128 为 0,可以表示 -127 ~ 126,取 127 为 0,可以表示 -126 ~ 127,表示的数更大。
11 位可用范围: ~ 11111111110 即 1 ~ 2046,取 1023 为 0,可以表示 -1022 ~ 1023。
尾数
一个小数既可以使用 0b1010.11101 * 2 ^ 10 表示,也可以使用 0b1.01011101 * 2 ^ 13 表示。
为了得到统一的编码,通过移位,将小数点前面的值固定为 1。IEEE754 称这种形式的浮点数为规范化浮点数(normal number)。
如 0b0.00101 = 0b1.01 * 2 ^ -3。
因为规定第 1 位永远为 1,因此可以省略不存,这样尾数部分多了 1 位,只需存 01。
因此对于规范化浮点数,尾数其实比实际的多 1 位,也就是说单精度的是 24 位,双精度是 53 位。为了作区分,IEEE754 称这种尾数为 significand。
基数
默认为 2,不参与存储。
[1] 为什么使用移码表示阶码:
工具网址
进制转换:
转载文章
3.3.4 一些例子
加空格只是为了更好的观察。
例 1:用 float 表示 329.301
符号位:0
阶码:8 + 127 = 135,对应的二进制为:10000111
尾数取出 23 位:01001001 01001101 0000111(规格化表示,1 省略不存)
所以单精度浮点数 0.15625 对应的二进制内存表示是:0 10000111 11010000111
3.3.4-1 float 内存表示对应的整数
// 329.301 的二进制内存表示对应的整数 int i = Float.floatToIntBits(329.301f); // 329.301 的二进制内存表示 Object s = Integer.toBinaryString(i); System.out.println(i);// 1134864007 System.out.println(s);// 1000011 10100100 10100110 10000111// -1 的补码为 11111111 11111111 11111111 11111111 System.out.println(Integer.toBinaryString(-1)); // -1 的补码为 ffffffff System.out.println(Integer.toHexString(-1)); // 加下划线不影响实际数值 System.out.println(0b11111111_11111111_11111111_11111111); System.out.println(0xffffffff);
例 2:用 double 表示 -666.875
符号位:1
阶码:9 + 1023 = 1032,对应的二进制为:1
尾数位:,不够 52 位就补 0。
所以双精度浮点数 -666.875 对应的二进制内存表示是:1 1 0000
3.3.4-2 double 内存表示对应的整数
long l = Double.doubleToLongBits(-666.875); Object s = Long.toBinaryString(l); // -4574294926501609472 System.out.println(l); // 1100 System.out.println(s);
nine hundred and ninety-nine thousand, nine hundred and ninety-nine
例 3:单精度浮点数内存表示:11001101,求它对应的小数
符号位:0,正数
阶码取 8 位:01111011,转为十进制 123,123 - 127 = -4,指数为 -4
尾数取 23 位:1,尾数为 1.1
小数的二进制表示为 0b1.1 * 2 ^ -4 = 0b0.101
0b0.101 对应的小数:0.11612...,由于 float 精度最多只能表示 8 位,所以对应的小数是 0.1。
float v = Float.intBitsToFloat(0b11001101);System.out.println(v);// 0.1
3.3.5 浮点数精度
浮点数的精度是指浮点数的有效数字的最大位数,从左边第一个不为 0 的数字开始的个数开始算起。
System.out.println(0.1111111111111f);//0.11111111
可以发现超过了 8 位后的数字都被舍弃,float 最多只能表示 8 位有效数字。
使用单精度浮点数表示,只能存储 23 位尾数,也就是 010010 11101001 01010100 1,丢掉了一部分,导致精度损失。
大部分文章的解释
单精度浮点数尾数有 23 位,加上默认的 1,2 ^ (23+1)= 16777216。因为 10^7 < 16777216 < 10^8,所以说单精度浮点数的有效位数是 7 位,部分可以达到 8 位。
双精度浮点数尾数有 52 位,2 ^ (52+1)= 9992,10^15 < 9992 < 10^16,所以双精度的有效位数是 15 位,部分可以达到 16 位。
说实话,这个解释不大满意,之前的二、十进制转换,求余得到二进制,勉强可以不探究原理;这里就不太好理解,难道是指 24 位全取 1,是最大值,再大就表示不了的意思?只能被迫舍去一些位?
没办法只能自己做实验了,以单精度浮点数为例。
整数部分
如果尾数部分只能存 1 位,即 0,后面的 111 被舍弃,实际存储的值为 0b1.0 * 2 ^ 4 = 0b10000 = 16,那么连一位有效数字都保证不了。
9 = 0b1001 = 0b1.001 * 2 ^ 3,如果尾数只能存 1 位,即 0,后面的 01 被舍弃,实际存储的值为 0b1000 = 8;如果尾数有 3 位,精度才不会损失。
15 = 0b1111 = 0b1.111 * 2 ^ 3,要想精确表示,应存储 111,尾数至少 3 位。
要想表示 1 ~ 9 范围内的所有整数:0001 ~ 1001,需要 4 位二进制,尾数至少需要 3 位。
表示 10 ~ 99:1010 ~ 1100011,至少需要 6 位尾数,如 1100011 = 1.100011 * 2 ^ 6,存储 100011 可以保证精度不损失。
...
表示 106 ~ 107:11110000 ~ 111010000000,至少需要 23 位尾数。
24 位二进制能表示的最大值:16777215,超过了此数,如 16777217,0b1 = 0b1. * 2 ^ 24,存储 23 位 0,第 24 位的 1 被略去,精度损失。
意思大概是小数由 0.5、0.25、0.125、0.0625、0.003125、0.015625、...等单位组合而成。
如 0.625 = 1 * 0.5 + 0 * 0.25 + 1 * 0.125,则 0.625 = 0b0.101
如果要表示的数比最小单位还小,如用 0.5、0.25、0.125、0.0675、0.03125、0.015625 组合表示不了 0.001,精度只能到十分位 0.1,有时可以到百分位。
例,六位二进制表示 0.175,最贴近的是 0b0.001011 = 0.171875。
要想保证精确到小数点后面 3 位,2 ^ x < 10-3,求得 x 的最大值为 -10,最小单位 0.,用十位二进制表示 0b0. = 0.17578125。
23 位尾数,能够表示的最小单位 2 ^ -24 = 0.4775 < 10-7,可以保证精确到小数点后 7 位,部分小数能够精确到 8 位。
小数部分的解释,我不大满意,因为有负指数的存在。还另外也没能解释整数与小数共存时的情况,这已经超出了我的能力范围。
0.1 + 0.2 = ?
使用单精度浮点数表示;
0.1 = 0b0.0001 10011001 10011001 1001100 1101...
类似四舍五入,第 23 位是否进位,看后面的位数是否大于 10000000 00000000 00000000 0000010 00000000...剩下都是 0,设此二进制为 b。
这是小数部分,可以无限往后填充 0,按位比就行。
例,比较 11 与 b,第 1 位相等,都是 1;第 2 位的 1 大于 0,所以 11 更大。
如 0b1.11101010 10101010 1010111 1011,1011 > b,所以第 23 位进位 + 1,应存储 11101010 10101010 1011000,多余的 1011 被舍弃。
如 0b1.10100101 10101111 1111010 01,01 < b,应存储 10100101 10101111 1111010。
尾数存储 23 位,实际值为 0b0.0001 10011001 10011001 1001101 = 0.11612
0.2 = 0b0.001 10011001 10011001 1001100 1101...
实际值 0b0.001 10011001 10011001 1001101 = 0.23224
所以 0.1 + 0.2 = 0.11612 + 0.23224 = 0.34836
得到的结果大于 0.3,不过单精度浮点数由于精度不够,显示不出来。
System.out.println(0.1f + 0.2f);// 0.3System.out.println(0.1 + 0.2);// 0.30004
3.3.6 最大值、最小值
单精度浮点数最大值
8 位可以表示的值:00000000 ~ 11111111 即 0 ~ 255,其中 00000000 和 11111111 被用作特殊情况,所以可用范围只有 1 ~ 254,用来表示负数,取 127 为 0,可以表示 -126 ~ 127。
单精度最大指数为 127,最小指数 -126。
双精度最大指数为 1023,最小指数 -1022。
由于阶码 0b11111111 被用作特殊情况,最大指数应为 0b11111110 - 127 = 254 - 127 = 127。
最大值在内存中的表示:0 11111110 11111111111111111111111
...
单精度浮点数最大值为 0b1.11111111 11111111 1111111 * 2 ^ 127 = 2 ^ 127 + 2 ^ 126 + ... + 2 ^ 104 = 2 ^ 127 * [1 + 1/2 + 1/4 + ... + 1/2^23] = 2^127 * [1 + 1 - 1/2^23] = 2^128 - 2^104 = 3.4028235E38
0b1.11111111 11111111 1111111 * 2 ^ 127 = 0x1.fffffe * 2 ^ 127。
单精度浮点数最小正值
使用规范化浮点数时,尾数默认省略了前导数 1,导致 0 无法表示。
即使尾数全部取 0,0b1.0000... * 2 ^ n = 1 * 2 ^ n,也无法精确表示 0。
所以规定了另一种浮点数:
当指数位全是 0 时,尾数部分的前导数为 0,同时指数部分的偏移值比规范形式的偏移值小 1。如单精度浮点数取 126 为 0。
最小指数 0b00000000 - 126 = -126。
最小正值在内存中的表示:0 00000000
单精度浮点数最小正值为 0b0.0000 0000 0000 0000 0000 001 * 2 ^ -126 = 2 ^ -23 * 2 ^ -126 = 2 ^ -149 = 1.4e-45
双精度浮点数最大值
内存表示:0 11111111110 1111111111111111111111111111111111111111111111111111
指数的最大值为 0b11111111110 - 1023 = 2046 - 1023 = 1023
这里使用二进制表示太长,转为十六进制表示
最大值 0x1.fffffffffffff * 2 ^ 1023 = 2 ^ 1023 * [1 + 1 - 1/2^52] = 2 ^ 1024 - 2 ^ 971 = 1.7976931348623157e308
双精度浮点数最小正值
内存表示:00001
指数的最小值为 0b - 1022 = -1022
最小正值 0x0.1 * 2 ^ -1022 = 2 ^ -52 * 2 ^ -1022 = 2 ^ -1074 = 4.9e-324
3.3.7 特殊值
为了表示 -1.0 / 0.0、1.0 / 0.0、0.0 / 0.0 等特殊数值,定义了三种类型 -Infinity(负无穷大)、Infinity(正无穷大)、NaN(非数字)。
无穷大
当指数位全为 1,尾数位全为 0,表示为无穷大,当一个数超出了浮点数的表示范围,就可以使用 Infinity 表示。符号为 0,是正无穷大,符号为 1,负无穷大。
单精度浮点数 Infinity 内存表示 0 11111111 0
双精度浮点数 -Infinity 内存表示 1 11111111111 0000
0b1111 1111 1111 0000 = 0xfff00...
/ 0111 1111 1111 0000000...,是正无穷大double d1 = Double.longBitsToDouble(0x7FF0L);// 1111 1111 1000 00000...,是负无穷大float f1 = Float.intBitsToFloat(0xFF800000);
非数字
符号位任意,指数位全为 1,尾数位不全为 0(至少有 1 位是 1),表示不是数,比如用 NaN 表示 -1 的平方根。
特性:
不等于自身,可以利用此特性判断一个数是否是 NaN
NaN 参与运算,最终结果还是 NaN
3.4 字符编码
内容导视:
字符集与字符编码
ASCII 字符集
GB2312 字符集
Unicode 字符集
计算机是以二进制的形式来存储数据的,我们在屏幕上看到的文字,在存储之前都被转换成了二进制(编码),在显示时也要根据二进制找到对应的字符。(解码)
字符集定义了文字和二进制的对应关系,为字符分配了唯一的编码。
如 ASCII 字符集定义的 &39; 的编码是 01100001,如果把它当作整数的补码,则为 97。
3.4.1 字符集与字符编码
字符集规定了某个文字对应的二进制数字存放方式(编码)和某串二进制数值代表了哪个文字(解码)的转换关系。
字符集只是一个规则集合的名字,对应到真实生活中,字符集就是对某种语言的称呼。例如:英语,汉语。
对于一个字符集来说,要正确编码转码一个字符需要三个关键元素:字库表(character repertoire)、编码字符集(coded character set)、字符编码(character encoding form)。
字库表中装着所有字符
编码字符集:即用一个序号(code point)来表示一个字符在字库中的位置,不同进制都可以,如 &39; 在 ASCII 字库表中的位置为 0x61。
字符编码:编码字符集和实际存储二进制之间的转换关系。一般来说直接将 code point 的值转为二进制直接存储。例如在 ASCII 字库中 &39; 的序号为 65,转为二进制 01000001,直接存储。
3.4.2 ASCII 字符集
American Standard Code for Information Interchange:美国信息交换标准代码
ASCII 字符集是由美国人发明的,没有考虑欧洲那些扩展的拉丁字母,也没有考虑韩语和日语、汉字。
ASCII 的标准版本于 1967 年第一次发布,最后一次更新则是在 1986 年,迄今为止共收录了 128 个字符,包含了基本的拉丁字母(英文字母)、阿拉伯数字(1234567890)、标点符号(,.! 等)、特殊符号(@#$%^& 等)以及一些具有控制功能的字符(往往不会显示出来)。1 个字节表示这些符号绰绰有余。
在 ASCII 编码中,大写字母、小写字母和阿拉伯数字都是连续分布的(见下表),这给程序设计带来了很大的方便。例如要判断一个字符是否是大写字母,就可以判断该字符的 ASCII 编码值是否在 65~90 的范围内。
3.4.2-1 ASCII 字库表
二进制
十进制
十六进制
字符/缩写
解释
NUL (NULL)
空字符
SOH (Start Of Headling)
标题开始
STX (Start Of Text)
正文开始
ETX (End Of Text)
正文结束
EOT (End Of Transmission)
传输结束
ENQ (Enquiry)
请求
ACK (Acknowledge)
回应/响应/收到通知
BEL (Bell)
响铃
BS (Backspace)
退格
HT (Horizontal Tab)
水平制表符
0A
LF/NL(Line Feed/New Line)
换行键
0B
VT (Vertical Tab)
垂直制表符
0C
FF/NP (Form Feed/New Page)
换页键
0D
CR (Carriage Return)
回车键
0E
SO (Shift Out)
不用切换
0F
SI (Shift In)
启用切换
DLE (Data Link Escape)
数据链路转义
DC1/XON (Device Control 1/Transmission On)
设备控制1/传输开始
DC2 (Device Control 2)
设备控制2
DC3/XOFF (Device Control 3/Transmission Off)
设备控制3/传输中断
DC4 (Device Control 4)
设备控制4
NAK (Negative Acknowledge)
无响应/非正常响应/拒绝接收
SYN (Synchronous Idle)
同步空闲
ETB (End of Transmission Block)
传输块结束/块传输终止
CAN (Cancel)
取消
EM (End of Medium)
已到介质末端/介质存储已满/介质中断
1A
SUB (Substitute)
替补/替换
1B
ESC (Escape)
逃离/取消
1C
FS (File Separator)
文件分割符
1D
GS (Group Separator)
组分隔符/分组符
1E
RS (Record Separator)
记录分离符
1F
US (Unit Separator)
单元分隔符
(Space)
空格
!
"
#
$
%
&
'
(
)
2A
*
2B
+
2C
2D
-
2E
.
2F
3A
:
3B
;
3C
<
3D
=
3E
>
3F
?
@
A
B
C
D
E
F
G
H
I
4A
J
4B
K
4C
L
4D
M
4E
N
4F
O
P
Q
R
S
T
U
V
W
X
Y
5A
Z
5B
[
5C
\
5D
]
5E
^
5F
_
`
a
b
c
d
e
f
g
h
i
6A
j
6B
k
6C
l
6D
m
6E
n
6F
o
p
q
r
s
t
u
v
w
x
y
7A
z
7B
{
7C
|
7D
}
7E
~
7F
DEL (Delete)
删除
如 &39; 存储时以 97 对应的二进制 01100001 存储。
后来欧洲人发明了 ISO8859-1。ISO8859-1 包含了 ASCII 中的所有字符(兼容),还加入了一些西欧字符。
兼容:新字符集包括原有字符集中的所有字符,且这些字符的编码在原有字符集中也是如此,如 ‘a’ 在 ASCII 的编码是 01100001,在 GBK 的编码也是 01100001,就称新字符集兼容原有字符集。
3.4.3 GB2312 字符集
ASCII 字符集中没有包含中文,国人规定了 GB2312 字符集,使用 2 个字节表示一个汉字(因为汉字太多,1 个字节最多表示 256 个字符),英文还是 1 个字节,兼容 ASCII 码。
那么存储时好存,找到字符对应的字符编码(序号转为二进制)存储即可,但对于非定长编码,读取时怎么知道是按 1 个字节读还是 2 个字节读。
如 1,凡是汉字,对应字符编码都以 1 开头,所以读取 2 个字节,1010 对应汉字 &39;,以 0 开头,读取 1 个字节,01100001 对应 &39;。
GB2312 编码范围:0xA1A1 ~ 0xFEFE,其中汉字编码范围:0xB0A1 ~ 0xF7FE。
分为 94 个区,每个区有 94 个数,GB2312 编码表:
通过序号查字符
0xA1A2,前两位是区号,后两位是区中的第几个数。
A1A2 代表 01 区的第 2 个数。(可以看出规律:01 + A0 = A1,02 + A0 = A2)
3.4.3-1 01 区中的字符
通过字符查序号
以 &39; 为例,它在 16 区的第 92 个数,那么对应的十六进制的序号是多少?把它们转成 16 进制分别与 A0 相加就可以了。
16 转成十六进制是 0x10 92 转成十六进制是 0x5C 10 + A0= B0 5C + A0 = FC
即 &39; 对应的序号是 0xB0FC,转成二进制是 1100。
3.4.3-2 16 区中的字符
可以查看 GB2312 中文简体字库表 直观一点:
3.4.3-3 字符编码集
&39; 对应的是 A1A0+2,即 A1A2,比较一目了然。
GBK 和 GB18030是后来的扩展编码,兼容 GK2312。
3.4.4 乱码的产生
不同国家有不同的编码方式,同一串二进制经不同的规则解码得到的结果很可能不一样,或者干脆显示一大堆的 ?。
如下面的例子:
使用 UTF-8 编码输入 “天下”,将文件转成 GBK 编码,会显示什么?
工具:字符集编码转换。
使用 UTF-8 编码(此编码中一个汉字占 3 个字节)储存 “天下” 得到的二进制是 11100101 10100100 10101001 11100100 10111000 10001011
使用 GBK 解码,1 开头的读 2 个字节,11100101 10100100 转成十六进制是 0xE5A4,即 69 区的第 4 个数:“澶”,再读 2 个字节转 0xA9E4是 “╀”,(因为 GBK 兼容 GB2312,所有前两字可以查 GB2312 表)
最后两个字节转成的十六进制 0xB88B 不属于 GB2312 的范围,查下 GBK编码表,,得 “笅”。
E5 - A0 = 0x45 = 69 A4 - A0 = 0x4 = 4
UTF-8 编码转为 GBK,“天下” -> “澶╀笅”。
所以要显示正确的结果,编码、解码需要使用同一种字符编码。
事实真的是这样吗?
创建一个 a.txt 文件,使用 UTF-8 编码,输入 “天下”。
右键此文件/属性:
两个汉字一共占用 6 个字节,说明 UTF-8 一个汉字占 3 个字节。
右键使用 Notepad++ 打开:
使用 GBK 编码读取这段二进制:
GBK 编码中,3 个汉字 6 个字节,一个汉字占 2 个字节。
3.4.5 BOM
BOM:byte order mark(标记字节顺序),因为网络传输中分为两种,大端(Big Endian)和小端(Little Endian)。[1]
在文件开头添加 BOM 标记(0xFEFF:大端、0xFFFE:小端)表明所使用的字节顺序,为 UTF-16、UTF-32 准备的。
UTF-8 不需要 BOM 表明字节顺序,但可以用 BOM 来表示编码方式,Windows 就是采用 BOM 来标记文本文件的编码方式的。
微软在 UTF-8 中使用 BOM 是因为这样可以把 UTF-8 和 ASCII 等编码区分开来,但这样的文件在 Windows 之外的操作系统里会带来问题。
图 3.4.5-1 IDEA 中应选择不带 BOM 的 UTF-8
不含 BOM 的 UTF-8 才是标准形式,即文件开头没有 0xEFBBBF = 0b1110 1111 10 111011 10 111111。
0b1111 111011 111111 = 0xFEFF。[2]
[1] 详见 2.3.5 解析的第 6 条注释。存储器以字节为单位存储,字节序是对于超过 1 个字节的数据而言的,如存储 0x00009A6C,先存储低字节 0x6C(小端: 0x6C9A0000),还是先存储高字节 0x00(大端:0x00009A6C)。
字节序是指编码单元内部字节与字节之间的顺序,而不是位之间的顺序,如 11011100 10110001 以大端存储:11011100 10110001 = 0xDCB1,小端存储:10110001 11011100 = 0xB1DC。
UTF-16、UTF-32 分别以 2、4 个字节为编码单元存储,是需要区分大小端的。如以 UTF-16LE 编码存储字符 “” ,对应编码是 0xD800DF00,编码单元之间的顺序确定,即 0xD800 与 0xDF00 这两个编码单元顺序确定,但编码单元内部,字节与字节之间的顺序不确定,即 0xD800 内究竟是 00 在前还是 D8 在前,没有规定;以小端存储应是低位在前,存储 0x00D8,所以 “” 的 UTF-16LE 编码为 0x00D800DF。
UTF-8 以 1 个字节为编码单元存储,不存在编码单元内的字节谁先谁后的问题,就 1 个也没法排序。
[2] 详见 3.4.9 UTF-8 的编码规则。
3.4.6 Unicode 字符集
Unicode 是为了解决传统的字符编码方案的局限而产生的,它为所有语言中的每个字符设定了统一并且唯一的序号(code point),以供全球人使用,序号也被称为代码点。
UCS-2 和 UCS-4
文字和序号之间的对应关系就是 UCS-2(Universal Character Set coded in 2 octets),UCS-2 使用 2 个字节表示序号,取值范围为 U+0000 ~ U+FFFF。
为了能表示更多的文字,人们又提出了 UCS-4,即用 4 个字节表示序号。它的范围为 U+00000000 ~ U+7FFFFFFF,其中 U+00000000 ~ U+0000FFFF 与 UCS-2 一样。
Unicode 本身只规定了每个字符与序号的对应关系,并没有规定这个序号在计算机中如何存储,你当然可以直接将序号转成二进制存储,但是对于 UCS-4 而言,序号直接转成二进制,每个字符需要 4 个字节,使用 ASCII 编码的地区国家,存储体积是原来的 4 倍,这是不太容易接受的,为此诞生了 UTF-8 可变长编码,英文字符还是只占一个字节。
规定存储方式的称为 UTF(Unicode Transformation Format),其中应用较多的就是 UTF-16 和 UTF-8 了。
直接存储序号转成的二进制,每个字符占用 4 个字节(包括英文字符)。比如马在 Unicode 中的序号为:U+9A6C,对应的二进制:00000000 00000000 10011010 01101100。
UTF-32 包括 UTF-32、UTF-32BE(Big Endian:大端),UTF-32LE(Little Endian:小端)。
表 3.4.7-1 使用十六进制编辑器打开文件
使用不同方式存储
存储 “马”
999999 nine hundred and ninety-nine thousand nine hundred and ninety-nine.1000000 one million.10000000 ten million.
UTF-32(Big Endian)
00 00 FE FF 00 00 9A 6C
UTF-32(Little Endian)
FF FE 00 00 6C 9A 00 00
UTF-32(不带 BOM)
00 00 9A 6C
没有提供 BOM,默认以大端解码。
工具网址
记得去掉空格,Unicode 编码转换:
汉字 Unicode 编码范围:
UTF-16 使用变长字节表示:
序号在 U+0000 ~ U+D7FF 的字符,使用 2 个字节表示,直接将序号转成二进制。
序号在 U+10000 ~ U+10FFFF 的字符,使用 4 个字节表示,编码规则如下。
0xD800 ~ 0xDFFF:0b11011 00000 000000 ~ 0b11011 11111 111111 是空,没有对应字符,如果编码以 11011 开头,那么必是读取 4 个字节,紧跟着的 5 位,如 00011,再加 1,得到序号的二进制的前缀 100。
表 3.4.8-1 UTF-16 编码与序号的对应关系
序号
序号对应二进制
编码
0x0000 ~ 0xD7FF、0xE000 ~ 0xFFFF
xxxxxxxx xxxxxxxx
xxxxxxxx xxxxxxxx
0x10000 ~ 0x1FFFF
1xxxxxx xxxxxxxxxx
11011 00000xxxxxx 110111xxxxxxxxxx
0x20000 ~ 0x2FFFF
10xxxxxx xxxxxxxxxx
11011 00001xxxxxx 110111xxxxxxxxxx
0x30000 ~ 0x3FFFF
11xxxxxx xxxxxxxxxx
11011 00010xxxxxx 110111xxxxxxxxxx
0x40000 ~ 0x4FFFF
100xxxxxx xxxxxxxxxx
11011 00011xxxxxx 110111xxxxxxxxxx
0x50000 ~ 0x5FFFF
101xxxxxx xxxxxxxxxx
11011 00100xxxxxx 110111xxxxxxxxxx
0x60000 ~ 0x6FFFF
110xxxxxx xxxxxxxxxx
11011 00101xxxxxx 110111xxxxxxxxxx
0x70000 ~ 0x7FFFF
111xxxxxx xxxxxxxxxx
11011 00110xxxxxx 110111xxxxxxxxxx
0x80000 ~ 0x8FFFF
1000xxxxxx xxxxxxxxxx
11011 00111xxxxxx 110111xxxxxxxxxx
0x90000 ~ 0x9FFFF
1001xxxxxx xxxxxxxxxx
11011 01000xxxxxx 110111xxxxxxxxxx
0xA0000 ~ 0xAFFFF
1010xxxxxx xxxxxxxxxx
11011 01001xxxxxx 110111xxxxxxxxxx
0xB0000 ~ 0xBFFFF
1011xxxxxx xxxxxxxxxx
11011 01010xxxxxx 110111xxxxxxxxxx
0xC0000 ~ 0xCFFFF
1100xxxxxx xxxxxxxxxx
11011 01011xxxxxx 110111xxxxxxxxxx
0xD0000 ~ 0xDFFFF
1101xxxxxx xxxxxxxxxx
11011 01100xxxxxx 110111xxxxxxxxxx
0xE0000 ~ 0xEFFFF
1110xxxxxx xxxxxxxxxx
11011 01101xxxxxx 110111xxxxxxxxxx
0xF0000 ~ 0xFFFFF
1111xxxxxx xxxxxxxxxx
11011 01110xxxxxx 110111xxxxxxxxxx
0x100000 ~ 0x10FFFF
10000xxxxxx xxxxxxxxxx
11011 01111xxxxxx 110111xxxxxxxxxx
表 3.4.8-2 0x0000 ~ 0xFFFF 之内范围的字符存储
使用不同方式存储
存储 “马”
UTF-16(Big Endian)
FE FF 9A 6C
UTF-16(Little Endian)
FF FE 6C 9A
UTF-16(不带 BOM)
9A 6C
的序号 U+10300 的二进制:1000000 1100000000,对应编码在表中第 2 项,将 000000 1100000000 填入 11011 00000xxxxxx 110111xxxxxxxxxx 得到编码:110111100 = 0xD800DF00。
表 3.4.8-3 0x10000 ~ 0x1FFFF 之内范围的字符存储
使用不同方式存储
存储 “”
UTF-16(Big Endian)
FE FF D8 00 DF 00
UTF-16(Little Endian)
FF FE 00 D8 00 DF
UTF-16(不带 BOM)
D8 00 DF 00
例 1:UTF-16 编码 1010 对应的字符。
0b1010,读取 2 个字节,不需要做特殊处理,转成十六进制 0x554A,直接在 Unicode 字库表 中找到 ‘啊’。
例 2:UTF-16 编码 11110011 对应的字符。
编码的第 6 ~ 10 位需要加 1 得到序号的二进制前缀,如 0b01111 + 1 = 0b10000。
11011 开头,应读取 4 个字节,110111111。
紧接着读取 5 位:00000,得到序号的二进制前缀:0b00000 + 0b1 = 0b1。
读取 6 位,得到 111100;
剩余位数:1111,去掉 110111,得到 1000110011;
将这三部分拼接得到:0b1 111100 1000110011 = 0x1F233,然后根据此序号在字库中查询到 “”。
例 3:“” 在 Unicode 的序号是 0x1F234,求它的 UTF-16LE 编码对应的十六进制。
序号的二进制前缀需要 - 1,得到编码的第 6 ~ 10 位,如 0b1100 - 1 = 0b01011。(不足 5 位前面补 0)
0x1F234 = 0b1 1111,这种 17 位的没法用 2 个字节存储,取出二进制的后 16 位:111100 1000110100,前缀为 1。
编码的开头为 11011;
紧接着的 5 位等于前缀 - 1 = 00000;
接着 6 位是取出的二进制的前 6 位:111100;
接着 6 位是固定的:110111;
最后 10 位是取出的二进制的最后 10 位:1000110100;
将其拼接得到:0b11011 00000 111100 110111 1000110100 = 0xD83C DE34。
使用小端表示,每两个字节交换顺序,再加上 BOM 表明字节顺序:0xFFFE 3CD8 34DE。
工具网址
UTF-16 工具转换:
图 3.4.8-1 添加前缀 FFFE 表明字节顺序
UTF-8 使用变长字节表示:
序号在 U+00 ~ U+7F 的字符,编码使用 1 个字节表示,直接将序号转成二进制。
序号在 U+80 ~ U+7FF 的字符,使用 2 个字节表示。
序号在 U+800 ~ U+FFFF 的字符,使用 3 个字节表示。
序号在 U+10000 ~ U+10FFFF 的字符,使用 4 个字节表示。
表 3.4.9-1 UTF-8 编码与序号的对应关系
序号
编码
0x00 ~ 0x7F(0 ~ 127)
0xxxxxxx
0x80 ~ 0x7FF(128 ~ 2047)
110xxxxx 10xxxxxx
0x800 ~ 0xFFFF(2048 ~ 65535)
1110xxxx 10xxxxxx 10xxxxxx
0x10000 ~ 0x10FFFF(65536 ~ 1114111)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
BOM:0xFEFF = 0b1111 111011 111111,填充到 1110xxxx 10xxxxxx 10xxxxxx 中,得到编码:11101111 10111011 10111111 = 0xEFBBBF。
例 1:“倩” 在 Unicode 的序号为 0x5029,求它的 UTF-8 编码对应的十六进制。
0x5029 处于 0x800 ~ 0xFFFF 范围内,对应编码 1110xxxx 10xxxxxx 10xxxxxx。
0x5029 = 0b1001,补足 16 位,不够前面补 0,截成三段,长度分别为 4、6、6 位:0101 000000 101001,然后填充到对应编码,11100101 10000000 10101001 = 0xE580A9。
带 BOM 的 UTF-8:0xEF BB BF E5 80 A9。
例 2:UTF-8 编码 111110100000,求它对应的字符。
观察前 4 位:1110,应读取 3 个字节,11100110 10010111 10100000,去掉第 1 个字节的 1110、第 2 个字节的 10、第三个字节的 10,得到 0110 010111 100000 = 0x65E0,在 Unicode 字库的 0 号平面查找到 0x65E0 对应的字符 “无”。
由于 UTF-8 的处理单元为一个字节(也就是一次处理一个字节),所以处理器在处理的时候就不需要考虑这一个字节的存储是在高位还是在低位,直接拿到这个字节进行处理就行了,因为大小端是针对大于一个字节的数的存储问题而言的。