目录
#有只想看原反补码的底层原理的可以直接点击目录移步观看(可能有搜过来只想看这个的大学生,可以直接跳到“什么是补码”,也可以从原码开始看起)
一、进制基础
进制就是进位制,是人们规定的一种进位方法,二进制逢2进1,八进制是逢8进1,十进制逢10进1,十六进制逢16进1。
不同进制形式:
二进制:0b或0B开头,由0和1组成
八进制:0开头,由0、1...6、7组成
十进制:常见整数,由0、1...8、9组成
十六进制:0x或0X开头,由0、1...8、9、a、b、c、d、e、f组成,大小写不区分
public class Test{
public static void main(String[] args) {
byte b1 = 0b01100010; // 二进制
byte b2 = 98; // 十进制
byte b3 = 0142; // 八进制
byte b4 = 0x62; // 十六进制
// 打印出来结果全是98,为什么?
System.out.println(b1);
System.out.println(b2);
System.out.println(b3);
System.out.println(b4);
}
}
上述案例中,在计算机底层存储时,都是按二进制存储的,其值按照十进制表示,都是98。
注意:不论什么类型的数据值,在计算机的底层存储时,统一按照二进制形式存储!
二、原码、反码、补码
学习这个知识点之前,我们先来看一个题目:写出10的二进制形式
0b 0 0(23个) 0000 1010
10对应的类型为int,在计算机底层占4字节,需要32个比特位表示 其中最高位为符号位,0表示正数,1表示负数,剩下的31位,其中23位都为0,低8位为0000 1010 = 8 + 2 = 10 连到一起,结果为正整数10
思考:-10的二进制形式如何表示?
如果要表示负整数的二进制形式,则必须学习原码、反码、补码。
相信很多小伙伴都知道原码反码补码的计算方式,不知道也没关系,这就来了`(*∩_∩*)′
原码:十进制数据的二进制表现形式,最左边是符号位,0为正,1为负
10的原码: 0000 1010
-10的原码: 1000 1010
反码:正数的反码是其本身,负数的反码是符号位保持不变,其余位取反
10的反码:跟10的原码相同 0000 1010
-10的反码: 拿到-10的原码,保留符号位其他位取反 1111 0101
补码:正数的补码是其本身,负数的补码是在其反码的基础上+1
10的补码: 跟10的原码相同 0000 1010
-10的补码:拿到-10的反码, 1111 0101
在反码基础上加1 1111 0110
那具体为啥要有反码、补码,反码为什么是原码取反;为啥要有补码,补码为什么是+1不是加3 (๑•ี_เ•ี๑)?
我先告诉你,数据在计算机底层进行存储或运算,以补码方式进行,而接下来的内容会告诉你为什么。
1.什么是原码
在计算机当中,一个0或者一个1,我们称之为bit,中文名叫做比特位,8个bit叫做一个字节,也就是计算机当中最小的储存单元。
可是还有负数怎么办,那没办法,计算机只能识别0和1,所以我们将一个bit位用来表示正负,0为正,1为负。
那么这样,在原码当中,一个字节最大值即为 0111 1111 ,也就是 +127,最小值也就是 1111 1111,就是-127(并不是 1000 0000,这样是 -0,也就还是0,是比负数大的)
就这样,利用原码对正数计算并不会出问题,0000 0001 (1)加上 0010 0001 (33)等于 0010 0010 (34)
可是负数的话,就有点问题了,1000 0000是 -0,也就是0,0+1=1,可是在原码这里变成了 1000 0001,变成了 -1;再继续加1呢,-1加1是0,在原码这里,1000 0001 加1变成了 1000 0010,实际上变成了-2
原码的弊端就体现出来了,利用原码对负数计算,结果会出错,实际结果与预期结果相反。
其实是因为符号位的限制,只要符号位是负的,加1就变成了减1,所以结果才会一直是相反的
2.什么是反码
为了解决原码不能计算负数的问题,反码出现了
正数不变,负数符号位保持不变,其余位取反,1变0,0变1
举个栗子(๑•̀ㅁ•́ฅ)
-10的反码是 1111 0101,加1,然后变成了 1111 0110,是 -118 ? 诶,可不要忘了变回去哦,1000 1001,这下变成 -9了,完美,这个问题解决了🤔
吗?既然有补码就说明还是有一丢丢小问题的
当你用 -1的反码 1111 1110 加1时候,会变成 1111 1111,是 -0的反码,-0也是0,对的
再加1呢,变成了0000 0000(超范围了,不过我们现在是一个字节,所以不管了),这样变成了0的反码(符号位为0,正数,反码就是原码),怎么还是0,嗯🤯?
0这个祸害,导致只要是跨0的反码运算,都被偷走了一位(诡计多端的0🤗)
十进制 数字 | 原码 | 反码 |
---|---|---|
2 | 0000 0010 | 0000 0010 |
1 | 0000 0001 | 0000 0001 |
0 | 0000 0000 | 0000 0000 |
-0 | 0000 0000 | 1111 1111 |
-1 | 1000 0001 | 1111 1110 |
-2 | 1000 0010 | 1111 1101 |
这个表格一目了然,只要是涉及到跨0,就相当于跨了两个台阶,那么聪明的你应该也因此想到了补码的用途。
3.什么是补码
如果你是直接跳到这的,那答案就是,它是 在为了解决符号位导致的负数计算错误而创造出来的反码基础上,做了改进解决了跨0少1的问题而出现的一个最终存储方案,即补码。
拿表格看就十分清晰了
十进制 数字 | 原码 | 反码 | 补码 |
---|---|---|---|
2 | 0000 0010 | 0000 0010 | 0000 0010 |
1 | 0000 0001 | 0000 0001 | 0000 0001 |
0 | 0000 0000 | 0000 0000 | 0000 0000 |
-0 | 0000 0000 | 1111 1111 | 0000 0000 |
-1 | 1000 0001 | 1111 1110 | 1111 1111 |
-2 | 1000 0010 | 1111 1101 | 1111 1110 |
它相当于把 0 的两种形式给屏蔽掉了(千万要注意这里的 +1 只有负数才加哦,都加就相当于啥都没干了(;¬_¬)
总结一下:
正数的原、反、补码都是一样的,是它二进制本身,符号位为0;
负数第一位符号位为1,反码保留符号位其他位取反,补码是反码+1
补码小细节这一块:-127 的补码是 1000 0001,这样就空出了一位,也就是1000 0000,这就是-128(这个也很好理解,因为 0 原来在反码中有两种表现形式),在一个字节中,它无原码、无反码,当然这并不碍事,计算机中本来也是以补码存储,所以我们说,一个字节它的取值范围是 -128 ~ 127,这才是真正的 音乐 原因
让我们再更深一点点
基本数据类型
byte类型的10 1个字节 0000 1010
short类型的10 2个字节 0000 0000 0000 1010
int类型的10 4个字节 0000 0000 0000 0000 0000 0000 0000 1010
long类型的10 8个字节 太长了就不写了(๑•̀ω•́๑)
模拟计算机底层进行运算:-10 + 10
计算机底层通过补码进行运算
先获取-10补码:1 1(23) 1111 0110 + 再获取 10补码:0 0(23) 0000 1010 = 结果: 1【0 0(23) 0000 0000】
结果分析:两个int类型数据相加后结果值类型仍旧是int,其中int类型表示范围为4字节32个比特位,所以上述结果中第33位的那个1被自动抛弃,只保留低32位数值, 0 0(23) 0000 0000,即 0。 所以:-10 + 10 == 0
三、类型转换
基本数据类型表示范围大小排序:
在变量赋值及算术运算的过程中,经常会用到数据类型转换,其分为两类:
- 隐式类型转换
- 显式类型转换
1.隐式类型转换
情形1:赋值过程中,小数据类型值或变量可以直接赋值给大类型变量,类型会自动进行转换
public class Test {
public static void main(String[] args) {
// int类型值 赋值给 long类型变量
long num = 10;
System.out.println(num);
// float类型值 赋值给 double类型变量
double price = 8.0F;
System.out.println(price);
char c = 'a';
// char 赋值给 int
int t = c;
System.out.println(t);
// 下面会编译报错
//float pi = 3.14;
//int size = 123L;
//int length = 178.5;
}
}
常量补充
整形数后面加'L'或'l',就表示long类型字面值常量
小数后面加'F'或'f',就表示float类型字面值常量
整形字面值,不论是二进制、八进制还是十进制、十六进制,默认都是int类型常量。
我个人建议大写L和F,因为这样明显,l 和 1 在编译器中还是比较难辨识的罒ω罒
情形2:byte、short、char类型的数据在进行算术运算时,会先自动提升为 int,然后再进行运算
public class Test {
public static void main(String[] args) {
byte b = 10;
short s = 5;
// (byte -> int) + (short -> int)
// int + int
// 结果为 int
int sum = b + s;
// 下一行编译报错,int 无法自动转换为 short进行赋值
// short sum2 = b + s;
System.out.println(sum);
}
}
情形3:其他类型相互运算时,表示范围小的会自动提升为范围大的,然后再运算
public class Test {
public static void main(String[] args) {
byte b = 10;
short s = 5;
double d = 2.3;
// (byte10->int10 - 5) * (short->int5) -> 5 * 5 = 25
// int25 + double2.3
// double25.0 + double2.3
// 结果:double 27.3,必须用double变量来接收该值
double t = (b - 5) * s + d;
// double赋值给float,编译报错
// float f = (b - 5) * s + d;
System.out.println(t);
}
}
2.显式类型转换(强制转换)
赋值过程中,大类型数据赋值给小类型变量,编译会报错,此时必须通过强制类型转换实现。
固定格式:
数据类型 变量名 = (目标数据类型)(数值或变量或表达式);
public class Test082_ExplicitTrans {
public static void main(String[] args) {
// 1.数据赋值 强制类型转换
float f1 = (float)3.14;
System.out.println(f1); //3.14
// 1.数据赋值 强制类型转换
int size = (int)123L;
System.out.println(size); //123
double len = 178.5;
// 2.变量赋值 强制类型转换
int length = (int)len;
System.out.println(length); //178
byte b = 10;
short s = 5;
double d = 2.3;
// 3.表达式赋值 强制类型转换
float f2 = (float)((b - 5) * s + d);
System.out.println(f2); //27.3
}
}
特殊情况
观察下面代码,思考为什么编译能够成功?
public class Test083_SpecialTrans {
public static void main(String[] args) {
// 为什么 int -> byte 成功了?
byte b = 10;
System.out.println(b);
// 为什么 int -> short 成功了?
short s = 20;
System.out.println(s);
// 为什么 int -> char 成功了?
char c = 97;
System.out.println(c);
// 为什么 b2赋值成功,b3却失败了?
byte b2 = 127;
System.out.println(b2);
//byte b3 = 128;
//System.out.println(b3);
// 为什么 s2赋值成功,s3却失败了?
short s2 = -32768;
System.out.println(s2);
//short s3 = -32769;
//System.out.println(s3);
// 为什么 c2赋值成功,c3却失败了?
char c2 = 65535;
System.out.println("c2: " + c2);
//char c3 = 65536;
//System.out.println(c3);
}
}
整形常量优化机制
使用整形字面值常量(默认int类型)赋值给其他类型(byte、char、short)变量时,系统会先判断该字面值是否超出赋值类型的取值范围,如果没有超过范围则赋值成功,如果超出则赋值失败。
public class Test {
public static void main(String[] args) {
// 为什么 int -> byte 成功了?
// 字面值常量10,在赋值给b的时候,常量优化机制会起作用:
// 系统会先判断10是否超出byte的取值范围,如果没有,则赋值成功,如果超出(比如128),则赋值失败
byte b = 10;
System.out.println(b);
// 为什么 int -> short 成功了?
// 系统会先判断20是否超出short的取值范围,结果是没有,则赋值成功
short s = 20;
System.out.println(s);
// 如果使用-32769赋值,超出short类型取值范围,则赋值失败
// short s3 = -32769;
// System.out.println(s3);
}
}
注意事项:
常量优化机制只适用于常量,如果是变量,则不可以该优化机制只针对int类型常量,对于long、float、double等常量,则不可以。
public class Test {
public static void main(String[] args) {
// 下面3个赋值语句都 编译失败
byte b = 10L;
short s = 3.14F;
int n = 2.3;
// 下面2个赋值语句 ok
byte b1 = 2;
short s2 = 3;
// 变量赋值或表达式赋值,都编译失败
byte b3 = s2;
byte b4 = b1 + s2;
}
}
四、补码与类型转换
补码作为数据类型存储的方法,我们知道其原理就可以解释类型转换了
隐式转换如 byte 类型的 10(0000 1010)转成 int 类型就是前面补零(0000 0000 0000 0000 0000 0000 0000 1010)
强制转换如 int 类型的 300(0000 0000 0000 0000 0000 0001 0010 1100)转成 byte 类型就是去掉前面的3 * 8 =24 个bit位:0000 0000 0000 0000 0000 0001 0010 1100,变成了44(0010 1100)这就是强制类型数据精度丢失或数据截断的原因(๑•̀ㅁ•́๑)✧
如 int 类型的200(0000 0000 0000 0000 0000 0000 1100 1000)变成 byte 类型,这下可不是直接变成200哦,也不是去掉符号位的 -72,要记住计算机是补码存储,所以这个时候应该转成原码(先变成反码 1100 0111,然后变成 1011 1000,所以是 -56)
其他的运算符
运算符 | 含义 | 运算规则 |
---|---|---|
& | 逻辑与 | 0为false,1为true |
| | 逻辑或 | 0为false,1为true |
<< | 左移 | 向左移动,低位补0 |
>> | 右移 | 向右移动,高位补0或1 |
>>> | 无符号右移 | 向右移动,高位补0 |
我们一个一个来
&:
public class Test {
public static void main(String[] args) {
int a = 200;
int b = 10;
System.out.println(a & b);
}
}
0000 0000 0000 0000 0000 0000 1100 1000(a)
&0000 0000 0000 0000 0000 0000 0000 1010(b)
——————————————————————
0000 0000 0000 0000 0000 0000 0000 1000(&都是true才是true,也就是都是1才是1)
一个一个对比比特位
转换为十进制为8
|:
public class Test {
public static void main(String[] args) {
int a = 200;
int b = 10;
System.out.println(a | b);
}
}
0000 0000 0000 0000 0000 0000 1100 1000(a)
&0000 0000 0000 0000 0000 0000 0000 1010(b)
——————————————————————
0000 0000 0000 0000 0000 0000 1100 1010(|有true就是true,有1就是1)
转换为十进制为202
<<:就是补码向左移动,低位补0
public class Test {
public static void main(String[] args) {
int a = 200;
System.out.println(a << 2);//左移两位
}
}
0000 0000 0000 0000 0000 0000 1100 1000
0000 0000 0000 0000 0000 0011 0010 0000
变成了800(左移一次就是*2,因为是二进制)
>>:就是补码向右移动,原来是正数,高位补0,负数补1
public class Test {
public static void main(String[] args) {
int a = 200;
System.out.println(a >> 2);//右移两位
}
}
0000 0000 0000 0000 0000 0000 1100 1000
0000 0000 0000 0000 0000 0000 0011 0010
变成了50(右移一次就是/2)
>>>:就是补码向右移动,不管符号直接补0
总结
文章系统讲解了计算机中的进制转换、原码/反码/补码原理及类型转换。通过补码解决负数运算问题,详细分析原码、反码、补码的转换规则及其必要性,并由此解释了Java中的隐式与显式类型转换机制、补码在类型转换中的应用原理,特别说明,byte/short/char的常量优化机制也很关键哦。
我通过代码示例演示了不同进制赋值、位运算等操作,并解释了计算机底层以补码形式存储数据的本质特性,大家看完也可以自己尝试一下🥳
之后的文章我要依次介绍运算符,流程控制与方法,大家敬请期待吧(๑`^´๑)
感谢点赞,感谢收藏,感谢关注,希望能帮到你,如果有错误可以在评论区指出。最后求关注,我会持续更新的(๑>︶<)و