Java移位操作及应用

0. 注意

阅读本文之前,务必搞清楚计算机中有关源码,补码的相关概念,位运算 & (按位与) | (按位或) ~ (取反) ^ (异或)相关概念和操作

1. 计算某个Long类型正数 二进制表示法中1的个数

基础知识

  • Java中各种进制的表示方法:
    • 二进制 0B1010_1000 【以0B ,或者 0b (第一个是零,b不区分大小写)】
    • 八进制 0123456 【以零开头】
    • 十六进制 0X12AB 【以零x开头,x不区分大小写】

对于一个数X,表达式 X & (X-1) 的含义是 : 将X(二进制) 最右边的1 清零,即把最右边的 1 设置为 0 。

举例(以整数为例,Java中一个整数占用4个字节,下划线是为了方便查看,Java编译器支持这种写法):

  • x = 8 (0B0000_0000_0000_1000)

    x-1 = 7 (0B0000_0000_0000_0111)

    8&7 (0B0000_0000_0000_0000)

    对比 8&7 与 8 的二进制,发现 最右边的 1 已经变成0 了

  • x = 7 (0B0000_0000_0000_0111)

    x-1 = 6 (0B0000_0000_0000_0110)

    7&6 (0B0000_0000_0000_0110)

    对比 7&6 与 7 的二进制,发现 最右边的 1 已经变成0 了

代码:不停的将target最右边的1 清零,清零一次,计数一次,直到 target为0为止。

	public int countOne(long target){
        int result=0;
        for(;target!=0;result++){
            target &=target-1;
        }
        return result;
    }
    @Test
    public void test1(){
       long target= 8L;
        assertEquals(1,countOne(target));//OK

        target=7L;
        assertEquals(3,countOne(target));//OK
    }

2. 判断(二进制表示)某一位上是0还是1

思路: int 类型数 119 (0B0000_0000_0111_0111‬),Java中是4个字节32位。从左往右,高位在左,底位在右(与10进制表示一样)

在这里插入图片描述

第5位(位数从0开始)的值是 1,如何判断?这要用到 左移操作,数字 1(0B0000_0000_0000_0001 因为Java中的byte,char,short,运算的时候会自动提升为int类型,所以用4个字节表示)左移1位就是:0B0000_0000_0000_0010,即 12^1 其实就是 2,左移2位就是 0B0000_0000_0000_0100 ,即 12^2, 为4 (左移位补右边补0,左边被挤掉丢弃),移到第5位的位置需要左移5次,即

0B0000_0000_0010_0000,而 119是

0B0000_0000_0111_0111, 现在将这两个数按位与 (&),其结果为:

0B0000_0000_0010_0000

假设第5位上的不是1,而是0, 那么最后的结果一定全部都是0

所以可以认定:最后的结果如果不是0,那么这一位是肯定是1,如果是0,那么这一位上就一定是 0

根据这个思路,代码如下:

	public boolean getBit(Long target,int offset){
        // 这里一定要写成 1L,后面2.1 小节会解释
        return (target & 1L<<offset)!=0;
    }
    @Test
    public void test2(){
        long target= 119L;
        assertTrue(getBit(target,5)); //OK

        target=0B0000_0000_0101_0111;// 87
       assertFalse(getBit(target,5)); //OK
    }

2.1 Java中关于左移的一个坑

首先看一段代码:

@Test
public void test2_1(){
    int resultInt=1<<31;
    //打印result的二进制表示
    //输出: 10000000000000000000000000000000
    System.out.println(Integer.toBinaryString(resultInt));

    long resultLong= 1<<63;
    //输出:1111111111111111111111111111111110000000000000000000000000000000
    System.out.println(Long.toBinaryString(resultLong));
}

第3行代码,因为移动了31位,在整数的范围内,所以用整数接收结果,输出的结果没有问题。

但是下面的移位63,输出的结果并不是预期的结果,预期的结果应该是第 63位(最左边的最高为)为1,其余全部是0才对,为什么中间多了好多1?

原来Java中左移运算符<< 在运算的时候是有要求的。JVM会检查数据类型,也就是检查 1 ,发现是int类型的,int是4个字节32位,那么它会做 63%32 运算,结果是31,这个31才是真正要左移的位数

上面的代码 1<<63, 其实真正左移了31位,就变成了10000000000000000000000000000000 ,它是个32位的整数,最高位为1,现在要将这个整型的值赋值给一个long类型(类型自动提升),为了不改变这个数值(它是个负数,最高位为1),在前面补上了32个1,这就是第10行输出的结果。

搞清楚了运算符 << 的规则后,代码做如下改变就可以得到我们想要的结果:

 long resultLong= 1L<<63;

只需要在1后面添加一个 L或者是l(小写),表明 1 现在是一个 long类型的,那么移位之前,做 64%63, 结果是 63,这才是真正要移动的位数

    @Test
    public void test2_1(){
        int resultInt=1<<31;
        //打印result的二进制表示
        //输出: 10000000000000000000000000000000
        System.out.println(Integer.toBinaryString(resultInt));

        long resultLong= 1<<63;
        //输出:1111111111111111111111111111111110000000000000000000000000000000
        System.out.println(Long.toBinaryString(resultLong));

        resultLong= 1L<<63;
        //输出:1000000000000000000000000000000000000000000000000000000000000000
        System.out.println(Long.toBinaryString(resultLong));
    }

3. 将二进制表示的某一位设置为1

第i 位(i从0开始)和0 或 (|) 保持不变,和1 或(|) 变成1,所以代码如下:

    public long setBitTrue(Long target, int offset){
        //注意1后面有一个L表示它是一个long类型的
        return target | 1L<<offset; 
    }
    @Test
    public void test3(){
        //注意这是long类型,共8个字节
        long target=0B0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0001L; //1
        assertEquals(3,setBitTrue(target,1));  //OK,结果是3

        //注意long类型的最高位是 63,同时也是符号位,这里只是将15位上设置成1了,符号位并没有改变
        assertEquals(32769,setBitTrue(target,15)); //OK,结果是32769

        //输出 1000000000000000000000000000000000000000000000000000000000000001
        System.out.println(Long.toBinaryString(setBitTrue(target,63)));

        //这里改变最高位,第63位为1,这个是符号位,由 0 变成了1,变成了负数
        //如果不明白这句话,需要补脑源码,补码的相关知识
        assertEquals(-9223372036854775807L,setBitTrue(target,63));
    }

那么问题来了,为什么是 -9223372036854775807 ?

首先看 long类型的1 在内存的表示是:

0B0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0001 (8字节,64位)

最高为,第 63位置为1 后的结果是:

0B1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0001

计算机中存储的数字二进制都是以补码的形式存放的,最高位为1,即符号位为1,那它必定是一个负数,那它的值是什么?将这个值取反后+1 就是这个数的绝对值(正数表示),也就是:

0B0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111, 可以直接用代码:

System.out.println(0B0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111L)

上面代码输出: 9223372036854775807 ,因为最高为是1,是一个负数 ,所以最终的结果就是-9223372036854775807

4. 将二进制表示的某一位设置为0

要将一个数如 -1 (0B1111_1111_1111_1111_1111_1111_1111_1111) ,整数,四个字节的第0 为设置为1, 只需要与

​ 0B1111_1111_1111_1111_1111_1111_1111_1110 做 & 操作即可,因为 和 1与(&) 不变, 和 0 与(&) 就会变成0 。

问题是如何得到 0B1111_1111_1111_1111_1111_1111_1111_1110 呢? 这个简单,只需要得到 0B0000_0000_0000_0000_0000_0000_0000_0001 , 然后取反 (~ 大键盘数字键1左边的那个键)

代码如下:

    public long  setBitFalse(Long target,int offset){
        long mask= ~(1L<<offset);
        return target & mask;
    }
    @Test
    public void test4(){
        long target=-1;
        //把第1位由1变成0, 相当于 -1,所以最终的结果是 -2
        assertEquals(-2,setBitFalse(target,0));
    }

5. 判断二进制表示的哪些位为1

如一个int类型的整数 10(4个字节), 二进制表示为: 0B0000_0000_0000_0000_0000_0000_0000_1010 , 为1的位有: [3,1]

因为一个int类型的占用4个字节,共32位,要判断有哪些位为1,只需要不断的做无符号右移操作,每次判断最末尾是否是1即可。

代码如下:

    public int[] getBitOffsets(Long target){
        //调用前面的contBit方法确定有多少个1
        int[] result=new int[contBit(target)];
        int resultOffset=0;

        //不停的做无符号右移,为0的时候循环结束
        for(int i=0;target!=0;i++){
            if((target & 1L)==1L){
                result[resultOffset++]=i;
            }
            target >>>=1;
        }
        return result;
    }

    @Test
    public void test5(){
        long target=10L;
        System.out.println(Long.toBinaryString(target));//1010

        int[] result=getBitOffsets(target);
        assertArrayEquals(new int[]{1,3},result); //OK

        target=-1L;// 64个1
        //输出 [0,1,2...63] 也就是所有位上都是1
        System.out.println(Arrays.toString(getBitOffsets(target)));
    }

6. 应用场景

6.1 整数排序

需求: 假设有一个整数序列,序列中的每个值都不超过64,而且值没有重复,序列的长度最长为64,现在对这个序列进行排序。

因为序列的长度不超过64,序列中的每个值都不超过64,而long类型刚好是64位,所以就可以将要排序的序列"映射"到二进制的序列上。比如要排序的序列是: {34,56,23,45,24,15,14,10,3,16},映射结果如下:

在这里插入图片描述

图中只标注了1, 其余不存在的数字均标记为0,上图为了保持整洁,并没有画出

代码:

    @Test
    public void test6_1(){
        int[] targetArray={34,56,23,45,24,15,14,10,3,16};

        long bitMap=0;//所有的二进制位全部置为0

        for(int item:targetArray){
            bitMap=setBitTrue(bitMap,item);
        }
        System.out.println("排序结果:");
        System.out.println(Arrays.toString(getBitOffsets(bitMap)));
    }

6.2 大数据量排序

上面的排序实际中很少用到,但是提供了一个BitMap(按位映射)的思路。假设最多有 200 亿个long类型的数据需要排序,那么该如何来排序?

首先考虑这200 亿个数据存储的问题,假设存储到文件中,那么这个文件有多大?

200 亿 * 8 字节= 200 * 10^8 * 8 字节=1600 * 10 ^8 字节=1562.5 * 10^5 KB = 1525.87890625 * 10^2 MB = 152587.890625MB =149.011GB, 这个数据将近149 G,很显然要将这么庞大的数据读入内存再排序是不可能的。

那如果使用 200 亿个比特位来映射这些数据呢?看看需要多少内存?

200 * 10^8 比特位 = 200/8 * 10^8 字节= 25 * 10^8 字节 = 24.414 * 10^5 KB = B= 23.8418* 10^2 MB = 2.3283GB

也就是说 200亿个比特位占用不到 2.4 G 的内存,这个用一般的PC机内存是可以存储的。

但是这么长的比特位在Java中如何构建出来呢?Java中提供了一个类 java.util.BitSet, 可以将它看成是一个可变长的比特位序列,每个元素都是一个boolean类型的值,其实就是 0和1 ,我们可以创建一个BitSet实例对象,然后将这200亿个数字映射到序列上,数字就是索引号,数字存在该索引号对应的值就是1,不存在对应的就是false。

下面对20个long类型的数据使用 BitSet进行排序:

 @Test
    public void test6_2(){
        long[] targetArray={32424L,4324324L,243123412L,223423L,54564L,64767L,7476L,432143L,67L,647L,
        8657L,5765L,654L,7654L,345L,7658L,979L,2345L,85876L,2354L};

        BitSet bitSet=new BitSet(targetArray.length);
        for(long item:targetArray){
            bitSet.set(Long.valueOf(item).intValue());
        }
        //bitSet 中最高的索引+1, 因为bitSet的索引从0开始的
        // int maxIndex= bitSet.length();

        int first=bitSet.nextSetBit(0) ;//返回第一个设置为 true 的位的索引,这发生在指定的起始索引或之后的索引上。如果没有则返回-1
        for(;first>0 ;first=bitSet.nextSetBit(first+1)){
            System.out.print(first+" ");
        }
        //输出 : 67 345 647 654 979 2345 2354 5765 7476 7654 7658 8657 32424 54564 64767 85876 223423 432143 4324324 243123412 
    }

使用这种 BitMap的方法来 判断元素是否存在重复也非常的容易,如果索引对应的值为true,表示这个值已经存在了。

同时 BitSet 也支持 &与 , |或 , ^异或 , 的操作,分别使用对应的方法 (and, or , xor ) ,详情请参考 API文档

BitSet 内部的二进制序列实际上是由多个 long类型的整数组合而成的。

6.3 连续签到场景

现在的app上都有签到的场景。每日签到,如果连续7日签到,获得送积分或者送优惠券的奖励。先假设如下的条件:

某app用户不超过 64人 ,id编号从0 开始 (实际场景的用户数要远比这个数大,这里只设置简单场景),连续签到3日可获得奖励。

  • 准备3 个long数据 用来存储每日签到,它的二进制序列的索引对应 用户编号,签到则将对应的二进制位置为1
  • 要判断是否连续签到,只需要将 3个long类型的值 做 &(与)运算,得到的结果就是这3天连续签到的用户
    在这里插入图片描述

id为13和10 的用户连续3日签到。

@Test
    public void test6_3(){
        long[] days={0,0,0}; // 3个long类型的二进制序列
        days[0] = setBitTrue(days[0],2); // 调用前面定义的方法
        days[0] = setBitTrue(days[0],10);
        days[0] = setBitTrue(days[0],13);  //第一天 2,10,13 号用户签到

        days[1] = setBitTrue(days[1],0);
        days[1] = setBitTrue(days[1],7);
        days[1] = setBitTrue(days[1],8);
        days[1] = setBitTrue(days[1],10);
        days[1] = setBitTrue(days[1],13);  //第二天 0,7,8,10,13 号用户签到

        days[2] = setBitTrue(days[1],3);
        days[2] = setBitTrue(days[1],10);
        days[2] = setBitTrue(days[1],13);
        days[2] = setBitTrue(days[1],14);
        days[2] = setBitTrue(days[1],15);  //第三天 3,10,13,14,15 号用户签到

        //求连续签到列表
        long result= days[0] & days[1] & days[2];

        int[] list= getBitOffsets(result);
        System.out.println("连续签到列表:"+Arrays.toString(list));
        //输出==> 连续签到列表:[10, 13]
    }

上面使用的是long类型来映射用户,实际场景中用户会非常多,如果用户有 20亿 ,就需要使用单独的服务器来存储。如果服务器能做到高可用,使用 BitSet 也是可以的。但这些数据直接放在内存也是不可取的,可以使用Redis,Redis是一个内存NOSQL数据库,它也支持 这种 BitMap的映射。

6.4 权限

用户在系统中的活动往往会有限制,如果系统中所有的权限加起来不超过 64,就也可以使用BitMap的方式来映射。假设有4中权限,伪代码如下:

long permissionCrate= 1L << 0 ;
long permissionUpdate= 1L << 1 ;
long permissionDelete= 1L << 2 ;
long permissionQuery= 1L << 3 ;

某个用户A拥有 permissionCrate, permissionQuery 两种权限,伪代码如下:

long userA_permissions = permissionCrate | permissionQuery ;

现在用户A 登录系统后,要做 permissionDelete 操作, 此时就需要鉴权:

if( permissionDelete & userA_permissions == permissionDelete){
    // 可以操作
}else {
    // 没有权限
}

7. 字节数组与 long/int之间的相互转换

java中 long类型占用8个字节,int占用 4 个字节, 那么如何将它们转换为 字节数组。

为什么有将long转换为字节数组的需求呢?有这样的一个场景:

两个用户之间需要传递文件,用户A 选择了一个文件列表传递给用户B,他们之间使用socket进行通信。我们知道socket通信的时候,我们要操作的主要是比特流(二进制流)。

实现的时候首先要获取 A 文件列表中的一个文件,读取文件的名称,文件的字节数. 接着向socket流写入一个long类型的数据,这个数据表示文件名的长度,然后再将文件名转换为字节数组写入流中,然后再写入一个long类型的数据,这个long类型的数据表示整个文件的长度,最后写入文件的二进制字节,下一个文件再按照这个规则写入流中。

用户B 需要从 socket流中读取字节,他首先要先读取4个字节(long类型数据),就知道了后面文件名要读取的字节数,紧接着按照这个字节数读取流就得到了文件名,接着读取4个字节,这表示文件内容的长度,按照这个长度读取流的内容,这样一个文件就算是传递完成了,接着按照前面的方式读取下一个文件。

在这里插入图片描述

在上面的场景中就用到了需要将int, long类型的值以 字节数组的方式写入到流中,那么读取解析的时候,又需要将字节数组转换为int或者long。

7.1 long/int 转字节数组

  • long或者int 拆分成字节数组

long或者int 二进制序列 最右边的 8 位(一个字节),它应该是字节数组的最后一个元素, 最左边的8位(一个字节)为数组的第一个元素

在这里插入图片描述

那么如何将每8位(1个字节)拆分出来,然后放到字节数组中?

拆分最前面的 00 ,只需要整体无符号右移 7个字节的长度,共56 个二进制位,这样它就会到达最末端,然后与 0xFF 做 & 运算,这样就将这个字节拆出来了。

接着的01 ,只需要整体无符号右移6个字节的长度,共48个二进制位,这样它也到达最末端,然后与0xFF 做 & 运算

其它一次类推即可。

  • 数组组装成long或者int

过程刚好与拆成字节数组相反,对于 00这个字节,需要与 0x00000000000000FFL做与运算(注意最后有一个L,表示long类型),这样就将它提升为long类型,然后 左移 7个字节的位置(56位),同理,01这个字节要左移 64位,最终将每个移动后的结果做 | 运算,就将一个long类型的数据组装好了。

注意左移的时候,一定要将类型提升为long类型后,也就是 & 0x00000000000000FFL 运算,再左移 56,要不然,移动的就不是56位,而是 56%32 = 24 位,因为java中 byte类型在参与运算的时候会提升为int类型,而int类型是 32 位,所以移位的时候会做 56%32 的操作,真正移动的是24位,而不是56位,这样最终的结果就会出现错误。

   public byte[] long2Bytes(long value){ //高位在左边,低位在右边
        byte b0=(byte)((value >>> 56) & 0xFF); //无符号右移,左边补0
        byte b1=(byte)((value >>> 48) & 0xFF);
        byte b2=(byte)((value >>> 40) & 0xFF);
        byte b3=(byte)((value >>> 32) & 0xFF);
        byte b4=(byte)((value >>> 24) & 0xFF);
        byte b5=(byte)((value >>> 16) & 0xFF);
        byte b6=(byte)((value >>> 8) & 0xFF);
        byte b7=(byte)(value & 0xFF);
        return new byte[]{b0,b1,b2,b3,b4,b5,b6,b7};

    }
    public long bytes2Long(byte[] bytes){
        long b0=(bytes[0] & 0x00000000000000FFL) << 56;
        long b1=(bytes[1] & 0x00000000000000FFL) << 48;
        long b2=(bytes[2] & 0x00000000000000FFL) << 40;
        long b3=(bytes[3] & 0x00000000000000FFL) << 32;
        long b4=(bytes[4] & 0x00000000000000FFL) << 24;
        long b5=(bytes[5] & 0x00000000000000FFL) << 16;
        long b6=(bytes[6] & 0x00000000000000FFL) << 8;
        long b7=bytes[7] & 0x00000000000000FFL;
        return b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7;
    }
    public byte[]  int2Bytes(int value){
        byte b0=(byte)((value >>> 24) & 0xFF);
        byte b1=(byte)((value >>> 16) & 0xFF);
        byte b2=(byte)((value >>> 8) & 0xFF);
        byte b3=(byte)(value & 0xFF);
        return new byte[]{b0,b1,b2,b3};
    }
    public int  bytes2Int(byte[] bytes){
        int b0=(bytes[0] & 0x000000FF) << 24;
        int b1=(bytes[1] & 0x000000FF) << 16;
        int b2=(bytes[2] & 0x000000FF) << 8;
        int b3=bytes[3] & 0x000000FF;
        return b0 | b1 | b2 | b3;
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
所有的整数类型以二进制数字位的变化及其宽度来表示。例如,byte 型值42的二进制代码是00101010 ,其中每个位置在此代表2的次方,在最右边的位以20开始。向左下一个位置将是21,或2,依次向左是22,或4,然后是8,16,32等等,依此类推。因此42在其位置1,3,5的值为1(从右边以0开始数);这样42是21+23+25的和,也即是2+8+32 。 所有的整数类型(除了char 类型之外)都是有符号的整数。这意味着他们既能表示正数,又能表示负数。Java 使用大家知道的2的补码(two's complement )这种编码来表示负数,也就是通过将与其对应的正数的二进制代码取反(即将1变成0,将0变成1),然后对其结果加1。例如,-42就是通过将42的二进制代码的各个位取反,即对00101010 取反得到11010101 ,然后再加1,得到11010110 ,即-42 。要对一个负数解码,首先对其所有的位取反,然后加1。例如-42,或11010110 取反后为00101001 ,或41,然后加1,这样就得到了42。 如果考虑到零的交叉(zero crossing )问题,你就容易理解Java (以及其他绝大多数语言)这样用2的补码的原因。假定byte 类型的值零用00000000 代表。它的补码是仅仅将它的每一位取反,即生成11111111 ,它代表负零。但问题是负零在整数数学中是无效的。为了解决负零的问题,在使用2的补码代表负数的值时,对其值加1。即负零11111111 加1后为100000000 。但这样使1位太靠左而不适合返回到byte 类型的值,因此人们规定,-0和0的表示方法一样,-1的解码为11111111 。尽管我们在这个例子使用了byte 类型的值,但同样的基本的原则也适用于所有Java 的整数类型。 因为Java 使用2的补码来存储负数,并且因为Java 中的所有整数都是有符号的,这样应用位运算符可以容易地达到意想不到的结果。例如,不管你如何打算,Java 用高位来代表负数。为避免这个讨厌的意外,请记住不管高位的顺序如何,它决定一个整数的符号。 二 位逻辑运算符 位逻辑运算符有“与”(AND)、“或”(OR)、“异或(XOR )”、“非(NOT)”,分别用“&”、“|”、“^”、“~”表示,4-3 表显示了每个位逻辑运算的结果。在继续讨论之前,请记住位运算符应用于每个运算数内的每个单独的位。 表4-3 位逻辑运算符的结果 A 0 1 0 1 B 0 0 1 1 A | B 0 1 1 1 A & B 0 0 0 1 A ^ B 0 1 1 0 ~A 1 0 1 0 按位非(NOT) 按位非也叫做补,一元运算符NOT“~”是对其运算数的每一位取反。例如,数字42,它的二进制代码为: 00101010 经过按位非运算成为 11010101 按位与(AND) 按位与运算符“&”,如果两个运算数都是1,则结果为1。其他情况下,结果均为零。看下面的例子: 00101010 42 &00001111 15 00001010 10 按位或(OR) 按位或运算符“|”,任何一个运算数为1,则结果为1。如下面的例子所示: 00101010 42 | 00001111 15 00101111 47 按位异或(XOR) 按位异或运算符“^”,只有在两个比较的位不同时其结果是 1。否则,结果是零。下面的例子显示了“^”运算符的效果。这个例子也表明了XOR 运算符的一个有用的属性。注意第二个运算数有数字1的位,42对应二进制代码的对应位是如何被转换的。第二个运算数有数字0的位,第一个运算数对应位的数字不变。当对某些类型进行位运算时,你将会看到这个属性的用处。 00101010 42 ^ 00001111 15 00100101 37 位逻辑运算符的应用 下面的例子说明了位逻辑运算符: // Demonstrate the bitwise logical operators. class BitLogic { public static void main(String args[]) { String binary[] = {"0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111" }; int a = 3; // 0 + 2 + 1 or 0011 in binary int b = 6; // 4 + 2 + 0 or 0110 in binary int c = a | b; int d = a & b; int e = a ^ b; int f = (~a & b) | (a & ~b); int g = ~a & 0x0f; System.out.println(" a = " + binary[a]); System.out.println(" b = " + binary[b]); System.out.println(" a|b = " + binary[c]); System.out.println(" a&b = " + binary[d]); System.out.println(" a^b = " + binary[e]); System.out.println("~a&b|a&~b = " + binary[f]); System.out.println(" ~a = " + binary[g]); } } 在本例中,变量a与b对应位的组合代表了二进制数所有的 4 种组合模式:0-0,0-1,1-0 ,和1-1 。“|”运算符和“&”运算符分别对变量a与b各个对应位的运算得到了变量c和变量d的值。对变量e和f的赋值说明了“^”运算符的功能。字符串数组binary 代表了0到15 对应的二进制的值。在本例中,数组各元素的排列顺序显示了变量对应值的二进制代码。数组之所以这样构造是因为变量的值n对应的二进制代码可以被正确的存储在数组对应元素binary[n] 中。例如变量a的值为3,则它的二进制代码对应地存储在数组元素binary[3] 中。~a的值与数字0x0f (对应二进制为0000 1111 )进行按位与运算的目的是减小~a的值,保证变量g的结果小于16。因此该程序的运行结果可以用数组binary 对应的元素来表示。该程序的输出如下: a = 0011 b = 0110 a|b = 0111 a&b = 0010 a^b = 0101 ~a&b|a&~b = 0101 ~a = 1100 三 左运算符 左运算符<<使指定值的所有位都左规定的次数。它的通用格式如下所示: value << num 这里,num 指定要移位值value 动的位数。也就是,左运算符<<使指定值的所有位都左num位。每左一个位,高阶位都被出(并且丢弃),并用0填充右边。这意味着当左的运算数是int 类型时,每动1位它的第31位就要被出并且丢弃;当左的运算数是long 类型时,每动1位它的第63位就要被出并且丢弃。 在对byte 和short类型的值进行移位运算时,你必须小心。因为你知道Java 在对表达式求值时,将自动把这些类型扩大为 int 型,而且,表达式的值也是int 型。对byte 和short类型的值进行移位运算的结果是int 型,而且如果左不超过31位,原来对应各位的值也不会丢弃。但是,如果你对一个负的byte 或者short类型的值进行移位运算,它被扩大为int 型后,它的符号也被扩展。这样,整数值结果的高位就会被1填充。因此,为了得到正确的结果,你就要舍弃得到结果的高位。这样做的最简单办法是将结果转换为byte 型。下面的程序说明了这一点: // Left shifting a byte value. class ByteShift { public static void main(String args[]) { byte a = 64, b; int i; i = a << 2; b = (byte) (a << 2); System.out.println("Original value of a: " + a); System.out.println("i and b: " + i + " " + b); } } 该程序产生的输出下所示: Original value of a: 64 i and b: 256 0 因变量a在赋值表达式中,故被扩大为int 型,64(0100 0000 )被左两次生成值256 (10000 0000 )被赋给变量i。然而,经过左后,变量b中惟一的1被出,低位全部成了0,因此b的值也变成了0。 既然每次左都可以使原来的操作数翻倍,程序员们经常使用这个办法来进行快速的2 的乘法。但是你要小心,如果你将1进高阶位(31或63位),那么该值将变为负值。下面的程序说明了这一点: // Left shifting as a quick way to multiply by 2. class MultByTwo { public static void main(String args[]) { int i; int num = 0xFFFFFFE; for(i=0; i<4; i++) { num = num << 1; System.out.println(num); } } 这里,num 指定要移位值value 动的位数。也就是,左运算符<<使指定值的所有位都左num位。每左一个位,高阶位都被出(并且丢弃),并用0填充右边。这意味着当左的运算数是int 类型时,每动1位它的第31位就要被出并且丢弃;当左的运算数是long 类型时,每动1位它的第63位就要被出并且丢弃。 在对byte 和short类型的值进行移位运算时,你必须小心。因为你知道Java 在对表达式求值时,将自动把这些类型扩大为 int 型,而且,表达式的值也是int 型。对byte 和short类型的值进行移位运算的结果是int 型,而且如果左不超过31位,原来对应各位的值也不会丢弃。但是,如果你对一个负的byte 或者short类型的值进行移位运算,它被扩大为int 型后,它的符号也被扩展。这样,整数值结果的高位就会被1填充。因此,为了得到正确的结果,你就要舍弃得到结果的高位。这样做的最简单办法是将结果转换为byte 型。下面的程序说明了这一点: // Left shifting a byte value. class ByteShift { public static void main(String args[]) { byte a = 64, b; int i; i = a << 2; b = (byte) (a << 2); System.out.println("Original value of a: " + a); System.out.println("i and b: " + i + " " + b); } } 该程序产生的输出下所示: Original value of a: 64 i and b: 256 0 因变量a在赋值表达式中,故被扩大为int 型,64(0100 0000 )被左两次生成值256 (10000 0000 )被赋给变量i。然而,经过左后,变量b中惟一的1被出,低位全部成了0,因此b的值也变成了0。 既然每次左都可以使原来的操作数翻倍,程序员们经常使用这个办法来进行快速的2 的乘法。但是你要小心,如果你将1进高阶位(31或63位),那么该值将变为负值。下面的程序说明了这一点: // Left shifting as a quick way to multiply by 2. class MultByTwo { public static void main(String args[]) { int i; int num = 0xFFFFFFE; for(i=0; i<4; i++) { num = num << 1; System.out.println(num); } } } 该程序的输出如下所示: 536870908 1073741816 2147483632 -32 初值经过仔细选择,以便在左 4 位后,它会产生-32。正如你看到的,当1被进31 位时,数字被解释为负值。 四 右运算符 右运算符>>使指定值的所有位都右规定的次数。它的通用格式如下所示: value >> num 这里,num 指定要移位值value 动的位数。也就是,右运算符>>使指定值的所有位都右num位。下面的程序片段将值32右2次,将结果8赋给变量a: int a = 32; a = a >> 2; // a now contains 8 当值中的某些位被“出”时,这些位的值将丢弃。例如,下面的程序片段将35右2 次,它的2个低位被出丢弃,也将结果8赋给变量a: int a = 35; a = a >> 2; // a still contains 8 用二进制表示该过程可以更清楚地看到程序的运行过程: 00100011 35 >> 2 00001000 8 将值每右一次,就相当于将该值除以2并且舍弃了余数。你可以利用这个特点将一个整数进行快速的2的除法。当然,你一定要确保你不会将该数原有的任何一位出。 右时,被走的最高位(最左边的位)由原来最高位的数字补充。例如,如果要走的值为负数,每一次右都在左边补1,如果要走的值为正数,每一次右都在左边补0,这叫做符号位扩展(保留符号位)(sign extension ),在进行右操作时用来保持负数的符号。例如,–8 >> 1 是–4,用二进制表示如下: 11111000 –8 >>1 11111100 –4 一个要注意的有趣问题是,由于符号位扩展(保留符号位)每次都会在高位补1,因此-1右的结果总是–1。有时你不希望在右时保留符号。例如,下面的例子将一个byte 型的值转换为用十六 进制表示。注意右后的值与0x0f进行按位与运算,这样可以舍弃任何的符号位扩展,以便得到的值可以作为定义数组的下标,从而得到对应数组元素代表的十六进制字符。 // Masking sign extension. class HexByte { static public void main(String args[]) { char hex[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'' }; byte b = (byte) 0xf1; System.out.println("b = 0x" + hex[(b >> 4) & 0x0f] + hex[b & 0x0f]);}} 该程序的输出如下: b = 0xf1 五 无符号右 正如上面刚刚看到的,每一次右,>>运算符总是自动地用它的先前最高位的内容补它的最高位。这样做保留了原值的符号。但有时这并不是我们想要的。例如,如果你进行移位操作的运算数不是数字值,你就不希望进行符号位扩展(保留符号位)。当你处理像素值或图形时,这种情况是相当普遍的。在这种情况下,不管运算数的初值是什么,你希望移位后总是在高位(最左边)补0。这就是人们所说的无符号动(unsigned shift )。这时你可以使用Java 的无符号右运算符>>> ,它总是在左边补0。 下面的程序段说明了无符号右运算符>>> 。在本例中,变量a被赋值为-1,用二进制表示就是32位全是1。这个值然后被无符号右24位,当然它忽略了符号位扩展,在它的左边总是补0。这样得到的值255被赋给变量a。 int a = -1; a = a >>> 24; 下面用二进制形式进一步说明该操作: 11111111 11111111 11111111 11111111 int型-1的二进制代码>>> 24 无符号右24位00000000 00000000 00000000 11111111 int型255的二进制代码 由于无符号右运算符>>> 只是对32位和64位的值有意义,所以它并不像你想象的那样有用。因为你要记住,在表达式中过小的值总是被自动扩大为int 型。这意味着符号位扩展和动总是发生在32位而不是8位或16位。这样,对第7位以0开始的byte 型的值进行无符号动是不可能的,因为在实际动运算时,是对扩大后的32位值进行操作。下面的例子说明了这一点: // Unsigned shifting a byte value. class ByteUShift { static public void main(String args[]) { int b = 2; int c = 3; a |= 4; b >>= 1; c <<= 1; a ^= c; System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("c = " + c); } } 该程序的输出如下所示: a = 3 b = 1 c = 6 还不清楚,移位运算有多大的用处,可是面试时经常会考。现在重温一下子。 原文地址:(http://www.ddvip.net/program/java/index1/28.htm)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

paopao_wu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值