从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)

十进制原数原码反码补码
100000 10100000 10100000 1010
-101000 10101111 01011111 0110
50000 01010000 01010000 0101
-51000 01011111 10101111 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位运算的一些事儿的主要内容,如果未能解决你的问题,请参考以下文章

Java 关于二进制的一些内容

逻辑运算和位运算

Java 位运算符

java位移运算符有啥意义

位运算在Android中的应用

位运算在Android中的应用