Java基本类型与位移操作

发布时间 2023-11-01 15:18:19作者: 朽木不可雕

Java基本类型与位移操作

参考

基本数据类型

浮点数的阶码,尾数与移码

IEEE754数据格式介绍和解析方式

计算机基础进制转换(二进制、八进制、十进制、十六进制)

位运算符以及常见的使用场景

位运算理解与常用场景

java位运算符常用场景

循环移位:循环左移和循环右移

验证工具:

在线进制转换

单精度验证工具

基本数据类型

​ Java中的数据类型分为基本数据类型和引用类型两大类,引用类型我们在面向对象时再提,基本数据类型是重点中的重点!首先我们需要了解有哪些类型。

​ 然后,我们需要知道的,并不是他们的精度如何,能够表示的范围有多大,而是为什么Java会给我们定义这些类型,计算机是怎么表示这些类型的,这样我们才能够更好的记忆他们的精度、表示的范围大小。

​ 所以,我们从计算机原理的角度出发,带领大家走进Java的基本数据类型。

计算机中的二进制表示

​ 在计算机中,所有的内容都是二进制形式表示。十进制是以10为进位,如9+1=10;二进制则是满2进位(因为我们的计算机是电子的,电平信号只有高位和低位,你也可以暂且理解为通电和不通电,高电平代表1,低电平代表0,由于只有01,因此只能使用2进制表示我们的数字!)

​ 比如1+1=10=21+0,一个位也叫一个bit,8个bit称为1字节,16个bit称为一个字,32个bit称为一个双字,64个bit称为一个四字,我们一般采用字节来描述数据大小。

十进制的 7 -> 在二进制中为 111 = 22 + 21 + 20

现在有4个bit位,最大能够表示多大的数字呢?

  • 最小:0000 => 0
  • 最大:1111 => 23+22+21+20 => 8 + 4 + 2 + 1 = 15

原码

​ 在Java中,无论是小数还是整数,他们都要带有符号(和C语言不同,C语言有无符号数)所以,首位就作为我们的符号位,还是以4个bit为例,首位现在作为符号位(1代表负数,0代表正数):

  • 最小:(1)111 => -(22+21+20) => -7 => -(24-1-1) => -(23-1)
  • 最大:(0)111 => +(22+21+20) => +7 => 7 => 23-1
  • 由于符号位占一位 所以需要 4-1
  • 由于 0 占总数的一个数字位, 所以总值需要 -1

​ 现在,我们 4 bit 能够表示的范围变为了[-7 ~ +7] => [ -(23-1) ~ 23-1 ] ,这样的表示方式称为原码。

反码

虽然原码表示简单,但是原码在做加减法的时候,很麻烦! 以4 bit位为例:

1+(-1) = (0)001 + (1)001 = 怎么让计算机去计算?(虽然我们知道该去怎么算,但是计算机不知道!)

我们得创造一种更好的表示方式!于是我们引入了反码:

  • 正数的反码是其原码本身
  • 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反

例:

10进制数 7 -7 6 -6 5 -5
原码 (0)111 (1)111 (0)110 (1)110 (0)101 (1)101
反码 (0)111 (1)000 (0)110 (1)001 (0)101 (1)010

经过上面的定义,我们再来进行加减法:

7+(-7) 
= (0)111 + (1)000 
= (1)111 => -0 
// (直接相加,这样就简单多了!)

5+(-6) 
= (0)101 + (1)001 
=(1)110 => -1

思考:(1)111代表-0,(0)000代表+0,在我们实数的范围内,0有正负之分吗?

  • 0既不是正数也不是负数,那么显然这样的表示依然不够合理!

错误问题

7+(-6) 
= (0)111 + (1)001
= (1)0000 => 0

7+(-5) 
= (0)111 + (1)010 
= (1)0001 => 1 

补码

根据上面的问题,我们引入了最终的解决方案,那就是补码,定义如下:

  • 正数的补码就是其原码本身 (不变!)
  • 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

例:

十进制数 7 -7 6 -6 5 -5
原码 (0)111 (1)111 (0)110 (1)110 (0)101 (1)101
反码 (0)111 (1)000 (1)110 (1)001 (0)101 (1)010
补码 (0)111 (1)001 (0)110 (1)010 (0)101 (1)011

其实现在就已经能够想通了,-0其实已经被消除了!我们再来看上面的运算:

7+(-7) 
= (0)111 + (1)001
= (1)0000 => +0 
// (现在无论你怎么算,也不会有-0了!)

5+(-6) 
= (0)101 + (1)010 
=(1)111 => -1

7+(-6) 
= (0)111 + (1)010
= (1)0001 => 1

7+(-5) 
= (0)111 + (1)011
= (1)0010 => 2

所以现在,4 bit 位能够表示的范围是:[-8 ~ +7] --> [-24-1 ~ 24-1-1 ](Java使用的就是补码!)

**补码的负数的实际值的计算方法为: **

-(2b - 忽略符号位取其反码正数值[忽略符号位的补码 -1] - 1) , 其中 b 为 数据类型 bit 位大小

\[\begin{aligned} &补码 : 1010 \\&反码: 补码 -1 => 1001 \\&忽略反码符号位 取其正数值 => 1001 = 9 \\&-(2^4-9-1)= -6 \end{aligned} \]

二进制速记

十进制数 等价于 二进制数
1 20 1
2 21 10
4 22 100
8 23 1000
16 24 10000
32 25 100000
64 26 1000000
128 27 10000000
256 28 100000000
512 29 1000000000
1024 210 10000000000
2048 211 100000000000
4096 212 1000000000000
8192 213 10000000000000
16384 214 100000000000000
32768 215 1000000000000000
65536 216 10000000000000000
举例

十进制数: 345

\[\begin{aligned} &345=256+89=256+64+25=256+64+16+9 \\&=256+64+16+8+1 \\&=2^8+2^6+2^4+2^3+2^0 \\&=101011001 \\&=159(16进制) \end{aligned} \]

二进制数: 10010111010101

\[\begin{aligned} &10010111010101 \\&=2^{13}+2^{10}+2^8+2^7+2^6+2^4+2^2+2^0 \\&=8192+1024+256+128+64+16+4+1 \\&=9685 \end{aligned} \]

整数类型

​ 整数类型是最容易理解的类型!既然我们知道了计算机中的二进制数字是如何表示的,那么我们就可以很轻松的以二进制的形式来表达我们十进制的内容了。

在Java中,整数类型包括以下几个:

类型 内存 范围 范围
byte 8 bit (单字节) -28-1 ~ +28-1-1 -128 ~ +127
short 16 bit (双字节) -216-1 ~ +216-1-1 -32768 ~ +32767
int 32 bit (四字节) -232-1 ~ +232-1-1 -2147483648 ~ +2147483647
long 64 bit (八字节) -264-1 ~ +264-1-1 -9223372036854775808L ~ +9223372036854775807L

long 都装不下怎么办? BigInteger!

数字已经达到byte的最大值了,还能加吗?为了便于理解,以4bit为例:

7 + 1
=(0)111 + (0)001
=(1)000 => -8

7 + 2
=0111 + 0010
=1001 => -7

-8 + -1
=1000 + 1111
=(1)0111 => 7

-8 + -2
=1000 + 1110
=(1)0110 => 6

整数还能使用8进制、16进制表示:

\[15_{10} = 0XF_{16} = 017_8 = 0XF_{16} = 1111_2(代码里面不能使用二进制!) \]

字符类型和字符串

在Java中,存在字符类型,它能够代表一个字符:

  • char 字符型(16个bit,也就是2字节,它不带符号!)范围是 [0 ~ 216-1=(65535)]
  • 使用Unicode表示就是:\u0000 ~ \uffff

字符要用单引号扩起来!比如 char c = '淦';

​ 字符其实本质也是数字,但是这些数字通过编码表进行映射,代表了不同的字符,比如字符'A'的ASCII码就是数字65,所以,char类型其实可以转换为上面的整数类型。

​ Java的char采用Unicode编码表(不是ASCII编码!),Unicode编码表包含ASCII的所有内容,同时还包括了全世界的语言,ASCII只有1字节,而Unicode编码是2字节,能够代表 216= 65536 种文字,足以包含全世界的文字了!(我们编译出来的字节码文件也是使用Unicode编码的,所以利用这种特性,其实Java支持中文变量名称、方法名称甚至是类名)

​ 既然char只能代表一个字符,那怎么才能包含一句话呢?String就是Java中的字符串类型(注意,它是一个类,创建出来的字符串本质是一个对象,不是我们的基本类型)字符串就像它的名字一样,代表一串字符,也就是一句完整的话。

字符串用双引号括起来!比如:String str = “一日三餐没烦恼”;

小数类型

小数类型比较难理解(比较难理解指的是原理,不是使用)首先来看看Java中的小数类型包含哪些:

  • float 单精度浮点型 (32bit,4字节)
  • double 双精度浮点型(64bit,8字节)

思考:小数的范围该怎么定义呢?我们首先要了解的是小数在计算机里面是如何存放的:

浮点数二进制存储结构

根据国际标准 IEEE 754,任意一个二进制浮点数 V 可以表示成下面的形式:

\[V = (-1)^S \times M \times 2^E \]

  1. (-1)S 表示符号位,当 S=0,V 为正数;当 S=1,V 为负数。
  2. M 表示有效数字,大于等于 1,小于 2,但整数部分的 1 不变,因此可以省略。
    1. 例如尾数为1111010,那么M实际上就是1.111010,尾数首位必须是1,1后面紧跟小数点,
      如果出现0001111这样的情况,去掉前面的0,移动1到首位;
    2. 题外话:随着时间的发展,IEEE 754 标准默认第一位为1,故为了能够存放更多数据,就舍去了第一位,
      比如保存1.0101 的时候, 只保存 0101,这样能够多存储一位数据
  3. 2E 表示指数位。(用于移动小数点)

浮点数 IEEE 754 标准计算方法

浮点数转换为二进制的方法为, 整数部分 取其补码, 小数部分 乘二取整

小数转换位二进制数

转换至 32 bit 单精度浮点型

float num=12.356
// 计算整数部分 二进制
12 => 100+10 = 1100
// 计算小数部分二进制 [乘二取整]
0.356 => 01011011001000101101
=0.356* 2 => (取整) 0
=0.712* 2 => 1
=0.424* 2 => 0
=0.848* 2 => 1
=0.696* 2 => 1
=0.392* 2 => 0
=0.784* 2 => 1
=0.568* 2 => 1
=0.136* 2 => 0
=0.272* 2 => 0
=0.544* 2 => 1
=0.088* 2 => 0
=0.176* 2 => 0
=0.352* 2 => 0
=0.704* 2 => 1
=0.408* 2 => 0
=0.816* 2 => 1
=0.632* 2 => 1
=0.264* 2 => 0
=0.528* 2 => 1
// 由于精度为 32bit 浮点精度, 尾数只保留 23位, 后续精度则忽略舍弃
=0.056* 2 => 0
=0.112* 2 => 0
=0.224* 2 => 0
=0.448* 2 => 0
=0.896* 2 => 1
=0.792* 2 => 1
=0.584* 2 => 1
=0.168* 2 => 0
=0.336* 2 => 0
=0.672* 2 => 1
=0.344* 2 => 0
=0.688* 2 => 1
=0.376* 2 => 0
=0.752* 2 => 1
=0.504* 2 => 1
=0.008* 2 => 0
=0.016* 2 => 0
=0.032* 2 => 0
=0.064* 2 => 0
=0.128* 2 => 0
=0.256* 2 => 0
=0.512* 2 => 1
=0.024* 2 => 0
=0.048* 2 => 0
=0.096* 2 => 0
=0.192* 2 => 0
=0.384* 2 => 0
=0.768* 2 => 1
=0.536* 2 => 1
// // 由于精度为 64 bit 浮点精度, 尾数只保留 52位, 后续精度则忽略舍弃
=0.072* 2 => 0
=0.144* 2 => 0
=0.288* 2 => 0

S 值 (符号位: 1位)
符号位 正数: 0 , 负数: 1
S = 0 
计算 M值(有效数字,尾数: 23位)

有效数字位 23位, IEEE 754 标准 忽略小数点前的 1,所以小数点后保留 23位

\[M=1100.01011011001000101101 \]

左移小数点

\[M = 1.10001011011001000101101\times2^3 \]

计算 32位精度的 E值 (阶码:8位)
  • 32位精度 指数位 8位, 64位精度 指数位 11位
  • 32位精度的 指数偏移真值为: 27-1 = 127
  • 64位精度的 指数偏移真值为: 210-1= 1023

\[\begin{aligned} \\&左移小数点 \\&M = 1.10001011011001000101101 \times2^3 \\&E= 127(指数偏移真值)+3 = 128 + 2 = 10000010 \end{aligned} \]

组装IEEE 754 格式浮点数

\[V = (-1)^S \times M \times 2^E \]

12.356 的二进制表示
S=0
E=10000010
M=1.10001011011001000101101
0 10000010 10001011011001000101101

+12.356 = 01000001010001011011001000101101
-12.356 = 11000001010001011011001000101101

布尔类型

布尔类型(boolean)只有truefalse两种值,也就是要么为真,要么为假,布尔类型的变量通常用作流程控制判断语句。

(C语言一般使用0表示false,除0以外的所有数都表示true)布尔类型占据的空间大小并未明确定义,而是根据不同的JVM会有不同的实现。

进制换算

二进制 转换成 R 进制

二进制转成十进制

二进制 转成 正整数
方法 : 把二进制数按权展开、相加即得十进制数。

将 二进制数 的每一位 数字, 根据 自身所在的索引位 (从右到左) , 乘以 2的 (索引位-1) 次方, 然后将各个和相加

举例 : 将 二进制数 : 1010 转换为十进制

\[\begin{aligned} &1 \times 2^{4-1}+0\times2^{3-1}+1\times2^{2-1}+0\times2^{1-1}\\ &= 1 \times 2^3+0\times2^2+1\times2^1+0\times2^0\\ &= 8+0+2+0\\ &= 10 \end{aligned} \]

即 二进制数 1010 转换成十进制 为 10

二进制转成 负整数
方法 : 负数二进制, 将补码逆转为正数补码, 然后通过 二进制转成正数 得到 结果的对数
  1. 负数二进制数, 需要先将补码逆转为正数二进制,
    1. 先减一, 取反码
  2. 然后通过 "二进制 转成 正整数" 方式转换得到的数,
  3. 用零减去 得到的差 (就是加上负号),即是转换的负数结果

举例 : 将 二进制数 11111111 11111111 11111111 11110110 转换成十进制

通常来说, 不等于 8/16/32(常用)/64 的二进制码,都为正数, 如果刚好等于其中一个数, 最高位 为 1 则为负数, 为 0 为正数

  1. 将 11111111 11111111 11111111 11110110 补码进行逆转 , 减1, 然后取反
    1. 减1 = 11111111 11111111 11111111 11110101
    2. 取反= 00000000 00000000 00000000 00001010
  2. 然后,通过 将二进制数按权展开 , 相加即得到十进制数 = 10,
  3. 然后用 0 减去结果 , 0 - 10 (加上符号) 得到 负数结果 -10

二进制转换成 八进制

方法 : " 取三合一 "

即从二进制的小数点为分界点,向左(或向右)每三位取成一位。

举例 将 二进制数 1010 0100(164) 转成 八进制

  1. 将 二进制数, 从右到左, 三三分组
    1. 10 100 100
  2. 然后, 分别将各组转成 十进制--> 八进制 ,
    1. 二进制转 十进制 : 2 4 4
    2. 转换成八进制 : 2 4 4

最终, 的八进制结果为 244

二进制 转成十六进制

方法 : " 取四合一 "

即从二进制的小数点为分界点,向左(或向右)每四位取成一位。

举例1 : 1010 0100(164) 转成 十六进制

  1. 将二进制数, 从右到左 四四分组
    1. 1010 0100
  2. 然后分别转成 十进制 --> 十六进制 ,
    1. 二进制转 十进制 : 10 4
    2. 转成 十六进制 : a 4

即 最终的十六进制结果为 a4

二进制 快速转换

下面的表格是8位二进制所对应的十进制数值,对进制转换以及类似题目的理解非常有用:

二进制十进制对照表 0 0 0 0 0 0 0
对应二进制码 1 1 1 1 1 1 1 1
对应十进制码 27 26 25 24 23 22 21 20
128 64 32 16 8 4 2 1

二进制速记

举例 : 十进制数 135 , 转换成 二进制数

  1. 可以将 135 拆分成 128 + 7 = 128 + 4 + 2 + 1
  2. 将 各部分 根据对照表 ,找到响应的 二进制码
    1. 10000000 (128) + 100 (4) + 10 (2) + 1(1) = 10000111 = 135

十进制转换 R进制

除R倒取余

十进制转换成二进制

正数十进制转成 二进制
方法 : "除二倒取余"

对十进制整数 除 2 , 或得 商和余数, 商为对应的二进制码

  1. 商部分即是相应的二进制数码
  2. 再用 余数 除 2, 又得到 新的商和余数
  3. 如此不断重复,知道余数为0 为止

最后一次得到的为最高位, 第一次得到的为最低位

举例1 : 计算 10 的二进制

除二计算 取余 <对应的二进制码>
10 ÷ 2 5 0
5 ÷ 2 2 1
2 ÷ 2 1 0
1 ÷ 2 0 1
1010

即 10 的二进制 为 1010 (第一次所得为最低位, 最后一次得到为最高位)

代码
public String toBinaryString(int num) {
    StringBuffer str = new StringBuffer();
    while (num > 0) {
        str.insert(0, num % 2);
        num = num / 2;
    }
    return str.toString();
}
负数十进制转换成二进制
负数转成二进制的方法 : "先计算出 对数的 二进制码,补码; 取反 ; 加一"

举例 1: -10

  1. 对数 二进制码, 即 10 的二进制码 : 1010
  2. 获取补码 : 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001010
  3. 取反 加一 获得 负数补码 : 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11110110

即 -10 的二进制补码为 : 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11110110

小数转换成 二进制
方法 : "乘二取整"

对十进制小数乘2得到的整数部分和小数部分

  1. 整数部分既是相应的二进制数码
  2. 再用2乘小数部分(之前乘后得到新的小数部分),又得到整数和小数部分
  3. 如此不断重复,直到小数部分为0或达到精度要求为止.

第一次所得到为最高位,最后一次得到为最低位

**举例 : 0.25 转换为 二进制数 **

乘 2 操作 取整
0.25 × 2 0.5 0
0.5 × 2 1 1
0.01

即 0.25 的 二进制小数为 0.01

伪代码
public String toBinaryString(String str,double num){
    num = num >= 1 ? num - 1 : num;
    num = num * 2;
    str = str.trim().length() <= 0 ? "0." : str;
    if (num == 0) {
        return str;
    }
    return toBinaryString(str + num >= 1? "1":"0", num);
}

十进制转成 八进制

方法 : "除八倒取余"

与十进制转 二进制类似 此处省略....

十进制转成 十六进制

方法 : "除十六倒取余"

与十进制转 二进制类似 此处省略....

伪代码
/**
 * 十进制正整数进制转换
 * num 待转换的十进制数
 * jz 进制数(2,8,16,...),默认为 2进制
 * str 拼接的结果字符串, 使用时忽略
 */
function binHexOct(num,jz=2,str=""){
    //生成 [A-Z] 数组
    let array=new Array(26).fill(65).map((item,i)=>String.fromCharCode(item+i))
    // 取余
    let surplus=num%jz;
    // 如果余数大于 9 , 则拼接 A-F 字母
    if(surplus>9){
        surplus=array[surplus-10]
    }
    str=surplus+str;
    num=parseInt(num/jz);
    return num===0?str:binHexOct(num,jz,str);
}

R 进制转成 十进制

与 二进制 转十进制类似 : 按权展开、相加即得十进制数。

\[V(10进制)=N(x进制)=N_0\times x^0+N_1\times x^1+N_2\times x^2+N_3\times x^3+... \]

示例八进制 转十进制

245(8)=165(10)

\[\begin{aligned} &2\times8^2+4\times8^1+5\times8^0\\ &=2\times64+4\times8+5\times1\\ &=128+32+5\\ &=165 \end{aligned} \]

位运算

​ 所谓位运算,就是对一个比特(Bit)位进行操作。比特(Bit)是一个电子元器件,8个比特构成一个字节(Byte),它已经是粒度最小的可操作单元了。 一个比特(Bit)位只有 0 和 1 两个取值。

需要注意的是, 所有位运算操作结果,都会忽略[舍弃]其结果的小数部分

七种位运算符

位运算符 描述 运算规则
<< 左移 各二进位全部左移若干位, 高位丢弃, 低位补 0
>> 右移 各二进位全部右移若干位, 低位丢弃, 正数高位补 0 , 负数高位补 1
>>> 无符号右移 各二进位全部右移若干位,低位丢弃,高位补 0
& 位与 两个位都为1时, 结果才为1 , 否则为 0
` ` 位或
^ 位异或 两个位相同时为 0 , 相异为 1
~ 位非 0 变 1 , 1 变 0, 相当于取反码, 由于java使用的补码, 所以取的反码不纯粹

其中位非(~)是一元运算符,其他六个都是二元运算符。

位与(&)

运算规则:当运算符两边相同位置都是1时,结果返回1,其他情况都返回0。

​ 参与 &(位与)运算的两个位都为 1 时,结果才为 1,否则为 0。例如1&1为 1,0&0为 0,1&0也为 0,这和逻辑运算符&&非常类似。

常用场景:

​ 按位与运算通常用来对某些位清 0,或者保留某些位。

​ 例如要把 n 的高 16 位清 0 ,保留低 16 位,可以进行n & 0XFFFF运算(0XFFFF 在内存中的存储形式为 0000 0000 – 0000 0000 – 1111 1111 – 1111 1111)。

  • 清零(将一个单元与0进行位与运算结果为零)

  • 取一个数指定位为0(例如置X=1010 1101的高四位置0, 则将X & 0xF得到0000 1101)。

  • 判断奇偶性:用if ((a & 1) == 0) 代替 if (a % 2 == 0)来判断a是不是偶数。

    • 利用 & 运算符的特性,来判断二进制数第一位是0还是1。
  • 2n 的数值进行取余计算: num & (2n -1) = num % 2n

    • 其实取余算法和上面的判断奇偶数原理是一样的。

    • 例:让a对16进行取余,那么就可以让 a & 15 得出来的结果就是余数。

    • 15
      0000 0000 0000 0000 0000 0000 0000 1111
      所以 a & 15 返回值就是a二进制的最低四位,也就是 a & 15 = a % 16。
      
    • 使用 & 来进行取余的算法比使用 % 效率高很多,虽然只能对 2n 的数值进行取余计算,
      但是在JDK源码中也是经常被使用到,比如说HashMap中判断key在Hash桶中的位置。

  • 生成第一个 >=a 的满足 2n 的数

    • public static final int tableSizeFor(int cap) {
          int MAXIMUM_CAPACITY=Integer.MAX_VALUE;
          int n = cap - 1;
          n |= n >>> 1;
          n |= n >>> 2;
          n |= n >>> 4;
          n |= n >>> 8;
          n |= n >>> 16;
          return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
      }
      // tableSizeFor(10) ==> 16 => 2的3次方;
      // tableSizeFor(16) ==> 16 => 2的3次方;
      // tableSizeFor(17) ==> 32 => 2的4次方;
      // tableSizeFor(18) ==> 32 => 2的4次方;
      // tableSizeFor(25) ==> 32 => 2的4次方;
      // tableSizeFor(32) ==> 32 => 2的4次方;
      // ...
      

例如:\(3 \& 5 = 1\)

0000 0000 0000 0000 0000 0000 0000 0011     -> 3
0000 0000 0000 0000 0000 0000 0000 0101     -> 5
0000 0000 0000 0000 0000 0000 0000 0001     -> 3 & 5 = 1
  • 其中3和5的只有第一位共同为1,所以3 & 5 = 1

位或(|)

运算规则 :当运算符两边相同位置都是0时,结果返回0,其他情况都返回1。

​ 参与 |运算 的两个二进制位有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0。例如 1|1 为1, 0|0 为0, 1|0 为1,这和逻辑运算中的 || 非常类似。

常用场景:

​ 按位或运算可以用来将某些位置 1,或者保留某些位。

​ 例如要把 n 的高 16 位置 1,保留低 16 位,可以进行n | 0XFFFF0000 运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。

  • 取一个数指定位 位1(例如将X=1010 1010的 第四位置 1,则将 X |0xF 得到1010 1111)。

例如:3 | 5 = 7

0000 0000 0000 0000 0000 0000 0000 0011     -> 3
0000 0000 0000 0000 0000 0000 0000 0101     -> 5
0000 0000 0000 0000 0000 0000 0000 0111     -> 3 | 5 = 7
  • 其中3和5的第一到第三位都有不为0的,所以 3 | 5 = 7

位异或(^)

运算规则:当运算符两边相同位置都是相同,结果返回0,不相同时返回1。

​ 参与^运算两个二进制位不同时,结果为 1,相同时结果为 0。例如0^1为1,0^0为0,1^1为0。

常用场景:

​ 按位异或运算可以用来将某些二进制位反转。

​ 例如要把 n 的高 16 位反转,保留 低 16 位,可以进行n ^ 0XFFFF0000 运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。

  • 使特定位翻转找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或即可。

    • 例:X=10101110,使X低4位翻转,用X ^0000 1111 = 1010 0001即可得到。
  • 与0异或得到原值

    • 例:X=10101110,用X^0=10101110
  • 使用 ^ 位运算符交换两个数:

    • // 临时变量
      int t = a;
      a = b;
      b = t;
      
      // 使用 ^ 位运算符
      a ^= b;
      b ^= a;
      a ^= b;
      
      

例如:3 ^ 5 = 1

0000 0000 0000 0000 0000 0000 0000 0011     -> 3
0000 0000 0000 0000 0000 0000 0000 0101     -> 5
0000 0000 0000 0000 0000 0000 0000 0110     -> 3 ^ 5 = 6
  • 其中3和5的第一和第三位不相同,所以 3 ^ 5 = 6

位非(~)

运算规则:将运算符后二进制数反转,0变1,1变 。一元操作符

​ 取反运算符~为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如~1为0,~0为1,这和逻辑运算中的!非常类似。。

常用场景

  • 求相反数: ~a + 1

例如:~ 3 = -4

0000 0000 0000 0000 0000 0000 0000 0011     -> 3
1111 1111 1111 1111 1111 1111 1111 1100     -> ~ 3 = -4
  • 将3的所有二进制位全部反转,所以~ 3 = -4。

位移运算

带符号位移

<< : 左移

运算符规则:各二进位全部左移若干位,高位丢弃,低位补0。

​ 左移运算符<<用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补0。

常用场景: 乘以 2 的 n 次方
  • 左移常被用来做 x*2n 的运算,因为直接基于二进制运算,所以左移效率比 x*2n高。

例如:6 << 2 = 24

0000 0000 0000 0000 0000 0000 0000 0110     -> 6
0000 0000 0000 0000 0000 0000 0001 1100     -> 6 << 2 = 24
  • 我们将6的二进位向左移动两位,低位补上两个0,高位丢弃,得出来的结果就是24。
>> : 右移

运算符规则:各二进位全部右移若干位,正数高位补0,负数高位补1,低位丢弃。

​ 右移运算符>>用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1。

常用场景: 除以 2 的 n 次方
  • 右移常被用来做 x/2n 的运算,因为直接基于二进制运算,所以右移效率比 x/2n 高。
    • 需要注意的是,运算结果舍弃了其小数部分`
  • 求绝对值: a >> 31 == 0 ? a : (~a + 1) ( Integer : 32-1=31 , Long : 64-1 = 63;
    • a >> 31 : 判断 a 是否为 正数
    • ~a+1 : 相反数

例如: 12 >> 2 = 3

0000 0000 0000 0000 0000 0000 0000 1100     -> 12
0000 0000 0000 0000 0000 0000 0000 0011     -> 12 >> 2 = 3
  • 因为12是正数,右移过程中高位补上两个0,低位丢弃,得出来的结果就是3。
被舍弃的小数部分

例:10/4 =2.5 = 10 / 22 => ≠ 10 >> 2 = 2

0000 0000 0000 0000 0000 0000 0000 1010		-> 10
0000 0000 0000 0000 0000 0000 0000 0010		-> 2

被舍弃的小数部分

无符号位移

>>> : 无符号 右移

运算符规则是:各二进位全部右移若干位,高位补0,低位丢弃。

例如: 12 >>> 2 = 3

0000 0000 0000 0000 0000 0000 0000 1100     -> 12
0000 0000 0000 0000 0000 0000 0000 0011     -> 12 >>> 2 = 3
  • 我们将12的二进位向右移动两位,高位补上两个0,低位丢弃,得出来的结果就是24。

例如:-12 >>> 2 = 1073741821

1111 1111 1111 1111 1111 1111 1111 0100    -> -12
0011 1111 1111 1111 1111 1111 1111 1101    -> -12 >> 2 = 1073741821
  • 我们将-12的二进位向右移动两位,高位补上两个0,低位丢弃,得出来的结果就是1073741821。

总结使用场景

计算 x * 2n

\[x\times2^n=x<<n \]

\[3 \times8 = 3\times2^3 = 3<<3 = 24 \]

判断一个数n的奇偶性

a&1 = 0 // 偶数
a&1 = 1 // 奇数
n&1==1?“奇数”:“偶数”

为什么与1能判断奇偶?

​ 所谓的二进制就是满2进1,那么好了,偶数的最低位肯定是0(恰好满2,对不对?),同理,奇数的最低位肯定是1.

​ int类型的1,前31位都是0,无论是 1&0 还是 0&0 结果都是0,那么有区别的就是1的最低位上的1了,若n的二进制最低位是1(奇数)与上1,结果为1,反则结果为0.

不用临时变量交换两个数

   a = a^b;
   b = b^a; // = b^b^a = a
   a = a^b; // = b^a^a = b

取绝对值

   (a ^(a>>31))-(a>>31)

使用位运算取绝对值的思路:

若a为正数,则不变,需要用异或0保持的特点;
若a为负数,则其补码为源码翻转每一位后+1,先求其源码,补码-1后再翻转每一位,此时需要使用 异或1(^1) 具有翻转的特点。

任何正数右移31后只剩符号位0,最终结果为0,任何负数右移31后也只剩符号位1,溢出的31位截断,空出的31位补符号位1,最终结果为-1. 右移31操作可以取得任何整数的符号位。
那么综合上面的步骤,可得到公式。

a>>31取得a的符号,
若a为正数,a>>31等于0,a ^ 0=a,不变;
若a为负数,a>>31等于-1 ,a-1(a0xFFFFFFFF)翻转每一位.

取数字 变量a的第k位

k 从 0 开始

\[a>>k\&1; \]

10 = 00001010
// 获取 右起 第 5 索引位的 数字
10>>5&1 = 0
// 获取 右起 第 1 索引位的 数字
10>>1&1 = 1

将数字变量a的第k位清0

\[a\&\sim(1<< k); \]

100     =  01100100;
// 将索引位 第2位 改为 0
a&~(1<<2)= 01100000;
= 96

将数字变量a的第k位置 改为 1

\[a\quad|\quad(1<< k); \]

100     =  01100100;
// 将索引位 第3位 改为 1
a |(1<<3)= 01101100;
= 108

循环位移

参考: https://blog.csdn.net/DuanLiuchang/article/details/103001518

​ 循环移位就是把数值变成二进制,然后循环移动的过程;无符号操作

​ 换句话说,循环移位就是将移出的低位放到该数的高位(循环右移)或把移出的高位放到该数的低位(循环左移),左移,和右移动都是对整数进行的操作

数字变量循环左移k次

即a=a << k|a>>32-k (设sizeof(int)=32)

\[a << k\quad|\quad a>>32-k; \]

a=100,k=3;
a=00000000000000000000000001100100;
a=num << k | num >> 32 - k;
= 00000000000000000000001100100000
=800
k=4;
= 00000000000000000000011001000000
=1600
数字变量a循环右移k次

即a=a>> k |a <<32-k (设sizeof(int)=32)

\[a=a>> k\quad|\quad a <<32-k; \]

a=100,k=3;
a=00000000000000000000000001100100;
a=a>> k | a <<32-k;
= 10000000000000000000000000001100
=-2147483636
k=4;
= 01000000000000000000000000000110
=1073741830

整数的平均值

​ 对于两个整数x,y,假设用 (x+y)/2 求平均值。会产生溢出。由于 x+y 可能会大于INT_MAX,可是我们知道它们的平均值是肯定不会溢出的。

我们用例如以下算法:

//返回X,Y 的平均值
int average(int x, int y) {
    return (x&y)+((x^y)>>1);
}

推断一个整数是不是2的幂

对于一个数 x >= 0,推断他是不是2的幂

boolean power2(int x){
    return ((x&(x-1))==0)&&(x!=0)。
}

取模运算转化成位运算 (在不产生溢出的情况下)

\[a \% 2^n = a \& (2^n - 1) = a\&((1<<n) -1) \]

\[\begin{aligned} &\ 18\%2^3 \\&= 18 \& (8-1) \\&= 18 \& 7 \\&= 2 \end{aligned} \]

乘法运算转化成位运算 (在不产生溢出的情况下)

\[a\times2^n=a<<n \]

\[12\times8 == 12<<3; \]

除法运算转化成位运算 (在不产生溢出的情况下)

\[a\div2^n=a>>n \]

\[12\div(/)8 == 12>>3; \]

交换限定值

​ 假设现在参数X的取值只可能a,b两个数,现在的实现逻辑是,如果x==a时,则把b的值赋给X;如果 x!=a时,则把a的值赋给X。

if (x == a){ 
    x= b;
}else{
    x= a;
}

x= a ^ b ^ x; 

这种替换有限制的:x只可能等于a,b两个数之间选择,如果x有第三种取值情况,上述等价替换不成立。

x 的 相反数

\[(\sim x+1) \]

10 = 00001010
~10+1 
= 11110101 +1
= 11110110
= -10