Java二进制运算符超详细讲解及扩展知识
作者:弥鸿
前言
看Java数据结构源码的时候发现很多源码中都用到了<<
和>>
运算符,果然是我太low了,用运算符都跟不上大佬的脚步;所以打算从头再看一次这些Java中的运算符;
本来像写的简单点的,但是一开始梳理就收不住了… 然后就又扯出来了一大堆知识点…
经过我个人的学习过程,梳理了一下相关的知识:
- 计算机存储单元
- 原码、反码、补码、掩码
- 基本数据类型
- 整数的存储
计算机存储单元
首先区分两个概念:
- 最小信息单元:比特(bit,二进制位)是计算机中表示信息的最小单位,对应二进制中的一个 “0” 或 “1”。
- 最小存储单元:字节(byte)是计算机硬件中用于存储数据的最小可寻址(可独立操作)单元,通常由 8 个比特组成(1 byte = 8 bit)
1B = 8bit;
1KB = 1024B;
1MB = 1024KB;
1GB = 1024MB;
1TB = 1024GB
区分这个概念的目的是:为什么二进制数的表示长度只能是8的倍数;(反正我刚学那会是不懂)
原码、补码、反码、掩码
一、原码(True Form)
定义:最直观的二进制表示法,由“符号位+数值位”组成,直接对应十进制数的正负和绝对值。
- 规则:
- 最高位为符号位:
0
表示正数,1
表示负数; - 其余位为数值位:直接表示该数的绝对值的二进制。
- 最高位为符号位:
- 示例(以8位二进制为例):
- 十进制
+5
的原码:00000101
(符号位0,数值位5
的二进制0000101
); - 十进制
-5
的原码:10000101
(符号位1,数值位同样是5
的二进制); - 十进制
0
的原码有两种:00000000
(+0)和10000000
(-0)。
- 十进制
- 特点与问题:
- 优点:直观易懂,与十进制的对应关系明确;
- 缺点:
- 0的表示不唯一(+0和-0),浪费存储空间;
- 加减法运算复杂(正数加负数需先比较绝对值,再做减法,符号单独处理),不适合计算机硬件直接实现。
二、反码(One’s Complement)
定义:原码的“变形”,是从原码过渡到补码的中间形式,主要用于简化负数的运算。
- 规则:
- 正数的反码 = 原码(与原码完全相同);
- 负数的反码 = 原码的“符号位不变,数值位按位取反”(0变1,1变0)。
- 示例(8位二进制):
- 十进制
+5
的原码:00000101
→ 反码:00000101
(不变); - 十进制
-5
的原码:10000101
→ 反码:11111010
(符号位1不变,数值位0000101
取反为1111010
); - 十进制
0
的反码:00000000
(+0)和11111111
(-0)(仍不唯一)。
- 十进制
- 特点与问题:
- 优点:一定程度上简化了减法运算(减法可转化为“加负数的反码”);
- 缺点:
- 0的表示仍不唯一;
- 运算后可能需要“循环进位”(最高位的进位需加到最低位),硬件实现仍复杂,实际中很少直接使用。
三、补码(Two’s Complement)
定义:计算机中实际使用的有符号整数表示法,解决了原码和反码的运算缺陷,能将减法统一为加法。
- 规则:
- 正数的补码 = 原码(与原码、反码相同);
- 负数的补码 = 反码 + 1(即“原码符号位不变,数值位取反后加1”)。
- 示例(8位二进制):
- 十进制
+5
的补码:00000101
(与原码、反码一致); - 十进制
-5
的补码:
原码10000101
→ 反码11111010
→ 反码+1 =11111011
(补码); - 十进制
0
的补码:00000000
(唯一表示,无+0和-0之分)。
- 十进制
- 核心优势:
- 0的表示唯一,节省存储空间;
- 减法可转化为加法:
a - b = a + (-b的补码)
,硬件只需实现加法器即可,无需单独的减法电路; - 运算时符号位自动参与运算,无需单独处理符号。
- 验证:用补码计算
5 - 3
(即5 + (-3)
):5
的补码:00000101
;-3
的补码:原码10000011
→ 反码11111100
→ 补码11111101
;- 相加:
00000101 + 11111101 = 100000010
(8位截断后为00000010
,即十进制2,正确)。
四、掩码(Mask)
定义:一种特定的二进制序列(通常是整数),通过与位运算符(&
、|
、^
)配合,实现对目标二进制数“特定位的提取、设置或清除”。
- 核心作用:精准操作二进制中的某几位,而不影响其他位。
- 常见用法:
- 提取特定位(用
&
运算):
掩码中需保留的位设为1
,其他位设为0
,与目标数&
后,仅保留目标数中对应掩码1
的位。
例:提取8位二进制10110101
的低4位:
目标数:10110101
掩码:00001111
(低4位为1)
结果:10110101 & 00001111 = 00000101
(仅保留低4位)。 - 设置特定位为1(用
|
运算):
掩码中需设置为1的位设为1
,其他位设为0
,与目标数|
后,目标数中对应掩码1
的位会被强制设为1。
例:将8位二进制10110101
的第5位(从0开始计数)设为1:
目标数:10110101
(第5位当前为0)
掩码:00100000
(第5位为1)
结果:10110101 | 00100000 = 10110101
(第5位变为1)。 - 清除特定位(设为0)(用
& ~掩码
运算):
掩码中需清除的位设为1
,其他位设为0
,先对掩码取反(~
),再与目标数&
,目标数中对应原掩码1
的位会被设为0。
例:清除8位二进制10110101
的高4位:
目标数:10110101
掩码:11110000
(高4位为1)
取反掩码:00001111
结果:10110101 & 00001111 = 00000101
(高4位被清除)。
- 提取特定位(用
- 典型应用:权限控制(用位表示不同权限)、数据打包/解包(如协议报文解析)、硬件寄存器操作等。
Java的基本数据类型
不同基本数据类型的区别在于二进制的“组织方式”和“长度”(即占用的比特数),但本质都是二进制。以下是具体说明:
一、 整数类型(byte/short/int/long)
整数类型直接以二进制补码形式存储(补码是计算机中表示有符号整数的标准方式,可统一加减法运算):
byte
(8位):如十进制66
存储为01000010
(符号位0表示正数,数值位为66的二进制)。int
(32位):如十进制1
存储为00000000 00000000 00000000 00000001
(32位补码,高位补0)。long
(64位):在int
的基础上扩展到64位,高位补0或符号位。
二、 浮点类型(float/double)
浮点类型遵循IEEE 754标准,以二进制形式存储,但结构更复杂(分为符号位、指数位、尾数位):
float
(32位):1位符号位 + 8位指数位 + 23位尾数位。double
(64位):1位符号位 + 11位指数位 + 52位尾数位。
例如,十进制3.14f
(float类型)会被转换为对应的32位二进制浮点格式存储。
三、 字符类型(char)
char
类型存储Unicode字符编码(本质是无符号整数),以16位二进制形式存储:
- 例如字符
'A'
的Unicode编码是65,对应二进制00000000 01000001
(16位)。 - 中文字符
'中'
的Unicode编码是20013,对应二进制01001110 00101101
。
四、 布尔类型(boolean)
boolean
类型比较特殊,Java规范未明确其占用的比特数(不同JVM实现可能不同),但本质仍是二进制表示:
- 通常用1位二进制表示(
0
对应false
,1
对应true
),但为了内存对齐,实际可能占用1字节(8位),仅用最低位表示真假。
Java中的整数存储
一、Java中整数的存储:补码是唯一标准
Java中所有有符号整数(byte
/short
/int
/long
)统一使用补码存储,不存在原码或反码的直接使用,这是Java为简化运算和硬件适配做的设计:
int
类型(32位):最高位为符号位,范围-2³¹ ~ 2³¹-1
,其中-2³¹
(-2147483648
)是特殊值,补码为10000000 00000000 00000000 00000000
(无对应的原码/反码)。long
类型(64位):同理,范围-2⁶³ ~ 2⁶³-1
,补码存储。
示例:
十进制-5
的int
补码计算:
原码(符号位1+数值位5)→ 10000000 00000000 00000000 00000101
反码(符号位不变,数值位取反)→ 11111111 11111111 11111111 11111010
补码(反码+1)→ 11111111 11111111 11111111 11111011
(Java中实际存储的形式)
二、Java中的位运算符:直接操作二进制补码
Java支持5种位运算符(针对整数类型),操作的是数值的补码二进制位:
运算符 | 名称 | 规则(逐位操作) | 示例(int 类型) |
---|---|---|---|
& | 按位与 | 全1为1,否则为0 | 3 & 5 → 00000011 & 00000101 = 00000001 → 1 |
` | ` | 按位或 | 按位或 |
^ | 按位异或 | 不同为1,相同为0 | 3 ^ 5 → 00000011 ^ 00000101 = 00000110 → 6 |
~ | 按位取反 | 0变1,1变0(补码取反) | ~3 → ~00000011 = 11111100(补码)→ -4 |
<< | 左移 | 左移n位,右补0 | 3 << 2 → 00000011 << 2 = 00001100 → 12 |
>> | 算术右移 | 右移n位,左补符号位(正数补0,负数补1) | -8 >> 1 → 11111000 >> 1 = 11111100 → -4 |
>>> | 无符号右移 | 右移n位,左补0(忽略符号位) | -1 >>> 1 → 11111111... >>> 1 = 01111111... → 2147483647 |
关键特性:
- 类型提升:对
byte
/short
进行位运算时,会先自动提升为int
(32位),结果也是int
。
例:byte b = 3; byte c = (byte)(b << 1);
(需强制转型,否则结果为int
)。 - 移位位数取模:移位位数若超过类型位数(如
int
移32位),会对位数取模(int
移33位 = 移1位,33 % 32 = 1
)。
三、掩码在Java中的典型应用
掩码(mask
)结合位运算,在Java中常用于权限控制、数据解析、状态标记等场景:
1. 权限控制(用二进制位表示不同权限)
// 定义权限掩码(每个权限对应1位) public class Permission { public static final int READ = 1 << 0; // 0001(1):读权限 public static final int WRITE = 1 << 1; // 0010(2):写权限 public static final int EXEC = 1 << 2; // 0100(4):执行权限 public static void main(String[] args) { int permissions = READ | WRITE; // 组合权限:0011(3) // 检查是否有写权限(用&) boolean canWrite = (permissions & WRITE) != 0; // true // 添加执行权限(用|) permissions |= EXEC; // 0111(7) // 移除读权限(用& ~) permissions &= ~READ; // 0110(6) } }
2. 解析二进制协议数据(提取特定字段)
假设某协议用16位二进制表示“命令(高8位)+ 参数(低8位)”:
short data = 0x1234; // 二进制:00010010 00110100(高8位0x12,低8位0x34) // 提取高8位(命令):右移8位,并用0xFF掩码保留低8位 int command = (data >> 8) & 0xFF; // 0x12(18) // 提取低8位(参数):用0xFF掩码保留低8位 int param = data & 0xFF; // 0x34(52)
四、注意事项
- 溢出问题:位运算可能导致溢出(如
int
最大值左移1位变为负数),但Java不抛异常,直接按补码截断。 boolean
不支持位运算:boolean
类型不能参与位运算(需用&&
/||
逻辑运算)。- 浮点数无位运算:
float
/double
不支持位运算符(底层是IEEE 754浮点格式,需先转整数再操作)。
二进制运算符应用场景
Java中除了位移运算符(<<
、>>
、>>>
),还有另外4种核心二进制运算符:按位与(&
)、按位或(|
)、按位异或(^
)、按位取反(~
)。它们直接对整数的补码二进制位进行逐位操作,广泛用于位级数据处理、权限控制、编码解码等场景。
一、按位与(&):全1为1,否则为0
规则:两个操作数的对应二进制位进行比较,只有当两个位都为1时,结果位才为1;其他情况(0&0、0&1、1&0)结果位为0。
示例(以8位二进制为例):
计算 3 & 5
:
- 3的补码:
00000011
- 5的补码:
00000101
- 逐位与运算:
00000011 & 00000101 ----------- 00000001 // 结果为1(十进制)
核心应用场景:
- 提取/保留特定位(配合掩码):
用掩码(mask)中为1
的位“保留”目标数的对应位,为0
的位“清除”对应位。
例:提取int
类型的低8位(取最后一个字节):
int num = 0x123456; // 二进制:00010010 00110100 01010110 int mask = 0xFF; // 掩码:00000000 00000000 11111111 int result = num & mask; // 0x56(仅保留低8位)
- 判断奇偶性:
一个数的二进制末位为1
则是奇数,为0
则是偶数。通过与1
按位与可快速判断:
boolean isOdd = (num & 1) == 1; // 末位为1 → 奇数
- 清零操作:
与全0掩码按位与,可将目标数清零(num & 0 → 0
)。
二、按位或(|):有1为1,全0为0
规则:两个操作数的对应二进制位进行比较,只要有一个位为1,结果位就为1;只有当两个位都为0时,结果位才为0。
示例:
计算 3 | 5
:
- 3的补码:
00000011
- 5的补码:
00000101
- 逐位或运算:
00000011 | 00000101 ----------- 00000111 // 结果为7(十进制)
核心应用场景:
- 设置特定位为1(配合掩码):
用掩码中为1
的位“强制设置”目标数的对应位为1,其他位保持不变。
例:将int
的第3位(从0开始)设为1:
int num = 0b1001; // 二进制1001(9) int mask = 1 << 3; // 掩码1000(第3位为1) int result = num | mask; // 1001 | 1000 = 1001(若原位为0则变为1)
- 组合权限/状态:
用不同位表示不同权限(如READ=1<<0
、WRITE=1<<1
),通过|
组合多个权限:
int permissions = READ | WRITE | EXEC; // 组合读、写、执行权限
- 填充数据:
将低n位填充为1(如num | ((1 << n) - 1)
),用于范围限制。
三、按位异或(^):不同为1,相同为0
规则:两个操作数的对应二进制位进行比较,若两个位不同(0和1),结果位为1;若相同(0和0或1和1),结果位为0。
示例:
计算 3 ^ 5
:
- 3的补码:
00000011
- 5的补码:
00000101
- 逐位异或运算:
00000011 ^ 00000101 ----------- 00000110 // 结果为6(十进制)
核心应用场景:
- 翻转特定位:
用掩码中为1
的位“翻转”目标数的对应位(0变1,1变0),为0
的位保持不变。
例:翻转int
的低4位:
int num = 0b1010; // 1010 int mask = 0b1111; // 掩码1111 int result = num ^ mask; // 1010 ^ 1111 = 0101(低4位翻转)
- 不借助临时变量交换两个数:
利用异或的特性(a ^ a = 0
,a ^ 0 = a
)实现无临时变量交换:
int a = 3, b = 5; a = a ^ b; // a = 3^5 = 6 b = a ^ b; // b = 6^5 = 3(原a的值) a = a ^ b; // a = 6^3 = 5(原b的值)
- 简单加密/校验:
对数据与密钥异或实现加密,解密时再次异或相同密钥:
int data = 0x1234; int key = 0x5678; int encrypted = data ^ key; // 加密 int decrypted = encrypted ^ key; // 解密(恢复原数据)
四、按位取反(~):0变1,1变0
规则:对操作数的每一位二进制位进行“取反”(0→1,1→0),包括符号位。结果为“操作数的补码取反”(对int
是32位取反,long
是64位取反)。
示例(32位int):
计算 ~3
:
- 3的补码:
00000000 00000000 00000000 00000011
- 按位取反:
11111111 11111111 11111111 11111100
(补码) - 转换为十进制:
-4
(补码取反后为负数,计算方式见补码规则)
核心应用场景:
- 生成反掩码:
对掩码取反得到“反掩码”,用于清除特定位(配合&
运算)。
例:清除int
的高16位:
int num = 0x12345678; int mask = 0xFFFF0000; // 高16位为1的掩码 int result = num & ~mask; // 清除高16位,保留低16位(0x5678)
- 计算负数的补码:
取反加1是求负数补码的方法(~n + 1 = -n
),例:~3 + 1 = -3
。 - 位运算中的“非”操作:
对布尔相关的位状态取反(如flag = ~flag & 1
,将0和1互转)。
五、左移(<<):高效的“乘以2ⁿ”运算
左移的核心特性是:无溢出时,a << n
** 等价于 **a × 2ⁿ
,且位操作比乘法指令执行更快(硬件层面仅需移位,无需复杂计算)。因此,左移主要用于需要“快速放大数值”或“按2的幂次调整范围”的场景。
1. 性能敏感的乘法计算
在高频计算场景(如游戏引擎、实时数据处理)中,用左移替代× 2ⁿ
可提升性能。
例:
- 计算
a × 8
(即a × 2³
):a << 3
比a * 8
执行更快; - 图形渲染中计算像素地址(如每行像素数为
width
,第y
行的起始地址可表示为baseAddress + (y << log2(pixelSize))
,通过左移快速计算偏移量)。
2. 生成掩码或标志位
左移可快速生成“仅某一位为1”的二进制掩码(用于位运算中的权限控制、状态标记)。
例:
// 生成第n位为1的掩码(如第3位:1000) int mask = 1 << 3; // 二进制1000,对应十进制8
这是权限控制、状态位设计的基础(如前面提到的READ = 1 << 0
、WRITE = 1 << 1
)。
3. 调整数值范围(缩放)
在需要按2的幂次缩放数据时(如将小范围数值映射到更大范围),左移是简洁的实现方式。
例:将8位灰度值(0~255)映射到32位ARGB颜色的红色通道(需左移16位):
int gray = 0x80; // 8位灰度值(128) int redChannel = gray << 16; // 映射到ARGB的红色通道(0x800000)
六、算术右移(>>):有符号数的“除以2ⁿ”运算
算术右移的核心特性是:a >> n
** 等价于 a ÷ 2ⁿ
(向下取整),且保持符号不变**(正数仍为正,负数仍为负)。因此,它适合处理“有符号数的缩小”或“需要保留符号的位移”场景。
1. 有符号数的除法优化
与左移对应,算术右移可替代÷ 2ⁿ
,且比除法指令更快,尤其适合负数场景(保证结果仍为负数)。
例:
- 计算
a ÷ 4
(即a ÷ 2²
):a >> 2
比a / 4
更高效; - 负数除法:
-8 >> 1
结果为-4
(正确),而若用无符号右移会得到正数(错误)。
2. 二分查找中的中间索引计算
在二分查找等算法中,计算中间索引(mid = (left + right) / 2
)时,用算术右移可避免溢出并提升效率。
例:
int left = 0, right = 1000; int mid = (left + right) >> 1; // 等价于 (left + right) / 2,结果为500
(注:更严谨的写法是left + ((right - left) >> 1)
,避免left + right
溢出,但核心仍依赖右移的除法特性。)
3. 音频/视频的音量调整(按比例缩小)
在多媒体处理中,音量调整本质是对音频采样值(有符号整数)按比例缩小,算术右移可高效实现“除以2ⁿ”的衰减。
例:将音量衰减为原来的1/2(右移1位):
short sample = 32767; // 音频采样值(有符号16位) short attenuated = (short) (sample >> 1); // 16383(约为32767/2)
七、无符号右移(>>>):无符号数据处理与位提取
无符号右移的核心特性是:左侧补0,不考虑符号位,将数值视为“无符号整数”处理。因此,它适合处理“无符号数据”或“需要提取二进制位”的场景(忽略符号影响)。
1. 解析二进制协议/文件格式
网络协议(如TCP/UDP报文头)、文件格式(如图片、视频)的字段常以“无符号二进制位”存储(如长度、标志),无符号右移可正确提取这些字段。
例:解析一个32位无符号整数的高16位和低16位:
int unsignedInt = 0x12345678; // 32位无符号数(十六进制) int high16 = unsignedInt >>> 16; // 提取高16位:0x1234 int low16 = unsignedInt & 0xFFFF; // 提取低16位:0x5678
2. 处理哈希值或UUID(无符号场景)
哈希值(如Object.hashCode()
)、UUID等通常被视为无符号数,无符号右移可避免符号位干扰,正确处理这些值的位操作。
例:将32位哈希值转换为0~1的浮点数(需视为无符号数):
int hashCode = "test".hashCode(); float ratio = (hashCode >>> 1) / (float) (1 << 31); // 无符号右移后计算比例
3. 生成非负整数(消除符号位影响)
当需要将负数转换为非负数(仅关注其补码的二进制位模式)时,无符号右移可直接“抹除”符号位(左侧补0)。
例:将-1
(32位全1)转换为最大非负整数:
int negative = -1; int positive = negative >>> 0; // 结果为2147483647(32位无符号最大值)
4. 位逆序或循环移位(底层算法)
在密码学、编码算法中,需要对二进制位进行逆序或循环移位,无符号右移可配合其他位运算实现(避免符号位干扰)。
例:将8位二进制位逆序(如10110010
→ 01001101
):
byte b = (byte) 0xB2; // 二进制10110010 int reversed = 0; for (int i = 0; i < 8; i++) { reversed = (reversed << 1) | ((b >>> i) & 1); // 无符号右移提取每一位 } // reversed结果为0x4D(二进制01001101)
总结
三种位移运算符的应用场景可归纳为:
运算符 | 核心特性 | 典型场景 |
---|---|---|
& | 全1为1,否则为0 | 提取特定位、判断奇偶、清零 |
` | ` | 有1为1,全0为0 |
^ | 不同为1,相同为0 | 翻转特定位、交换变量、简单加密 |
~ | 0变1,1变0 | 生成反掩码、计算补码、位状态取反 |
<< | 等价于× 2ⁿ ,快速放大 | 性能敏感的乘法、生成掩码、数值范围调整 |
>> | 等价于÷ 2ⁿ ,保持符号 | 有符号数除法、二分查找、音量调整 |
>>> | 无符号处理,左侧补0 | 二进制协议解析、哈希值处理、位提取 |
理解这些场景的关键是:根据是否需要“保留符号”“处理无符号数据”或“追求运算效率”选择合适的位移方式,充分利用位操作的高性能特性。
补充:反码循环进位
在反码的加法运算中,“循环进位”是一个特殊的处理规则,指的是当两个反码相加时,若最高位(符号位)产生进位,则需要将这个进位“循环”到结果的最低位(即把进位值1加到结果的最低位),以保证运算结果的正确性。
为什么会出现循环进位?
反码的核心问题是0的表示不唯一(存在+0和-0),这导致反码的加法运算可能产生“虚假进位”。如果直接丢弃最高位的进位,会导致结果错误,因此需要通过“循环进位”修正。
举例说明循环进位的过程
以8位反码为例,计算 3 + (-1)
:
- 先写出两数的反码:
- 3的反码:
00000011
(正数反码=原码) - -1的原码:
10000001
→ 反码(符号位不变,数值位取反):11111110
- 3的反码:
- 反码相加:
00000011 (3的反码) + 11111110 (-1的反码) ----------- 100000001 (结果,最高位产生进位1)
- 处理循环进位:
最高位的进位“1”不能丢弃,需加到结果的最低位:
00000001 (去掉最高位进位后的结果) + 1 (循环进位的1) ----------- 00000010 (最终结果,对应十进制2,正确)
若不循环进位会怎样?
如果直接丢弃最高位的进位,上面的结果会是 00000001
(对应十进制1),与正确结果2不符。可见,循环进位是反码加法中修正错误的必要步骤。
总结
- 循环进位:反码加法中,最高位产生的进位需加到结果的最低位,是反码为解决“0的双重表示”导致的运算错误而设计的规则。
- 局限性:这种处理增加了硬件实现的复杂度,这也是反码仅作为“过渡形式”存在,而计算机最终采用补码(无需循环进位)的重要原因。
到此这篇关于Java二进制运算符超详细讲解及扩展知识的文章就介绍到这了,更多相关Java二进制运算符内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!