当前位置: 首页 > 科技观察

Java还在纠结原码、补码、反码?其实SoEasy

时间:2023-03-12 08:34:02 科技观察

平时在看jdk源码的时候会有很多感触,那就是基础真的很重要,那么什么是基础呢?除了java的基本语法,最基本的就是原码、反码和补码以及基本的计算!1、原码、反码、补码大家都应该知道。数据以二进制形式存在于计算机中。例如,字节a=6;byteb=-6分为两种情况,一种是正数,一种是负数;对于正数6,原码为00000110,反码和补码也一样;而对于-6,原来的代码是10000110,这是为什么呢?因为***位(最左边)是一个符号,0表示正数,1表示负数;不管是byte、short、int还是其他数,***位都是用来表示一个符号的,所以-6的原码是10000110,负数的倒数同号,其他倒数是11111001;负数的补码是在反码上加1,因为是二进制,是每2进1,所以补码是:11111010,;注意:计算机中的加减运算(计算机中只有加法,减法可以用加法表示,是补码的形式,见下面的栗子)是指补码之间的运算!而负数在计算机中以补码的形式存在并参与运算。如果要改成十进制,首先需要改成原码,然后才能改成十进制或其他基数;那么如何表示0呢?我们可以简单的看一下:0=6-6=6+(-6)=[00000110]complement+[11111010]complement=[00000000]complement=[00000000]original,那么有人要问了,补码10000000代表多少?看个简单的-1-127=(-1)+(-127)=[10000001]原+[11111111]原=[11111111]补+[10000001]补=[10000000]补=[00000000]原码,比较两者,如果都用原码,同一个原码[00000000]可以表示0和-128两个数,但是如果用补码,可以用一个补码对应到一个数字。显然,一个补码对应一个数字更符合我们的需求!!!补充一点,对于负数,原码和反码可以相互转化,试图理解记忆:原码------>反码:符号位不变,而其他位被反转;例如[10000001]original=[11111110]inverseoriginalcode------>补码:符号位不变,其他位取反,再+1;如[10000001]original=[11111111]complementcomplement------>原码:符号位不变,其他位取反;例如[11111110]反码=[10000001]原反码------>补码:+1;例如[11111110]inverse=[11111111]complement补码------->原码:符号位不变,其他位取反,然后+1;例如[11111111]complement=[10000001]originalcomplement-------->反码:先变成原码,再变成反码;比如[11111111]complement=[11111110]inverse这些都是基本的东西,只要记住在电脑里的运算,就是补码的形式,这里会涉及到一个过程,画一个简单的图,计算机中的计算都是用补码进行的;并且中间的转换过程可以通过电脑进行非常快速的转换,所以我们不用担心这个;让我们看看该操作中包含哪些操作。.2.基本计算说到现实中的计算,无非就是加减乘除四种算术运算。相应地,计算机中也有加法、减法、乘法和除法。加法和减法上面已经说了,加法和减法可以通过补加法来实现。但是乘法和除法呢?这里说说最简单的乘除法,比如乘以2除以2,如果是其他数的乘除法就比较复杂了。暂时没兴趣研究。有兴趣的朋友可以去查查计算机中乘除法的实现,过瘾!以字节类型为例,字节类型的第一位是符号位,所以取值范围在11111111到011111111之间,变成十进制。-127到127,加上0000的原码0000对应两个数0和-128,所以整个范围就是-128到127;乘法和除法也分为两块,正数和负数;2.1正数乘以2(左移用<<表示)先看一个简单的数(这里我也写正数的补码):bytea=5;5的原码为:[00000101]原码=[00000101]补码;乘以2等于10,原码为[00001010]原码=[00001010]补码看看这两个补码是什么关系?就是在5的补码最右边加一个0,左边去掉一个0!***是把5的补码看成一个整体,将整一位向左移动一位,去掉左边多余的位,右边空的位置补0。这时候就会出现一个问题,如果补码是01000000,也就是64,向左移动一位,你觉得是多少?答案是-128,应该是正数128,为什么是负数呢?记住,这个移位操作会覆盖符号位,左移一位的补码是10000000。注意这一定不能变成原码。***节里说10000000的补码是-128(这两个补码一定要注意,很特别,00000000代表十进制的0,10000000代表十进制的-128!不要和原码比,因为它们的原码都是00000000,无法区分)publicvoidnum(){bytea=64;字节b=(字节)(a<<1);System.out.println(b);//-128}2.2正数除以2(右移用>>表示)由于左移一位是乘以2,所以移动一位到对必须除以2!但是记住一个规则,如果向右移动,右边多余的部分会被去掉,左边空出的位置加上与符号位相同的数字!(记住这个负数右移是一样的规则)比如65的补码是01000001,右移一位,补码应该是00100000,记住,此时最左边的0是根据符号位只有为0时才加0,是正数,右移后的原码和补码一样,那么十进制应该是32,这里可以看出结果大于0的奇数右移就是先除以2再加到Rounddown,如果是偶数就直接除以2publicvoidnum(){bytea=64;byteb=(byte)(a<<1);System.out.println(b);//-128}2.3负数乘以2其实计算(<<)正数比较容易,负数有点麻烦!比如-127的补码是10000001,左移一位的补码是00000010。由于这个补码是正数,所以原码也是这个,转成十进制是2,是不是觉得特别有意思,哈哈哈!你知道为什么吗?因为byte的范围是-128到127,只要超过这个范围,就会变成一个意想不到的数字!不超范围的例子,-6的补码是11111010,左移一位的补码是11110100。由于是负数,原码变成10001100,对应十进制的-12。这个结果和想象中的一样!2.4负数除以2(>>)记住2.2的那句话,如果向右移动,超出最右边的部分直接去掉,左边空出的位置用和符号位!很抽象,举个栗子:-6的补码是11111010,右移一位的补码是11111101,是负数,可以知道对应的十进制数成为原始代码。..2.5.无符号右移(>>>)本来应该完成正负数的左移和右移,但是还有一个特殊的操作方法,就是无符号右移(注意只有无符号有符号右移,没有无符号左移shift!),简单来说,不管正负数,只要往右移,超出最右边的部分直接舍弃,左边空出的位置补0!没关系!好像没什么好说的,举个栗子吧!-6的补码是11111010,无符号右移的补码是01111101,是正数,原码和补码是一样的,所以对应的小数点应该是125,但是有问题实际情况,代码如下:@org.junit.Testpublicvoidnum(){bytea=-6;byteb=(byte)(a>>>1);System.out.println(b);//-3}为什么打印出来的结果是-3?下面是一个小细节操作。在进行右移操作时,会先将byte类型的数字变为int类型,int类型的变原码再变为补码。移位操作后,将后8位转换为字节型,然后改变原码,最后转换为十进制。...麻烦吗!Stilltakethe-6aboveasachestnut,-6needstobeunsignedrightshifted,sotheoriginalcodeof-6shouldbe32bits10000000000000000000000000000110//Originalcode11111111111111111111111111010//Complementcode01111111111111111111111111111111111111111111111111111111111111111111111111111111Shiftonebit11111101//Takethelast八位,是字节类型的补码10000011//字节类型的原码对应十进制的-33。简单总结一下,既然我们用byte类型来举例,这也是为了方便,比如你用int类型,那么任意一个数的原码都是一个很大的字符串,看的时候会眼花缭乱它。..其实byte类型的移位操作就很清楚了,其他类型也一样。看了这么多,不知道大家有没有总结出一些规律。说说我的理解吧!首先我们要明确当前的数是什么类型,移位操作后会不会超出这个类型的范围?如果超过了,我们就不能直接得出乘以2或者除以2之类的简单结论,会得到一个意想不到的数;那么,如果移位操作没有超出当前类型的范围,那么大胆的说,左移一位乘以2,右移一位除以2,向下取整!!!那么,对于一个正数,左移一位就是去掉第一个位,第一个位加0;向右移动一位添加与符号位相同的数字,并删除最后一位;负数也是一样的,***不多说了,就是无符号右移,这里要注意int类型的原二进制码,改成补码,然后进行移位操作,而截取后的8位就是我们需要的字节类型的补码,再把原码改一下,最后会变成十进制。..4、“或”、“与”、“非”、“异或”请注意java中的“或”、“与”、“非”和||、&&!别搞糊涂了,java中的这几个是用来做逻辑判断的,这里的“或”、“与”、“异或”是用来计算二进制的,跟它一点关系也没有。虽然写法有些相似,但是“或”用了一个竖线表示|,用&表示,而不是用~,“异或”用^表示。下面简单介绍一下它们的作用:或:在二进制中,两个操作数进行或运算,只要有一个为1,则结果为1,否则为0;**计算出的补码是11111011,原码是10000101,对应十进制的-5,所以-6|3的结果是-5!很简单,现在你应该知道操作数是什么了吧!publicvoidnum(){bytea=-6;System.out.println(~a);//5}和:两个操作数同时为1,则结果为1,否则为0;XOR:只看名字,两个操作数不同则结果为1,否则为0;not:是自己取反(符号位也要取反),用法如下,因为-6的补码是11111010,取反后的补码是00000101,对应十进制5publicvoidnum(){bytea=-6;System.out.println(~a);//5}5.简单练习上面这些你都明白了,那么下面的就很容易了;直接说说这个方法的用处,就是,如果你只是输入一个int类型的数,它会返回你一个2的次方,比如1,2,4,8,16.32.64等(1等于2的零次方也是2的次方2)staticfinalinttableSizeFor(intcap){intn=cap-1;n|=n>>>1;n|=n>>>2;n|=n>>>4;n|=n>>>8;n|=n>>>16;return(n<0)?1:n+1;}这个方法其实很简单,就是先把传入的int类型cap减一,赋值给n,然后进行五次无符号右移对n的操作,每次右移后,与n进行“或”运算,如果判断n小于零,则返回1,否则返回n+1。比如我们传入5,则n等于4,无符号右移一位然后与n“或”,因为位数太多,我直接写结果:0000000000000000000000000000100//n的补码00000000000000000000000000000110//无符号右移一位再与n0000000000000000000“或”//无符号右移两位再与n“或”000000000000000000000111//无符号右移四位再与n000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bitsandthen"OR"withn00000000000000000000000000000111//无符号右移十六位再与n"或"***n右移16位的结果应该是7,然后到return语句,返回的是n+1,即返回8,8为2^3,满足上述返回2的幂;有兴趣的可以试试其他的数,返回的结果一定是2的幂,是不是觉得这个算法特别牛逼!真是无敌啊!如果你了解这个方法,你可以打开你的Eclipse或者IDEA,用jdk1.8找一个叫HashMap的类,就可以看到这个方法(当然我把***的return语句稍微改了一下),这是HashMap扩容的一个方法,所以我们可以知道HashMap初始化扩容后的容量总是2的幂级数,是不是很简单!当然有时候面试官会问你HashMap的容量为什么要设置成2的幂级数?这个问题有点棘手。首先你可以把这部分算法告诉他,Mad!源码是这样写的!你还问我为什么?那还说什么呢,其实很简单,还涉及到一个“与”运算,看这个(n-1)&hash,hash是将键值对的key通过hash算法传递得到的一个大数,n是hashmap的长度,是2的次方,那么(n-1)&hash代表什么?什么?如果你有兴趣,你可以玩它。其实相当于hash%n,相当于取n的余数。这个余数必须小于n。这样一来,首先可以保证获取到的数组中的索引不会超过数组,而使用这种方式有两种方式可以保证数据在hashmap中均匀分布在数组中。我只是在这里简单提一下,很简单!