java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java二进制运算符

Java二进制运算符超详细讲解及扩展知识

作者:弥鸿

Java源码中涉及到大量的二进制操作,非常的复杂,但非常的快速,下面这篇文章主要介绍了Java二进制运算符超详细讲解及扩展知识的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

看Java数据结构源码的时候发现很多源码中都用到了<<>>运算符,果然是我太low了,用运算符都跟不上大佬的脚步;所以打算从头再看一次这些Java中的运算符;

本来像写的简单点的,但是一开始梳理就收不住了… 然后就又扯出来了一大堆知识点…

经过我个人的学习过程,梳理了一下相关的知识:

计算机存储单元

首先区分两个概念:

1B = 8bit;
1KB = 1024B;
1MB = 1024KB;
1GB = 1024MB;
1TB = 1024GB

区分这个概念的目的是:为什么二进制数的表示长度只能是8的倍数;(反正我刚学那会是不懂)

原码、补码、反码、掩码

一、原码(True Form)

定义:最直观的二进制表示法,由“符号位+数值位”组成,直接对应十进制数的正负和绝对值。

二、反码(One’s Complement)

定义:原码的“变形”,是从原码过渡到补码的中间形式,主要用于简化负数的运算。

三、补码(Two’s Complement)

定义:计算机中实际使用的有符号整数表示法,解决了原码和反码的运算缺陷,能将减法统一为加法。

四、掩码(Mask)

定义:一种特定的二进制序列(通常是整数),通过与位运算符(&|^)配合,实现对目标二进制数“特定位的提取、设置或清除”。

Java的基本数据类型

不同基本数据类型的区别在于二进制的“组织方式”和“长度”(即占用的比特数),但本质都是二进制。以下是具体说明:

一、 整数类型(byte/short/int/long)

整数类型直接以二进制补码形式存储(补码是计算机中表示有符号整数的标准方式,可统一加减法运算):

二、 浮点类型(float/double)

浮点类型遵循IEEE 754标准,以二进制形式存储,但结构更复杂(分为符号位、指数位、尾数位):

三、 字符类型(char)

char类型存储Unicode字符编码(本质是无符号整数),以16位二进制形式存储:

四、 布尔类型(boolean)

boolean类型比较特殊,Java规范未明确其占用的比特数(不同JVM实现可能不同),但本质仍是二进制表示:

Java中的整数存储

一、Java中整数的存储:补码是唯一标准

Java中所有有符号整数(byte/short/int/long统一使用补码存储,不存在原码或反码的直接使用,这是Java为简化运算和硬件适配做的设计:

示例
十进制-5int补码计算:
原码(符号位1+数值位5)→ 10000000 00000000 00000000 00000101
反码(符号位不变,数值位取反)→ 11111111 11111111 11111111 11111010
补码(反码+1)→ 11111111 11111111 11111111 11111011(Java中实际存储的形式)

二、Java中的位运算符:直接操作二进制补码

Java支持5种位运算符(针对整数类型),操作的是数值的补码二进制位:

运算符名称规则(逐位操作)示例(int类型)
&按位与全1为1,否则为03 & 5 → 00000011 & 00000101 = 00000001 → 1
``按位或按位或
^按位异或不同为1,相同为03 ^ 5 → 00000011 ^ 00000101 = 00000110 → 6
~按位取反0变1,1变0(补码取反)~3 → ~00000011 = 11111100(补码)→ -4
<<左移左移n位,右补03 << 2 → 00000011 << 2 = 00001100 → 12
>>算术右移右移n位,左补符号位(正数补0,负数补1)-8 >> 1 → 11111000 >> 1 = 11111100 → -4
>>>无符号右移右移n位,左补0(忽略符号位)-1 >>> 1 → 11111111... >>> 1 = 01111111... → 2147483647

关键特性

  1. 类型提升:对byte/short进行位运算时,会先自动提升为int(32位),结果也是int
    例:byte b = 3; byte c = (byte)(b << 1);(需强制转型,否则结果为int)。
  2. 移位位数取模:移位位数若超过类型位数(如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)

四、注意事项

  1. 溢出问题:位运算可能导致溢出(如int最大值左移1位变为负数),但Java不抛异常,直接按补码截断。
  2. boolean不支持位运算boolean类型不能参与位运算(需用&&/||逻辑运算)。
  3. 浮点数无位运算float/double不支持位运算符(底层是IEEE 754浮点格式,需先转整数再操作)。

二进制运算符应用场景

Java中除了位移运算符(<<>>>>>),还有另外4种核心二进制运算符:按位与(&按位或(|按位异或(^按位取反(~。它们直接对整数的补码二进制位进行逐位操作,广泛用于位级数据处理、权限控制、编码解码等场景。

一、按位与(&):全1为1,否则为0

规则:两个操作数的对应二进制位进行比较,只有当两个位都为1时,结果位才为1;其他情况(0&0、0&1、1&0)结果位为0。

示例(以8位二进制为例):

计算 3 & 5

  00000011
& 00000101
-----------
  00000001  // 结果为1(十进制)

核心应用场景:

  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. 判断奇偶性
    一个数的二进制末位为1则是奇数,为0则是偶数。通过与1按位与可快速判断:
boolean isOdd = (num & 1) == 1; // 末位为1 → 奇数
  1. 清零操作
    与全0掩码按位与,可将目标数清零(num & 0 → 0)。

二、按位或(|):有1为1,全0为0

规则:两个操作数的对应二进制位进行比较,只要有一个位为1,结果位就为1;只有当两个位都为0时,结果位才为0。

示例:

计算 3 | 5

  00000011
| 00000101
-----------
  00000111  // 结果为7(十进制)

核心应用场景:

  1. 设置特定位为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)
  1. 组合权限/状态
    用不同位表示不同权限(如READ=1<<0WRITE=1<<1),通过|组合多个权限:
int permissions = READ | WRITE | EXEC; // 组合读、写、执行权限
  1. 填充数据
    将低n位填充为1(如num | ((1 << n) - 1)),用于范围限制。

三、按位异或(^):不同为1,相同为0

规则:两个操作数的对应二进制位进行比较,若两个位不同(0和1),结果位为1;若相同(0和0或1和1),结果位为0。

示例:

计算 3 ^ 5

  00000011
^ 00000101
-----------
  00000110  // 结果为6(十进制)

核心应用场景:

  1. 翻转特定位
    用掩码中为1的位“翻转”目标数的对应位(0变1,1变0),为0的位保持不变。
    例:翻转int的低4位:
int num = 0b1010; // 1010
int mask = 0b1111; // 掩码1111
int result = num ^ mask; // 1010 ^ 1111 = 0101(低4位翻转)
  1. 不借助临时变量交换两个数
    利用异或的特性(a ^ a = 0a ^ 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的值)
  1. 简单加密/校验
    对数据与密钥异或实现加密,解密时再次异或相同密钥:
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

核心应用场景:

  1. 生成反掩码
    对掩码取反得到“反掩码”,用于清除特定位(配合&运算)。
    例:清除int的高16位:
int num = 0x12345678;
int mask = 0xFFFF0000; // 高16位为1的掩码
int result = num & ~mask; // 清除高16位,保留低16位(0x5678)
  1. 计算负数的补码
    取反加1是求负数补码的方法(~n + 1 = -n),例:~3 + 1 = -3
  2. 位运算中的“非”操作
    对布尔相关的位状态取反(如flag = ~flag & 1,将0和1互转)。

五、左移(<<):高效的“乘以2ⁿ”运算

左移的核心特性是:无溢出时,a << n** 等价于 **a × 2ⁿ,且位操作比乘法指令执行更快(硬件层面仅需移位,无需复杂计算)。因此,左移主要用于需要“快速放大数值”或“按2的幂次调整范围”的场景。

1. 性能敏感的乘法计算

在高频计算场景(如游戏引擎、实时数据处理)中,用左移替代× 2ⁿ可提升性能。
例:

2. 生成掩码或标志位

左移可快速生成“仅某一位为1”的二进制掩码(用于位运算中的权限控制、状态标记)。
例:

// 生成第n位为1的掩码(如第3位:1000)
int mask = 1 << 3; // 二进制1000,对应十进制8

这是权限控制、状态位设计的基础(如前面提到的READ = 1 << 0WRITE = 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ⁿ,且比除法指令更快,尤其适合负数场景(保证结果仍为负数)。
例:

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位二进制位逆序(如1011001001001101):

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)

  1. 先写出两数的反码:
    • 3的反码:00000011(正数反码=原码)
    • -1的原码:10000001 → 反码(符号位不变,数值位取反):11111110
  2. 反码相加:
  00000011  (3的反码)
+ 11111110  (-1的反码)
-----------
100000001  (结果,最高位产生进位1)
  1. 处理循环进位:
    最高位的进位“1”不能丢弃,需加到结果的最低位:
  00000001  (去掉最高位进位后的结果)
+       1  (循环进位的1)
-----------
  00000010  (最终结果,对应十进制2,正确)

若不循环进位会怎样?

如果直接丢弃最高位的进位,上面的结果会是 00000001(对应十进制1),与正确结果2不符。可见,循环进位是反码加法中修正错误的必要步骤。

总结

到此这篇关于Java二进制运算符超详细讲解及扩展知识的文章就介绍到这了,更多相关Java二进制运算符内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文