版权声明:本文作者:Qiujuer https://github.com/qiujuer; 转载请注明出处,盗版必究!!! https://blog.csdn.net/qiujuer/article/details/85319509
「WTF系列」深入Java中的位操作
学完本章节你将学会位的基础概念与语法,并且还会一些骚操作!!
开始本章节之前,我们先思考一个问题:
byte a = 33; byte b = -3;
若我们输出a、b的二进制字符串是多少?
答案是这样的么?
a->// 00100001 b->// 10100001
当然同学们可能会觉得我既然问了就肯定不是这样;是吧~别着急你们试试就知道了。
在Java中输出一个值对应的二进制方法有很多,这里提供一个简单的方法:
int value = 33; String bs = String.format("%32s", Integer.toBinaryString(value)).replace(" ", "0");
在方法中是int值,int占4字节32位,所以是: “%32s” 若是byte将32改成8即可;当然对于byte你还需要加上**“&0xFF”**来做高位清零操作。
String bs = String.format("%8s", Integer.toBinaryString(value&0xFF)).replace(" ", "0");
在Java中是采用的有符号的运算方式,故:高位为符号位,其余位存储数据信息。
简单来说:
+1 ->// 00000001 -1 ->// 10000001
默认例子中的值都按byte来算,占8位,减少大家的记忆负担。
因为byte占8位,所以有效数据存储7位,最高位为符号位。int值则是31位存储数据。
上述的-1的表示方法其实并不是机器码,而是人脑的理解方式。
我们认为+1与-1的差异就是高位不同而已,这是我们基于自然规律来看的;而机器真正存储的值其实是:11111111;这里其实就给大家提到了最初的问题。
二进制的计算规则是: 逢2进1
这个很好理解,因为表示的数字就是:0、1两个数字,想要表示更大的值就只能往前递增进步。
在平时生活中是逢10进1;因为咱们有10个数字:9、8、7、6、5、4、3、2、1、0;所以11就是:当为 0|9 增加为10的时候就进一格所以变成: 1|0 ,个位再把剩余的1补上就是: 1|1 ;所以就是11。
那么:
1就是:0|0|0|0|0|0|0|1 2就是:0|0|0|0|0|0|1|0 3就是:0|0|0|0|0|0|1|1 4就是:0|0|0|0|0|1|0|0
含义 | C语言 | Java |
---|---|---|
按位与 | a & b | a & b |
按位或 | a | b | a | b |
按位异或 | a ^ b | a ^ b |
按位取反(非) | ~a | ~a |
左移 | a << n | a << n |
右移(带符号) | a >> n | a >> n |
右移(无符号) | / | a >>> n |
byte a = (byte) 0b01011000; // 88 byte b = (byte) 0b10101000; // -88 int n = 1;
输入2个参数
输入2个参数
输入2个参数
输入1个参数
输入1个参数a;n = 1
输入1个参数b;n = 1
这里将参数换为b是因为b为负数,第一个位为1
这里需要注意的是其左边补充的值取决于b的最高位也就是 符号位 : 符号位是1则补充1,符号位是0则补充0。
输入1个参数b;n = 1
这里将参数换为b是因为b为负数,第一个位为1
这里需要注意的是其左边 补充的值永远为0 ,不管其最高位(符号位)的值。
这个小节是插曲,部分同学可能注意到上面写的进制定义是: 0b01011000 ,部分同学 可能疑惑为什么不是 0x 之类的。
范围
在上述运算法则中:两个不同长度的数据进行 位运算 时,系统会将二者按右端对齐左端补齐,然后进行 位运算 。
执行: a&b 、a|b 、a^b….等操作时:
若b = 0b01011000 补齐后:0b 00000000 00000000 00000000 01011000
若b = (byte) 0b10101000 补齐后:0b 11111111 11111111 11111111 10101000
为什么 b = 0b10101000 需要加上 (byte) 强转?
因为默认的0b10101000会被理解为:0b 00000000 00000000 00000000 10101000,这个值是一个超byte范围的int值(正数):168。
当强转 byte 后高位丢弃,保留低8位,对于byte来说低8中的高位就是符号位;所以运算后就是:-88(byte)。
相信看了上面那么多的各种规定后,大家有一定的疑问,为什么正数与负数与大家所想的不大一样呢?
我相信大家觉得正数负数就是这样的:
// 错误的理解 // 0b01011000 -> 88 : (64+16+8) // 0b11011000 -> -88 : -(64+16+8)
大家可能会想,正数与负数不就应该只是差符号位的变化么?
// 正确的理解 // 0b01011000 -> 88 : (64+16+8) // 0b10101000 -> -88 : -(64+16+8)
0b10101000 : -(64+16+8) ??WTF?? 除了符号位能懂以外请你告诉我是怎么得出 64、16、8的?
在这里我们先设两个基本的概念:
允许我先说一个小故事:对于在坐的各位来说计算1-1是非常简单的,但是对于计算机来说就是计算:00000001 与 10000001 (暂且按8位,原码)。
计算机需要识别出橙色部分的符号位,然后提取出粉色部分的数据进行计算;这里有两个问题:
但对于计算机来说做加法就够了,将 1-1换算为:1+(-1) ;OK这一步就是将所有的减法都换算为加法进行计算,减少了减法硬件模块的设计,提升了计算机的硬件利用率。
但是这里就有一个问题了,既然是将-1当作了一个值来进行运算,那么必然这个值需要方便做加法才行;按上图来说我们必不可免的需要去做一次符号位的判断,然后再做数据位的减法操作,简单来说还是在做减法。
所以若计算机的 机器码 直接采用 原码 则会导致硬件资源的设计问题。
有没有一种办法将符号位直接存储到整个结构中,让计算机在计算过程中不去管所谓的符号位与数据位?有的!就是反码。
[+1] = [00000001]原 = [00000001]反 [-1] = [10000001]原 = [11111110]反
如上图,咱们将 -1 的原码转化为了反码;此时我们使用 反[+1] + 反[-1] 进行一次运算:
此时咱们可以得到一个值x,这个值可以确定的是符号位为1,为负数,后面数据位全部为1;因为此时是 反码状态 ,所以要想我们能直接读取数据是不是应该 转化为原码状态 啊。
// 反码转原码流程就是倒过来,符号位不变,其余位为取反即可。 1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原= [00000001]反 + [11111110]反 = [11111111]反 = [10000000]原 = -0
可以看出我们已经解决好了运算的问题了,计算机只需要按照反码的方式去计算即可,只需要做加法,不需要做减法就可以运算减法流程。计算完成后对于人脑来说需要将反码转化为原码就是可读的数据了。
但上述也暴露一个问题:-0 的问题;对于0的表示将会出现两种情况:
也就是出现两种为0的表示值,-0与+0;但对于我们来说0就是0,不需要做区分。所以又引入了补码。
[+1] = [00000001]原 = [00000001]反 = [00000001]补 [-1] = [10000001]原 = [11111110]反 = [11111111]补
此时若计算机使用补码直接进行计算会怎样?
当我们使用补码计算时,因为末尾的两位均为1,1+1 = 2;对于二进制来说满2进1,所以往前进位1,进位后又遇到 1+1 = 2的情形,所以依次进位,当前位置0。
最终计算后就是: 1 00000000 ,一共9位,因为当前只有8位,所以自然就只剩下: 00000000 。
如此我们就完成了整个流程的运算,但你还需注意的是,当前运算后的值是 补码 ,也就是机器直接操作的编码;如果要还原为我们可读的值需要反向转化为 原码 。由最初定义可知,正数:原码=补码;上述补码为正数,所以原码也是:00000000。整个流程如下:
// 补码计算流程 1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [00000001]反 + [11111110]反 = [00000001]补 + [11111111]补 = [00000000]补 = [00000000]原 = 0
正数的补码就是原码
负数:
若是某个计算完成后的补码值为:10000000 那么他对应的值是什么呢?
// 按方案1来看: [10000000]补 = [11111111]反 = [10000000]原 // 按方案2来看: [10000000]补 = [11111111]补反 = [10000000]补补 = [10000000]原 可见方案1、方案2都是一样的,补码的补码就是原码。
[10000000]原 = 是等于0呢?还是-0呢?还是-128呢?
因为我们已经规定了:[00000000]原 = 0;为了充分利用位的存储区间,所以将:[10000000]原 = -128
一般情况下不会对[10000000]补码求原码,因为也没啥意义~
对于正数:
127 = [01111111]原 = [01111111]反 = [01111111]补
对于负数:
-127 = [11111111]原 = [10000000]反 = [10000001]补
对于计算机来说,其存储的值 都是补码 ,所以也就造成了一开始我们提到的问题:为什么88与-88的二进制并不只是符号位不同?
再次强调:计算机存储的是补码,为了方便运算;我们想要理解其表示的值需要转化为原码。
因为计算机计算过程中不再区别符号位,直接将符号位也纳入运算流程中;所以也就可以解释2个基础问题:(溢出)
大家可以分析一下:
上述计算在byte变量范围下进行计算,尝试分析一下补码的计算流程。
默认的对于采用补码的计算机系统而言,其存储值的有效范围是:-2^(n-1) ~ 2^(n-1) -1 ;n代表当前的位数。
若,我想在byte中存储超过127的值会怎样?
int i = 200; // 0000 0000 0000 0000 0000 0000 1100 1000 (200) byte b = (byte) 200; // 1100 1000
当我们将200强转为byte时高位丢弃仅剩下低8位:1100 1000
如果我们对byte进行输出会怎样?
System.out.println(b); // "-56"
首先其直接调用的是: public void println(int x) 方法,OK,既然是int输出为啥不是200?而是-56?
就算有这样的方法: public void println(byte x) 方法,会输出200么?也不会!!
首先对于byte b来说: 1100 1000 这是一个负数的补码,其原码流程是:
[1100 1000]补 = [1011 0111] = [1011 1000]原 = -(32+16+8) = -56
这里有一个有趣的事情,int转byte时是直接丢掉高位的所有数据:24个0;但byte转int时,补充高24位时是根据当前的符号位来补充的,若当前符号位是1则添1,若符号位是0则添0;对于byte来说第一位就是符号位,当前的 1100 1000 符号位是**“1”**所以添加的就是24位1。
int c = b; // b -> 1100 1000 // c -> 1111 1111 1111 1111 1111 1111 1100 1000
若直接打印的是byte值,就是-56;上面我们分析1100 1000的原码时就已经证明了。那么打印c是不是呢?
对于范围较少的类型转换位大类型时不会丢失数据,原来是什么就是什么。
OK,就算不是上面那句话,我们来看看:
[1111 1111 1111 1111 1111 1111 1100 1000]补 = [1000 0000 0000 0000 0000 0000 0011 0111] = [1000 0000 0000 0000 0000 0000 0011 1000]原 = -(32+16+8) = -56
若我们转换为int时想要还原最初的200这个值该如何办?
分析上面的补码,可以看出其与最初的补码差异仅仅在于左边24位的不同:
[1111 1111 1111 1111 1111 1111 1100 1000]补 = -56 [0000 0000 0000 0000 0000 0000 1100 1000]补 = 200
那么我们只需要将前面的24位重置为0即可,这里就有一个与操作的简单用法:
/** * * 1111 1111 1111 1111 1111 1111 1100 1000 (the int) * & * 0000 0000 0000 0000 0000 0000 1111 1111 (the 0xFF) * ======================================= * 0000 0000 0000 0000 0000 0000 1100 1000 (200) */ System.out.println(b & 0xFF); // "200"
在这里我们做了一次特殊的: b & 0xFF 操作,b 转换为int之后的值与 0xFF 进行按位与操作。
0xFF = 255 其int原码为:0000 0000 0000 0000 0000 0000 1111 1111,恰好最后8位为1,其余24位为0;所以可以用来做高位擦除操作。
这样的用法可用以存储超范围的数据,比如对于文件的大小来说永远都是 >= 0,不可能会使用到 < 0 的值,所以对于原始的我们可以根据这个,使用较少的byte表示更多的区间,简单来说就是 无符号 。将符号位也用以存储数据。
int i = 0xFF60; // 65376 System.out.println(i); // 00000000000000001111111101100000 System.out.println(String.format("%32s", Integer.toBinaryString(i)).replace(" ", "0")); byte b1 = (byte) i; byte b2 = (byte) (i >> 8); // 01100000 System.out.println(String.format("%8s", Integer.toBinaryString(b1 & 0xFF)).replace(" ", "0")); // 11111111 System.out.println(String.format("%8s", Integer.toBinaryString(b2 & 0xFF)).replace(" ", "0")); int ret = (b1 & 0xFF) | (b2 & 0xFF) << 8; System.out.println(String.format("%32s", Integer.toBinaryString(ret)).replace(" ", "0")); // 65376 System.out.println(ret);
若没有做 & 0xFF 操作,其值应是:
/* * 0000 0000 0000 0000 0000 0000 0110 0000 (b1) * | * 1111 1111 1111 1111 1111 1111 0000 0000 (b2<<8) * ======================================= * 1111 1111 1111 1111 1111 1111 0110 0000 (-160) */ System.out.println(b1 | b2 << 8); // "-160"
65376 本质来说超过了short的存储范围:-32768~32767 ,但其在int中依然只需占2个字节16位:65376<65536。所以我们只需要使用2个byte即可存储,而不需要int的4个byte来存储。
在Socket传输中使用这样的方式能有效降低传输的字节冗余。
有这样一个情形:一个四边形,四条边可以是虚线也可以是实线,四条边相互独立;定义为 a/b/c/d 四边;此时我们需要在画布上画出这个四边形;但是因为4边相互独立,所以我们常见的就是定义4个bool值:
boolean a = true; boolean b = false; boolean c = false; boolean d = true; void changeA(boolean fullLine) { a = fullLine; }
简单来说我们定义这样的方式其一比较麻烦,其二总占用的内存空间至少是4个byte,也有可能是16byte(按int存的情况)。
但是我们表示的内容无非就是2种:实线、虚线
所以我们可以这样做:
static byte a = 0b00000001; static byte b = 0b00000010; static byte c = 0b00000100; static byte d = 0b00001000; byte x = 0b00000000;
定义a、b、c、d为static,并且使用最后的4位即可。
若我们想要改变a边的实虚:
void changeA(boolean fullLine) { if (fullLine) { x = (byte) (x | a); } else { x = (byte) (x & ~a); } }
通过该方法,若a边为实线,则将a flag的值填入x中,反之擦除掉x中的a边信息;同时保证其他信息不变。
若要拿,也就是判断a是否为实线该如何办?
boolean isFullLine() { // return (x & a) != 0; return (x & a) == a; }
2种写法都是OK的,不过需要注意若对应的a使用了符号位则需要使用0xFF先清理自动补充的符号位。因为与、或、非等操作默认会将参数转化为int类型进行;所以会出现自动补充符号位的情况。
这样的操作方案在Android或Socket传输中都是非常常见的,比如Socket NIO中的SelectorKey中的ops变量就是这样的机制;这能有效减少存储多个参数的情况;并且位操作并不会带来多少计算负担。
以上就是关于Java 位操作的常见疑问与原理的讲解,其实还有一些深入的东西,比如:同余、负数取模、小数、规律运算等;这些因为使用较少并且篇幅有限就等下期再给大家一一介绍了。
我是QIUJUER,我在慕课授课,新课上线:
Socket网络编程进阶与实战
手把手开发一个完整即时通讯APP
请大家多多支持正版,包会教学、相信你能收获到更多~~