从Android 源码跟踪到的Java位运算的一些事儿
Posted 思忆(GeorgeQin)
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Android 源码跟踪到的Java位运算的一些事儿相关的知识,希望对你有一定的参考价值。
前言
在我们Java程序员的日常开发中因为面向对象,其实关于位运算还是接触的比较少的,但其实看看有些框架的源码,发现还有通过位运算实现的比较巧妙的设计,今天我们就来稍微了解一下位运算。
基础回顾
bit 和 byte
1)bit指“位”,是数据传输速度的计量单位,常简写为“b”;Byte指“字节”,是文件大小的计量单位,常简写为“B”。
2)Byte和bit的换算关系是,1 Byte=8 bits。在电脑上,一个英文字母需要占用1 Byte的硬盘空间,一个汉字则需占用2 Byte。
如下图:
例如,在我们java语言中,一个int 占4 byte,也就是占32bit,后面我会讲到int在一些源码里面的妙用
机器数
一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.
原码,反码,补码
原码
将一个数字转换成二进制(机器数)就是这个数值
反码
反码的表示方法是:正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。
补码
补码的表示方法是:正数的补码就是其本身;负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1。 (即在反码的基础上+1)
十进制原数 | 原码 | 反码 | 补码 |
---|---|---|---|
10 | 0000 1010 | 0000 1010 | 0000 1010 |
-10 | 1000 1010 | 1111 0101 | 1111 0110 |
5 | 0000 0101 | 0000 0101 | 0000 0101 |
-5 | 1000 0101 | 1111 1010 | 1111 1011 |
设计意义
简化了计算机的设计,计算机只能进行加法运算,通过补码的设计,使之可以在这种设计下,进行减法运算。
比如 1-1 在计算机中执行的 实际上是 1 +(-1) 即补码运算,也就是说,所有计算都是使用该数的补码,计算完成以后再换回源码。后面基于负数的计算可以详细了解。
位运算符( &、|、^、~、>>、<<、>>>)
& “与”
两个数,从最低位到最高位,一一对应。如果某 bit 的两个数值对应的值都是 1,则结果值相应的 bit 就是 1,否则为 0.
int x = 1; // 0000 0001
int y = 2; // 0000 0010
为方便后续运算和对比,我后面的运算符均使用这两个数
x&y = 0001 & 0010 = 0000 = 0
y&x = 0010 & 0001 = 0000 = 0
x&x = 0001 & 0001 = 0001 = 1
y&y = 0010 & 0010 = 0010 = 1
| “或”
两个数,从最低位到最高位,一一对应。如果某 bit 的两个数值其中一个是 1,则结果值相应的 bit 就是 1,否则为 0.
x|y = 0001 | 0010 = 0011 = 3
y|x = 0010 | 0001 = 0011 = 3
x|x = 0001 | 0001 = 0001 = 1
y|y = 0010 | 0010 = 0010 = 2
^ “异或”
两个操作数进行异或时,对于同一位上,如果数值相同则为 0,数值不同则为 1。
x^y = 0001 ^ 0010 = 0011 = 3
y^x = 0010 ^ 0001 = 0011 = 3
x^x = 0001 ^ 0001 = 0000 = 0
y^y = 0010 ^ 0010 = 0000 = 0
~ “取反”
对于这个数每一位 1变0、0变1
~x = ~0000 0001 = 1111 1111 ......(省略) 1111 1110
>> “右移运算符”
规则 a >> b 将数值 a 的二进制数值从 0 位算起到第 b - 1 位,整体向右方向移动 b 位,符号位不变,高位空出来的位补数值 0。
y>>1 = 0000...0010(源码) >>1 = 0000...0010(补码)>>1 = 0000...0001(运算后的补码)=0000 ... 0001(源码)= 1
-y>>1 = 1000 ... 0010(源码) >>1 = 1111 ... 1110(补码)>>1 = 1111 ...1111(运算后的补码)= 1000...0001(源码)= -1
//其实所有运算都经历了源码-补码-计算-源码的过程,下面就省略这个过程直接给结论
<< “左移运算符”
规则 a << b 将数值 a 的二进制数值从 0 位算起到第 b - 1 位,整体向左方向移动 b 位,符号位不变,低位空出来的位补数值 0。
y<<1 = 0000 ... 0010<<1= 0000 ... 0100 = 4
-y<<1 = 1000 ... 0010<<1= 1000 ... 0100 = -4
公式总结:
- a >> b = a / ( 2 ^ b )
- a << b = a * (2 ^ b)
>>> “无符号右移”
- 忽略符号位,空位都以0补齐
无符号右移规则和右移运算是一样的,只是填充时不管左边的数字是正是负都用0来填充,无符号右移运算只针对负数计算,并且结果一定是一个正数,因为对于正数来说这种运算没有意义
y>>>1 = 0000 ... 0010 >>>1 = 0000 ... 0001 = 1
-y>>>1 = 1000 ... 0010 (源码)>>1 = 1111 ... 1110 (补码)>> 1 == 0111 ... 1111(计算之后的补码) = 0111 ... 1111(源码)= 2147483647
//因为负数最高位补0 变成了正数,正数的补码源码都是它自己,所以变成了一个很大的数,这一点要特别注意。
应用示例
- 两个数互换
x = x^y = 0001 ^ 0010 = 0011 = 3
y = y^x = 0010 ^ 0011 = 0001 = 1
x = x^y = 0011 ^ 0001 = 0010 = 2
正好互换了,所以以后就可以这么写:
x^=y,
y^=x,
x^=y
基于以上特性,可以实现对一个数字进行加密,一串数字,对一个中间数字进行异或运算,得到加密数据,解密再次对中间数字进行异或即可。
- | 与 & 结合起来,一个int 表示多个属性
平时大家写代码是否遇到过这样的场景:一个类,有一个属性是用boolean表示,隔了一段时间,又需要新加一个boolean表示新的属性。。。所以就如同下面的代码:
public class Human
/**
* 是否是学生
*/
private boolean isStudent;
/**
* 是否已经成年
*/
private boolean isAdult;
/**
* 是否单身
*/
private boolean isSingle;
// private boolean is.....
public void setStudent(boolean student)
isStudent = student;
public boolean isStudent()
return isStudent;
// setter and getter...
那现在,通过位运算,我们可以这么写:
public class Human
/**
* 是否是学生
*/
public static final int IS_STUDENT = 1;
/**
* 是否已经成年
*/
public static final int IS_ADULT = 2;
/**
* 是否单身
*/
public static final int IS_SINGLE = 4;
private int properties;
public void setProperties(int properties)
this.properties = properties;
public boolean isStudent()
return (properties & IS_STUDENT) != 0;
public boolean isAdult()
return (properties & IS_ADULT) != 0;
public boolean isSingle()
return (properties & IS_SINGLE) != 0;
@Override
public String toString()
return "是否是学生 " + isStudent() + " 是否成年 " + isAdult() + " 是否单身 " + isSingle();
我们在传入参数只提供一个setProperties 方法,我们在传入参数的地方用 “|”运算符,分隔我们想要指定的属性,下面是测试代码:
public static void main(String args[])
Human human = new Human();
human.setProperties(Human.IS_STUDENT);
System.out.println(human.toString());
human.setProperties(Human.IS_STUDENT | Human.IS_SINGLE);
System.out.println(human.toString());
human.setProperties(Human.IS_SINGLE | Human.IS_ADULT);
System.out.println(human.toString());
human.setProperties(Human.IS_STUDENT | Human.IS_SINGLE | Human.IS_ADULT);
System.out.println(human.toString());
输出结果
是否是学生 true 是否成年 false 是否单身 false
是否是学生 true 是否成年 false 是否单身 true
是否是学生 false 是否成年 true 是否单身 true
是否是学生 true 是否成年 true 是否单身 true
原理分析:
首先,注意看,我定义的常量除了0之外都是1、2、4 、即2 ^ n
a = 1 = 0000 0001
b = 2 = 0000 0010
c = 4 = 0000 0100
d = 8 = 0000 1000
e = 16 = 0001 0000
f = 32 = 0010 0000
//......即我可以定义最多31个数(第32位表正负)
这样一来,我们用“|” 运算符将其中任意两个或者多个进行计算的时候,实际上是把它们按照自己的占位保存了例如:
int x = c|d|e = 0001 1100 = 28
//此时判断x是否包含 c 或者 d
//用&即可
int y = z&c = 0000 0100 = 4 = c
int z = z&d = 0000 1000 = 8 = d
其实只要结果不为 0000 0000 也就是0 表示&运算符成立 ,就可以判断是否包含该数字,即上面函数的方法的定义。
下面我看看,众所周知,我们android LinearLayout 有这样一个属性"showDividers":
<LinearLayout
android:showDividers="beginning|middle|end"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
分别表示你可以在view的几个位置展示分割线,为什么可以用这个属性表示三个位置呢?我们进入源码看看:
private int mShowDividers;
public static final int SHOW_DIVIDER_NONE = 0;
public static final int SHOW_DIVIDER_BEGINNING =1;
public static final int SHOW_DIVIDER_MIDDLE = 2;
public static final int SHOW_DIVIDER_END = 4;
public void setShowDividers( int showDividers)
if (showDividers == mShowDividers)
return;
mShowDividers = showDividers;
setWillNotDraw(!isShowingDividers());
requestLayout();
protected boolean hasDividerBeforeChildAt(int childIndex)
if (childIndex == getVirtualChildCount())
return (mShowDividers & SHOW_DIVIDER_END) != 0;
boolean allViewsAreGoneBefore = allViewsAreGoneBefore(childIndex);
if (allViewsAreGoneBefore)
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
else
return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0;
我只列出了上面核心的几行代码,首先,所有的的显示属性都是 一个int 的 mShowDividers 表示,set方法用“|” 进行指定,在看核心的代码在onDraw 方法中绘制分割线的时候,会调用这个方法,判断方法就是用&运算符。
另外,View.MeasureSpec 和Gravity 的类也用到了位运算符,具体这里就不深入探讨了。
参考源码:
Android :
- LinearLayout
- View.MeasureSpec
- Grivaty
以上是关于从Android 源码跟踪到的Java位运算的一些事儿的主要内容,如果未能解决你的问题,请参考以下文章