本人只是 Android小菜一个,写技术文档只是为了总结自己在最近学习到的知识,从来不敢为人师,如果里面有些不正确的地方请大家尽情指出,谢谢!
1.概述
Java 位移运算符是Java中基本的位运算操作,但是平时工作中没有直接使用到,所以一直对其没有仔细研究过,对其实现也并不清楚。最近打算学习下HashMap的实现原理,发现里面有些代码用到了位移操作,下面的代码用到了无符号右移,第一次看到这段代码的时候完全不了解这个右移到底要达到什么效果。
static final int hash(Object key){
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
小菜打算正好借着这个机会,好好学习下Java中的位移操作,首先要明白位移运算只能应用在long,int,short,char,byte这几种数据类型,而且当要应用在short,char和byte时,首先会把他们拓展成int再进行运算。Java的位移运算具体包含下面几种:
操作符
名称
含义
例子
<<
左移运算符
把操作数的二进制按位左移指定位数,并用零在右边进行补齐
60 << 2
>>
右移运算符
把操作数的二进制按位右移指定位数,并用符号位在左边进行补齐
60 >> 2
>>>
无符号右移运算符
把操作数的二进制按位右移指定位数,并用零在左边进行补齐
60 >>> 2
2. 左移运算符
左移运算就是把操作数按位进行左移,左边的位将被移除,后边新增位用零进行补齐,下图演示了一个左移过程:
由于位移操作针对的二进制数据,在示例中直接使用二进制数进行演示,分别演示了“左移1位”,“左移2位”,“左移3位”几种情况。
在左移1位的过程中,由于最左位代表是符号位,在左移的过程中这个符号位丢失,直接使用原来左二位作为符号位,同时用零在右边补齐,最终导致的结果原来的正整型数变成了负整型数.
可以用代码进行验证:
int source = 1740967785; // 二进制为:01100111110001010000111101101001
Log.i(TAG, Integer.toBinaryString(source) + " << 1 : " + Integer.toBinaryString(source << 1) + " : " + (source << 1));
Android_Test: 1100111110001010000111101101001 << 1 : 11001111100010100001111011010010 : -813031726
复制代码
可以看到由于在左移1位的过程中导致符号位发生了变化,原来的正数变成了负数。细心的读者可能会发现在输出中,原数据并没有输出我们期望的32位二进制而是只有31位,这是因为其符号位是0,在输出过程中被忽略了。
在左移2位的过程中,和左移1位比较类似,也会引起符号位的变化,这里直接给出验证结果:
int source = 1740967785; // 二进制为:01100111110001010000111101101001
Log.i(TAG, Integer.toBinaryString(source) + " << 2 : " + Integer.toBinaryString(source << 2) + " : " + (source << 2));
Android_Test: 1100111110001010000111101101001 << 2 : 10011111000101000011110110100100 : -1626063452
复制代码
在左移3位的过程中,从上面的示例图可以看到,符号位没有发生变化,右边直接用零对齐,其验证结果如下:
Log.i(TAG, Integer.toBinaryString(source) + " << 3 : " + Integer.toBinaryString(source << 3) + " : " + (source << 3));
Android_Test: 1100111110001010000111101101001 << 3 : 111110001010000111101101001000 : 1042840392
复制代码从上面的演示结果可以发现:在左移过程中,数据的符号位有可能发生变化,最终的结果就是发生由正到负或者由负到正的转变,并不是直观上认为的是原来的2倍。所谓的左移1位变为原来的2倍这个结论只适用了较小的正整数,因为它们在最初的几次左移过程中不会发生符号位的变化。
3. 右移运算符
右移运算就是把操作数按位右移,右边的位将被移除,左边新增位用符号位补齐,下图演示了一个右移过程:
图中演示了右移1位的操作,包括“正整数右移1位”和“负整数右移1位”,可以看到在右移的过程中右边会有位被移出,但左边会用对应的符号位进行补齐,如果操作数是正数就用0补齐,反之就用1补齐,这也就说明在整个右移过程中不会改变操作数的符号,正数始终为正,负数始终为负。
int source = 0b01100111110001010000111101101001;
Log.i(TAG, Integer.toBinaryString(source) + " >> 1 = " + Integer.toBinaryString(source >> 1) + " ; " + source + " >> 1 = " + (source >> 1));
Android_Test: 1100111110001010000111101101001 >> 1 = 110011111000101000011110110100 ; 1740967785 >> 1 = 870483892
复制代码
这段代码中操作数是正数,进行右移一位操作后,结果仍然是正数并且是原操作数的一半。
int source = 0b11100111110001010000111101101001;
Log.i(TAG, Integer.toBinaryString(source) + " >> 1 = " + Integer.toBinaryString(source >> 1) + " ; " + source + " >> 1 = " + (source >> 1));
Android_Test: 11100111110001010000111101101001 >> 1 = 11110011111000101000011110110100 ; -406515863 >> 1 = -203257932
复制代码
这段代码中操作数是负数,进行右移一位操作后,结果仍然是负数并且是原操作数的一半。
从上面的分析和代码验证情况看,右移过程中,操作数始终保持符号不变,且每右移一位,就变为原操作数的一半。
4. 无符号右移运算
无符号右移运算相比较右移运算更加简单,因为其在右移过程中直接在左边用0补齐即可,无须考虑当前操作数的符号,下图演示了一个无符号右移过程:
图中演示了无符号右移4位的过程,包括“正整数无符号右移4位”和“负整数无符号右移4位”,可以看到在无符号右移过程中,右边为直接移除,左边位直接用0补齐,这就保证了无论原操作数的符号位是什么在右移后都是0,即移动后都是正数。
int source = 0b01100111110001010000111101101001;
int source_2 = 0b11100111110001010000111101101001;
Log.i(TAG, Integer.toBinaryString(source) + " >>> 4 = " + Integer.toBinaryString(source >>> 4) + " ; " + source + " >>> 4 = " + (source >>> 4));
Log.i(TAG, Integer.toBinaryString(source_2) + " >>> 4 = " + Integer.toBinaryString(source_2 >>> 4) + " ; " + source_2 + " >>> 4 = " + (source_2 >>> 4));
Android_Test: 1100111110001010000111101101001 >>> 4 = 110011111000101000011110110 ; 1740967785 >>> 4 = 108810486
Android_Test: 11100111110001010000111101101001 >>> 4 = 1110011111000101000011110110 ; -406515863 >>> 4 = 243028214
复制代码
5. 总结
本文主要介绍了Java中的位移运算符,包括“<< 左移运算符”,“>> 右移运算符”和“>>> 无符号右移运算符”,在使用过程中要注意“补齐方式”和补齐后操作数的符号,简单总结如下:
操作符
补齐方式
结果符号
<<
右边用 0 补齐
和原操作数没有绝对关系,取决于左移后符号位。
>>
左边有原符号位补齐
和原操作数有相同符号。
>>>
左边用 0 补齐
和原操作数无关,一直为正数。