深入理解Java中的原码、反码和补码概念
Java中int类型转byte类型
首先需要了解原码、反码和补码的概念:
对于正数:
- 反码、补码都与原码一样。
对于负数:
- 反码:原码中除去符号位,其他的数值位按位取反,即0变1,1变0
- 补码:反码+1
下面给出几个示例:
40:
原码:00101000
反码:00101000
补码:00101000
-216:
原码:1000000011011000
反码:1111111100100111
补码:1111111100101000
-107:
原码:11101011
反码:10010100
补码:10010101
可以看到,对于正数,其原码、反码、补码相同。对于负数,原码中最高位用来表示符号,反码就是除了最高位外,其余位取反,补码就是反码+1
为什么要设计补码
上面介绍了原码、反码和补码三者的概念,那么,计算机中为什么要设计补码这一概念呢?因为直接用原码涉及到减法操作,这就增加了计算机底层电路涉及的复杂性。而用补码操作时,当减去一个数时,可以看做加上一个负数,然后转变位加上这个负数的补码。即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了.
而使用补码的好处还有,在计算中可以直接带上符号位进行计算,比如计算
40-13:
其中 40 的补码为:0010 1000
-13的补码为:1111 0011
因此实际的运算过程就可以直接带符号位进行相加,同时,如果最高位(符号位)有进位,则舍弃。0010 1000 + 1111 0011 = 1 0001 1011
这里最高符号位的进位可以舍弃,因为8位2进制,能表示的数在-128~127之间。而1 0001 1011
= 283,将283转化到这个范围中即为:
283 % 256 = 27。
其实也相当于 0001 1011
. 所以符号位的最高进位可以舍去。
这种思想其实就类似于时钟理解法,如下图所所示:
图中所示4位二进制表示的数范围在-8~7 之间,因为最高位要用来表示符号位。从图中可以看出,表一共可以表示16个数(0~15),-1就相当于表从0的位置向左移动一格,也相当于表从0的位置向右移动15格。所以-1补码就是15(-1+16=15),-2的补码就是14(-2+16=14),依次类推。
查阅实际的进制转化结果可以进行证明:
-1的补码为:11111111
15的补码为:00001111
-2的补码为:11111110
14的补码为:00001110
-7的补码为:11111001
9的补码为:00001001
可以看到除了符号位外,其余位的结果是相等的。即验证了刚刚的思想。
所以对于负数来说都可以在某个周期内找到一个对应的正数来表示,而这个正数的计算公式为:
n = m+c
其中 c 表示周期
n 表示周期内的一个负数
m 表示n在周期c内对应的正数
因此对于4-2的计算过程可以如下所示:
4 + (-2) = 0100(补) + 1110(补) = 1 0010
舍弃掉符号位的进位结果,即得到0010=2。
另一种计算方法是:
由上述介绍可知,4位二进制表示的周期位16,则-2的补码为16+(-2)=14
所以14+4=18 % 16 = 2
对应的二进制加法过程为:
1110 + 0100 = 1 0010
同样舍弃掉最高位的进位后,得到的结果也为0010=2
因此在计算机中,利用补码的方式进行运算,就可以解决直接用原码来计算得到的结果是错误的问题,也解决了减法的问题。而根据所给的负数去得到它对应的补码的过程,就是找到一个正数来代替其负数的过程。。
比如对于-2 的原码为 1 0010,按照求反码的规则对其按位取反的结果为:
1 1101
不考虑符号位,结果为13。
反码的结果+1 = 1 1110 结果为14
跟我们一开始计算16+(-2)=14的结果相同,就找到了替换-2的正数14。所以其实对原码求反码后再加1的过程,就是在对一个负数加上一个范围(4位二进制能表示的范围就是16个数)。因此,对于负数,求补码其实就是为了求出一个在某一范围内可以代替它的正数,而求补码的过程就是求该正数的过程。
最后就是带符号进行运算,得到结果。
通过上述的例子我们可以看到求补码就是找到一个正数来替代一个负数,完成原先的减法操作。那么如何来找这个正数呢?为什么找到这个正数就可以替换该负数进行减法运算呢?直观上,正数可以用上面的公式来找到,接下来介绍一种更严谨的方式,来获取对应的正数。
原码、反码、补码再深入
上述通过逻辑和实例演示了求补码的目的、思路和过程。接下来,介绍其背后蕴含的数学原理。
同样是上面的钟表图,想象其位一个1位的16进制数,如果当前时间是4点,希望将时间设置成2点,需要怎么做呢?
第一种是往回拨2个小时,4 - 2 = 2
第二种是往前拨14个小时, (4+14)mod 16=2
第三种是往前拨14+16=30个小时,(4+30)mod 16 = 2
上述中的mod表示取模操作。所以通过上面的过程不难看出,钟表的回拨(减法)的结果可以用往前拨(加法)来替代。
因此,还是上面的问题,如何找到一个正数来替代一个负数呢?通过上面的过程以及开头的描述,不难发现一些端倪。但是数学是严谨的,不能靠感觉。
首先需要介绍一个数学中相关的概念:同余
数学上,两个整数除以同一个整数,若得相同余数,则二整数同余
记作 a≡b (mod m)
比如:
26≡2(mod 12)
所以26, 2 关于模12同余
负数取模
x mod y = x-y ×「x/y」
其中「/」表示对结果取下界。上面公式的意思就是:x mod y等于 x- (y×x与y的商的下界)。
比如:
-3 mod 2
= -3 - 2×「-3/2」
= -3 - 2 × 「-1.5」
= -3 - 2 × (-2)
= -3 + 4
= 1
所以
(-2) mod 16 = 16 -2 =14
(-4) mod 16 = 16 -4 = 12
(-7) mod 16 = 16-7 = 9
有了同余和负数的模的概念后,就可以开始证明了:
还是回到上面的时钟例子上,
(-2) mod 16 = 14
14 mod 16 = 14
因此由同余的概念可知,-2和14是同余的。
同时,有同余数的两个定理:
反身性:
反身性:a≡a (mod m)
显而易见,一个数肯定跟它自己是同余的。
线性运算定理:
如果a ≡ b (mod m),c ≡ d (mod m) 那么:
(1)a ± c ≡ b ± d (mod m)
(2)a * c ≡ b * d (mod m)
所以:
4 ≡ 4 (mod 16)
(-2) ≡ 14 (mod 16)
4 -2 ≡ 4 + 14 (mod 16)
因此,通过上面的过程,我们首先找到了一个正数,于待减的负数是同余数,然后又利用公式证明了一个恒等式,即对于(4-2) mod 16 恒等于 (4+14) mod 16。也就是证明了,我们可以用该正数来代替该负数,得到的结果是相同的。
接下来,回到二进制的问题上,看一下4-2=2的问题:
4 -2 = 4 + (-2) = [0000 0100](原) + [1000 0010](原)
= [0000 1011](反) + [1111 1101](反)
先到这一步,-2的反码表示为1111 1101。这里如果认为其为原码,则1111 1101 = -125,这里将符号位除去,即认为是126.
现有:
(-2) mod 127 = 125
125 mod 127 = 125
即
(-2) ≡ 125 (mod 127)
又 4 ≡ 4 (mod 127)
,同样由上面的线性定理可知:
4-2≡ 4 + 125 (mod 127)
可以看出,4-2 与 4+125的余数结果是相同的。而这个余数,正是我们所期望的计算结果:4-2=2
从上面的过程,也就能大致端倪出为什么求补码前要先求反码。
其实求反码,实际上就是在求这个数对于一个模的同余数。而这个模代表的就是最大值。就跟钟表的思想一样,转了一圈(最大值16)后总能找到在可表示范围内的一个正确的数值。
而4+125显然已经超过了最大值127,相当于转过了一轮,而因为符号位是参与计算的,正好和溢出的最高位形成正确的运算结果。
既然上述过程,通过0反码已经可以将减法变成加法,那么为什么还要使用补码呢?为什么在反码的基础上需要加1才能得到正确的结果呢?
4+(-2) = [0000 0100](原) + [1000 0010](原)
=[0000 1011](补) + [1111 1110](补)
如果把[1111 1110]当成原码,去除符号位,得到的结果为
[0111 1110] = 126
其实,在反码的基础上+1,只是相当于增加了模的值:
(-2) mod 128 = 126
126 mod 128 = 126
4-2 ≡ 4+126 (mod 128)
此时,表盘相当于128个刻度转一轮。所以用补码表示的运算结果最小值和最大值应该是[-128,128],但是由于0的特殊性,没有办法表示128,所以补码的取值范围是[-128, 127]。
这里反码+1,也是因为-128的补码为1000 0000,所以整个刻度可以划分为128份,便对128取模。而这里的模同样可以取256,也就是最开头模的划定方式,因为256是128的周期倍。
int类型转byte类型
有了上面的知识,理解下面这段代码就很容易了:
public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i = 0; i < len; i++) {
b[len - i - 1] = (byte)((value >> 8 * i) & 0xff);
}
return b;
}
这里的参数value 表示待转换的int类型变量, len表示字节数组的长度。
因为字节数组每一位都代表了8位二进制数,所以对value右移8位,第二次右移16位,依次类推,来保存字节数组中每一位的数值。
比如,对于-216,设置len=4的结果为:
-216的补码:11111111111111111111111100101000
字节数组的内容:[-1, -1, -1, 40]
参考文章
Java基础—原码、反码,补码详解
java中原码、反码与补码—时钟理解法
上一篇: 电脑基础:原码、反码与补码的解析
下一篇: V8 Promise源码全面解读
推荐阅读
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
深入理解 Java 泛型中的
和 : 详解与应用 -
深入理解Java中replace、replaceAll和replaceFirst这三个字符串替换方法的具体差异
-
深入理解原码、反码和补码的本质与详析
-
原码反码补码的概念,以及原码反码的表示形式-本文主要讲解计算机的原码, 反码和补码.的概念,以及原码反码的表示形式,以及原码反码补码之前如何相互转换,还有计算机中数字是怎么样存储的。
-
计算机中的原码、反码和补码
-
数的机器码表示:原码、反码、补码、变形补码、移码和浮点数编码-数学定义:例:+111的原码为0111,-101的原码为1101 (2) 纯小数的原码表示 纯小数的原码首位同样为符号位,后面的数值则表示小数的尾数,纯小数的整数位为默认为0无需表示。 例:+0.111的原码为0111,-0.101的原码为1101 可以看到,+111和+0.111的原码同为0111,这是因为约定的小数点位置不同,整数的原码的小数点约定在末尾,纯小数的原码的小数点约定在数值的最前面,这样通过约定小数点的位置来表示数的方法就称为定点数表示法,约定小数点位置实际上就是约定编码中每一位的权重。 二、反码 正数的反码与其原码相同。 负数的反码是其对应原码的符号位不变,数值位按位取反。 数学定义:例: 真值 +111 -101 +0.111 -0.101 原码 0111 1101 0111 1101 反码 0111 1010 0111 1010 三、补码 原码虽然转换很简单,但是在做减法时操作很复杂(减不够还要借位),因此计算机在做加负数操作时会先将负数的原码转换为补码再做加法。 先举个栗子,假设时钟现在是9点钟,我把时针往回拨3个小时是6点钟,或者顺时针往后拨9个小时还是6点钟,也就是说9-3的结果等同于9+9(mod 12),对于模数12,-3的补码为+9,这就引申出了一种将减法转换为加法的思想,把减去一个正数视为加上一个负数(例如9+(-3)),再将负数转换为对应的补码,最后就可以和补码做加法了,若结果超出了模数则丢弃一个模数即可。 如图所示:9减去灰色的部分(-3)就等同于加上蓝色的部分,即-3的补码即为蓝色部分的长度9(mod 12)。即补码=模数+真值(超出模数则舍弃一个模数) (1) 整数的补码表示 对于一个n位的二进制真值x,则取模数为2^(n+1),若x为正数则补码和原码相同(加上一个模数又需舍弃一个模数 故相同),若为负数则补码为模数加上x。相对于原码,补码这里的首位就不仅代表原数真值的符号了,也是补码自己的一个数值位。 取模数为2^(n+1)是因为在需要舍弃模数时只需要舍弃运算结果(二进制数)的最高位即可,这在计算机中很容易实现 数学定义:例:三位二进制数的模数2^4就是10000,故+111的补码为0111(即10000 + 111 = 0111 (舍弃模数位)),-101的补码为1011(即10000 - 101 = 1011) 补码运算示例:那么+111 - 101 = +111 + (-101) = 0111 + 1011 = 10010,运算结果只保留后四位(即舍弃模数位),故计算结果为0010。这样就通过加法实现了减法运算。 补码可表示数据范围:由数学定义可知,n位二进制补码可表示的数据范围为 -2n-1~2n-1-1。以8位的byte类型数为例,可表示的数据范围为 -27~27-1,即-128至+127,最小负数-128(补码:1000 0000),最大负数-1(补码:1111 1111),0(补码:0000 0000),最小正数1(补码:0000 0001),最大正数127(补码:0111 1111)。 由补码求真值:正数的补码即为原码即为真值,负数的真值由计算规则可知 负数真值= - (模数 - 补码),以补码1111 1111为例,其真值 = - (1 0000 0000 - 1111 1111) = - 0000 0001 = -1 (2) 纯小数的补码表示 对于一个纯小数x,则取模数为2^1,正数的补码和原码相同,负数的补码为模数2加上x。同样补码的首位不仅代表原数真值的符号,也是补码的数值位。 数学定义:例:纯小数的模数2就是10,故+0.111的补码为0111,-0.101的补码为1011(小数点约定在符号位后) 计算机中求补码的规则 可以注意到求负数的补码时还是要做减法,这在计算机中就很不方便了,但是通过其数学定义可以看到无论是整数还是纯小数,负数的补码都等于反码的末尾加1,而这又等同于原码数值位从右向左遇到第一个1后,这个1左边的数值位都按位取反,故实际计算机中求补码的规则如下:正数的补码等于原码负数的补码等于原码的数值位从右向左的第一个1左边的所有数值位按位取反(例:byte类型值-6的原码为1000 0110,则其补码为1111 1010) 四、变形补码 两个补码在运算时可能会溢出从而产生错误的结果,比如0111+0101 = 1100,两个正数相加反而得到了一个负数,那么在计算机中要如何判断运算结果是否溢出了呢,这就引申出了变形补码。从直观上看,相对于补码来说变形补码就是用两位来表示符号位,00表示正数,11表示负数。运算结果符号位为01表示正溢出,10表示负溢出。
-
原始说明:深入浅出:详解原码、反码与补码的基础概念
-
计算机里的原码、反码和补码:理解负数是怎么被编码的
-
计算机里的原码、反码和补码:理解负数是怎么被编码的