JavaSE(二) 运算符详解:整数除法、自增自减、短路逻辑与位运算

本文深入剖析Java运算符中易被忽视的关键机制:整数除法截断、取余符号规则、复合赋值隐式类型转换、自增自减执行时序、关系运算符左结合性、逻辑短路保障空安全、有/无符号右移差异、条件运算符类型统一规则,以及优先级误区。重点解释'为什么'而非仅结论,覆盖JLS规范行为与典型生产踩坑场景。

面向 Java 零基础阶段的知识梳理,重点讲「为什么」而不是只给结论。
从 5 / 2 为什么等于 2 说起,逐层拆掉运算符里那些反直觉的地方。

一、整数除法:第一个"算错了"的地方

System.out.println(5 / 2);    // 2,不是 2.5

很多人第一次看到这个结果,会以为是打印出了问题。其实不是——5 / 2 在 Java 里真的就等于 2。

原因:5 和 2 都是 int,int / int 的结果还是 int,小数部分被直接截断(不是四舍五入)。

System.out.println(5 / 2);      // 2
System.out.println(5 % 2);      // 1   ← 余数在这
System.out.println(5 / 2.0);    // 2.5 ← 有一个是 double,结果就是 double
System.out.println((double) 5 / 2);   // 2.5

想让结果带小数,至少让一个操作数变成浮点类型。 这就是为什么算平均值时经常要写 (double) sum / count,光写 sum / count 会白白丢掉小数。

这个特性在什么时候有用

有时候需要的恰好就是整数除法:

// 1234 秒 = 20 分 34 秒
int minutes = 1234 / 60;    // 20
int seconds = 1234 % 60;    // 34

/ 和 % 配合,就是整除取商、取余数,这个组合在做分页、进制转换、时间换算时天天用。

二、取余的符号:为什么 -10 % 3 是 -1

System.out.println(10 % 3);     // 1
System.out.println(-10 % 3);    // -1
System.out.println(10 % -3);    // 1
System.out.println(-10 % -3);   // -1

规律:取余结果的符号,只看被除数(左边的数),和除数无关。

这个规则跟数学上的"模"不完全一样。数学里 -10 mod 3 通常取 2(保证结果非负),但 Java 的 % 是取余不是取模,符号跟着被除数走。

如果业务上需要"永远是正数"的模,得自己处理:int r = ((a % b) + b) % b;

一个容易写错的例子
System.out.println(-11.5 % 2.0);

凭直觉容易写成 1.5,实际输出是 -1.5。同一个规则——符号跟被除数。小数也可以取余,只是实际业务里极少用。

三、复合赋值:+= 有一手"暗中强转"

int a = 1;
a += 1;    // 等价于 a = a + 1

看起来是语法糖,但在小类型上它的行为不一样:

byte b = 10;
b = b + 1;    // ❌ 编译错误:从 int 转换到 byte 可能会有损失
b += 1;       // ✅ 完全合法

为什么?因为 复合赋值运算符隐含了一次强制类型转换。JLS 规定 b += 1 等价于:

b = (byte) (b + 1);

编译器替你把这个 (byte) 加上了。所以 +=、-=、*=、/=、%= 都能在小类型上过编译。

但代价是:溢出静默发生,编译器一声不吭。

byte b = 127;
b += 1;
System.out.println(b);    // -128

这是运算符里最危险的一个坑——报错的代码会被改掉,不报错的代码可能直接上线。

四、++a 和 a++

int a = 10;
int b = ++a;
System.out.println(a + " " + b);   // 11 11

int c = 10;
int d = c++;
System.out.println(c + " " + d);   // 11 10

口诀:

  • ++a(前缀):先自增,再取值
  • a++(后缀):先取值,再自增

拆开看:

b = ++a;   →   a = a + 1;  b = a;
d = c++;   →   d = c;      c = c + 1;

共同点:变量自己最终都会加 1。 区别只在"参与运算时用的是加之前还是加之后的值"。

所以单独成句时两者完全等价:

int a = 10;
a++;    // a 变 11
++a;    // a 变 12
一个反直觉的句子
int i = 1;
i = i++;
System.out.println(i);   // 1,不是 2

拆解:i++ 先把旧值 1 交出去参与运算,然后 i 自增变成 2;紧接着赋值把交出去的 1 又写回 i,于是 i 被打回 1。

不用记这个结果,只要记住:永远别写 i = i++。 生产代码里这样写会被 review 打回来。

五、关系运算符:3 < a < 5 为什么编译不过

int a = 3;
System.out.println(3 < a < 5);    // ❌ 编译错误

报错说:运算符 '<' 不能应用于 boolean,int。

原因:< 是左结合的,3 < a < 5 实际算的是 (3 < a) < 5:

  1. 3 < a 得到 boolean(false)
  2. 再算 false < 5 —— 布尔值和数字比大小,Java 不允许

正确写法:

System.out.println(3 < a && a < 5);

这个坑在 C 语言里更阴险:3 < a < 5 在 C 里合法,会被算成 (3<a)<5,也就是 0 < 5 或 1 < 5,永远为真。Java 靠强类型在编译期就拦住了。

关系运算符的返回永远是 boolean,这是它能被 if 直接使用的原因。

六、逻辑运算符与短路

boolean r1 = a > b && a == 10;    // 两个都真才真
boolean r2 = a > b || a == 10;    // 有一个真就真
boolean r3 = !(a > b);            // 取反

&& 和 || 的核心特性是短路:

左边已经能决定结果时,右边根本不执行。

int i = 0;
boolean r = (i > 0) && (i++ > 0);    // i>0 是 false,i++ 没执行
System.out.println(i);               // 0

int j = 10;
boolean r2 = (j > 0) || (j++ > 0);   // j>0 是 true,j++ 没执行
System.out.println(j);               // 10
& 和 | 也能用在 boolean 上

这是最容易混淆的地方。&、|、^ 对 boolean 也是合法的,区别是不短路:

int i = 0;
boolean r = (i > 0) & (i++ > 0);   // 两边都算
System.out.println(i);             // 1

所以这个经典写法必须用 &&:

if (str != null && str.length() > 0) { }

如果写成 &,当 str 是 null 时,str.length() 照样执行,直接空指针。

这就是短路存在的意义:它不只是性能优化,也是正确性保障。

七、位运算符

位运算是对二进制每一位逐个操作。

  0101 1011
& 0011 0011     & 按位与:两位都是 1 才得 1
-----------
  0001 0011

  0101 1011
| 0011 0011     | 按位或:有一个 1 就得 1
-----------
  0111 1011

  0101 1011
^ 0011 0011     ^ 按位异或:两位不同才得 1
-----------
  0110 1000

  ~ 0101 1011   ~ 按位取反:0变1,1变0
-----------
    1010 0100
移位运算:>> 和 >>> 的区别
int x = -8;
System.out.println(x >> 1);     // -4
System.out.println(x >>> 1);    // 2147483644
  • >> 有符号右移:左边补符号位。负数右移左边补 1,所以结果还是负数
  • >>> 无符号右移:左边一律补 0,负数会变成一个大正数

左移一位约等于乘 2,右移一位约等于除 2:

System.out.println(5 << 1);    // 10
System.out.println(5 >> 1);    // 2  (小数被截断)

**别拿它替代 2*。现代编译器会自动把 *2 优化成移位,可读性远比这点微优化重要。JDK 源码里用它,是因为那里真的一纳秒都要省。

八、条件运算符 ? :

int max = 10 > 20 ? 10 : 20;
System.out.println(max);    // 20

格式:条件 ? 值1 : 值2,条件为真取值1,为假取值2。

它有类型统一的规则:

int r = true ? 1 : "hello";       // ❌ 编译错误,int 和 String 无法统一
Object o = true ? 1 : "hello";    // ✅ 统一为公共父类 Object

为什么? 两个分支的类型必须能合并成一个。int 和 String 没有共同的数值父类,只能提到 Object。

这个规则如果两个分支都是数字,会做数值提升:

System.out.println(true ? 1 : 2.0);    // 1.0,不是 1

因为 int 和 double 统一成 double,所以 1 被提升成 1.0。这个行为在看输出格式时容易出意外。

九、优先级速查(记不住就加括号)

从高到低:

()  []  .                      括号、数组、成员访问
++  --  !  ~  (强转)            一元
*  /  %                        乘除取余
+  -                           加减
<<  >>  >>>                    移位
<  >  <=  >=  instanceof       关系
==  !=                         相等
&                              按位与
^                              按位异或
|                              按位或
&&                             短路与
||                             短路或
? :                            条件
=  +=  -=  *=  /=  %=          赋值

实际写代码只记三条:

  1. && 优先级高于 ||,所以 a || b && c 是 a || (b && c)
  2. 赋值运算符优先级最低,所以 a = b + c 一定是先算右边
  3. 拿不准就加括号 —— 括号不加分,但能省一次 bug

十、常见误区与踩坑记录

  1. 5 / 2 = 2,整数除法截断,要小数必须让操作数变浮点
  2. -10 % 3 = -1,取余符号跟被除数走;-11.5 % 2.0 容易误写成 1.5,实际是 -1.5
  3. byte b = 127; b += 1; 结果是 -128,编译器不报错
  4. 3 < a < 5 编译不过,必须拆成 3 < a && a < 5
  5. && 是短路,& 不短路——判空那个场景用错就是空指针
  6. >>> 对负数会变成大正数
  7. true ? 1 : 2.0 结果是 1.0,因为类型被统一成 double

十一、自测

  1. 5 / 2 和 5 / 2.0 分别输出什么?5 % 2 呢?
  2. -7 % 3 等于多少?为什么?
  3. int i = 1; i = i++; 之后 i 是几?
  4. byte b = 127; b += 1; 输出什么?为什么不报错?
  5. str != null & str.length() > 0 有什么风险?
  6. -8 >>> 1 和 -8 >> 1 结果差多少?为什么?
  7. true ? 1 : 2.0 输出 1 还是 1.0?

相关阅读:Java 八种基本数据类型详解:类型提升、String 本质与常见踩坑——byte + byte 为什么不能赋给 byte,以及 b += 1 静默溢出背后的类型规则。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值