今天, 发生一件非常有趣的事情.
公司同事问了我一个问题: 为什么 2.0 - 1.1 = 0.89999999 呢? 不应该是 0.9 吗?
原来是, 他问了周围一圈的同事, 都给他的是同一个回答, 说这是精度问题. 他百思不得其解, 怎么就会产生精度问题呢. 再问, 就没人知道原因了.
然后, 我就看到了他抱着一本厚厚的书在看. 拿过来一看, 是一本 Java 书, 厚厚的六百多页, 这还仅是第一卷. 哟呵, 这是准备大干一场啊.
看在他这么努力学习的份上, 还有他那对知识极度渴望的眼神. 我决定, 把我毕生所学传授与他.
于是, 就给他详细讲解了, 计算机中是怎么存储一个数的, 十进制是怎么在转二进制的过程中丢失精度的, 以及浮点数是怎么遵循 IEEE 754 规范的, 在浮点数进行加减运算的过程中会经历对阶, 移位运算等过程, 以及在此过程中是怎么丢失精度的.(这些问题在之前的文章中都有解答, 参看 "为什么 0.1+0.2=0.30000000000000004")
然后, 成功的把他彻底搞懵逼了. 怎么这么难啊.
原来, 他的计算机基础比我还匮乏, 不知道什么是位运算, 不知道什么是原码, 反码和补码.
本着我的热心肠, 我就给他普及了一下这些知识 ---- 负数的补码形式和位移运算.
我们知道, 一个数分为有符号和无符号. 对于, 有符号的数来说, 最高位代表符号位, 即最高位 1 代表负数, 0 代表正数.
在计算机中, 存储一个数的时候, 都是以补码的形式存储的. 而正数和负数的补码表示方式是不一样的. 正数的补码就等于它的原码, 而负数的补码是原码除符号位以外都取反, 然后 + 1 得来的. 以一个 int 类型为例 (4 个字节即 32 位)
14 的原码为:
0000 0000 0000 0000 0000 0000 0000 1110
它的反码, 补码和原码都是一样的.
-14 的原码为:
- // 最高位 1 为符号位, 代表此数为负数
- 1000 0000 0000 0000 0000 0000 0000 1110
反码为原码除了符号位以外的其他位都取反 (即 0 变为 1,1 变为 0),
1111 1111 1111 1111 1111 1111 1111 0001
补码为反码 + 1 , 注意二进制中是满二进一.
1111 1111 1111 1111 1111 1111 1111 0010
位的左移, 右移运算就是分别向左和向右移动 N 位. 移位的规则是:
不管有没有符号位, 左移都是在低位补 0
带符号右移, 是在高位补符号位, 即正数补 0, 负数补 1
无符号右移, 无论该数是正数还是负数都在高位补 0
因左移就在右边低位补 0 就可以了, 比较简单, 我就以负数的右移来举例, 是怎么计算无符号右移和带符号右移的. 还是以 -14 为例.
- // -14 的补码
- 1111 1111 1111 1111 1111 1111 1111 0010
- // 带符号右移用 >> 表示, 即右移一位 -14>>1, 高位补符号位 1, 低位舍去
- 1111 1111 1111 1111 1111 1111 1111 1001
- // 无符号右移用 >>> 表示, 即右移一位 -14>>>1, 最高位补 0
- 0111 1111 1111 1111 1111 1111 1111 1001
我们可以通过程序来验证一下 -14>>1 和 -14>>>1 的结果是否正确.
- 1. -14>>1 = -7
- // 我们算出来 -14>>1 的补码为:
- 1111 1111 1111 1111 1111 1111 1111 1001
- // 那它具体代表的数值是多少呢?
- // 首先, 补码 -1 得到反码
- 1111 1111 1111 1111 1111 1111 1111 1000
- // 然后, 反码取反得到原码, 最高位符号位不变
- 1000 0000 0000 0000 0000 0000 0000 0111
这结果不就是 -7 吗, 然后通过程序计算一下结果:
- public class TestMove {
- public static void main(String[] args) {
- System.out.println( -14>>1);
- }
- }
结果同样也是 - 7 . 说明了我们位移操作没问题.
2. -14>>>1=2147483641
我们通过一段程序去验证:
- package com.test.binary;
- /**
- * @Author zwb
- * @DATE 2019/12/3 15:49
- */
- public class TestBinary {
- public static void main(String[] args) {
- // 我们自己计算出来的 -14>>>1 结果
- String bin = "01111111111111111111111111111001";
- double res = binToDec(bin);
- System.out.println(res);
- // 通过计算机计算的结果
- System.out.println(-14>>>1);
- }
- // 二进制转为十进制
- public static double binToDec(String bin){
- int index = bin.indexOf(".");
- int len = bin.length();
- double res = 0;
- //index 为 - 1 说明没有小数
- if(index == -1){
- for(int i = 0; i< len; i++){
- res += Math.pow(2,i) * Integer.parseInt(String.valueOf(bin.charAt(len-1-i)));
- }
- }else{
- // 整数部分
- int partA = 0;
- for(int i = 0; i< index; i++){
- partA += Math.pow(2,i) * Integer.parseInt(String.valueOf(bin.charAt(index-1-i)));
- }
- // 小数部分
- double partB = 0;
- for(int j = index + 1; j < len; j++){
- partB += Math.pow(2,index - j) * Integer.parseInt(String.valueOf(bin.charAt(j)));
- }
- res = partA + partB;
- }
- return res;
- }
- }
运行之后的结果, 可以在控制台打印看到:
上边第一个是我们自己通过推算它的补码, 然后通过二进制转十进制的一个算法算出来的最终结果, 第二个就是直接通过位运算算出来的结果. 可以看到结果是一模一样的.
至此, 是不是对原码, 反码, 补码以及位运算左移右移, 有了比较清晰的认识了呢?
来源: https://www.cnblogs.com/starry-skys/p/11991906.html