- 0
- 309 words
上篇我们把 8 种基本数据类型和变量声明搞清楚了,数据在变量里就是要拿来用的。这一篇先讲运算符——从加减乘除到自增自减,从短路逻辑到三元表达式,外加面试必考的原码反码补码。这些看似基础的东西,坑比你想的多。
一、算术运算符
先来个全家福:
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
+ | 加 | 5 + 3 | 8 |
- | 减 | 5 - 3 | 2 |
* | 乘 | 5 * 3 | 15 |
/ | 除 | 5 / 3 | 1(整数除法截断!) |
% | 取模(取余) | 5 % 3 | 2 |
1.1 整数除法的陷阱
上一篇提过,两个整数相除结果还是整数:
System.out.println(7 / 3); // 2,不是 2.333...
System.out.println(7.0 / 3); // 2.3333333333333335
System.out.println(7 / 3.0); // 2.3333333333333335
只要操作数中有一个是浮点数,Java 就会自动做浮点数除法。
1.2 取模运算符 %
% 得到的是余数,用途很广:
// 判断奇偶
int num = 17;
System.out.println(num % 2 == 0 ? "偶数" : "奇数"); // 奇数
// 对负数取模的结果符号与被除数相同
System.out.println(7 % 3); // 1
System.out.println(-7 % 3); // -1
System.out.println(7 % -3); // 1
🐛 面试高频:
a % b的结果符号与a相同。这个问题在实现循环数组、哈希表等场景会出现,有公司把它当笔试题考。
1.3 字符串拼接的 + 号
+ 在 Java 里是个”双重身份”的运算符——两边都是数字时做加法,有一边是 String 时就做拼接:
System.out.println(1 + 2 + "Java"); // "3Java"(从左到右,先算加法)
System.out.println("Java" + 1 + 2); // "Java12"(遇到 String 后的 + 全变拼接)
System.out.println(1 + (2 + "Java")); // "12Java"(括号改变优先级)
你看出规律了吗?从左到右执行,只要遇到字符串,后面的 + 就变成了拼接符。
二、赋值运算符
最基本的 = 你已经见过了。Java 还提供了复合赋值运算符,把算术和赋值合并:
int x = 10;
x += 5; // 相当于 x = x + 5 → 15
x -= 3; // 相当于 x = x - 3 → 12
x *= 2; // 相当于 x = x * 2 → 24
x /= 4; // 相当于 x = x / 4 → 6
x %= 3; // 相当于 x = x % 3 → 0
复合赋值不仅有写法简洁的好处,还有一个隐式的类型转换特性:
byte b = 10;
b = b + 5; // ❌ 编译报错!b + 5 的结果是 int,不能直接赋给 byte
b += 5; // ✅ 编译通过!复合赋值自带隐式强转(相当于 b = (byte)(b + 5))
💡 原理:
E1 op= E2等价于E1 = (T)((E1) op (E2)),其中 T 是 E1 的类型。所以b += 5会自动强转回 byte。
三、自增自减运算符
不管是写循环还是做计数,++ 和 -- 都用得极其频繁。它们的唯一作用就是让变量的值 +1 或 -1。
关键在于前置和后置的区别:
int a = 5;
int b = a++; // 后置++:先使用 a 的当前值(5)赋值给 b,然后 a 自增为 6
System.out.println(a); // 6
System.out.println(b); // 5
int c = 5;
int d = ++c; // 前置++:先将 c 自增为 6,然后把 6 赋值给 d
System.out.println(c); // 6
System.out.println(d); // 6
一句话口诀:“先取值后自增”(后置),”先自增后取值”(前置)。
面试里经常这样考:
int i = 0;
for (int j = 0; j < 5; j++) {
i = i++;
}
System.out.println(i); // 猜猜结果?
答案是 0。因为 i = i++ 的执行顺序是:先用临时变量保存 i 的值(0)→ i 自增为 1 → 再把临时变量的值(0)赋值给 i。所以循环跑完 i 还是 0。这在新手面试中是个经典陷阱,很多人想当然地认为是 5。
四、关系运算符
关系运算符用来比较两个值的大小或相等关系,结果永远是 boolean(true / false):
int a = 10, b = 20;
System.out.println(a == b); // false 等于
System.out.println(a != b); // true 不等于
System.out.println(a > b); // false 大于
System.out.println(a = b); // false 大于等于
System.out.println(a <= b); // true 小于等于
⚠️ 千万别把 == 写成 =:
// 经典 bug
int x = 5;
if (x = 10) { // ❌ 编译报错!Java 中 if 要求 boolean,而 x = 10 的结果是 int
// ...
}
// 但如果是 boolean 类型…
boolean flag = false;
if (flag = true) { // 🚨 不会报错!但 flag = true 是赋值表达式,结果就是 true
// 这段代码永远会执行!这是老 C 程序员常犯的错误
}
Java 比 C 语言更安全——它对条件表达式做了类型检查,int 不能直接当 boolean 用。但 boolean 类型的变量赋值仍然可能踩坑。建议养成习惯:把常量写前面,即 if (true == flag),这样少写一个 = 就会编译报错。
五、逻辑运算符
| 运算符 | 名称 | 示例 | 结果 |
|---|---|---|---|
&& | 短路与 | true && false | false |
|| | 短路或 | true || false | true |
! | 非 | !true | false |
5.1 短路的含义
“短路”是逻辑运算符最核心的特性——只要结果已经确定,后面的表达式就不再计算:
int a = 5, b = 10;
// && 短路:左边为 false,右边不执行
boolean result1 = (a > 10) && (b++ > 0);
System.out.println(b); // 10(b++ 没被执行!)
// || 短路:左边为 true,右边不执行
boolean result2 = (a 0);
System.out.println(b); // 10(b++ 仍然没被执行)
面试题实战:
int i = 0, j = 0;
boolean r = (i++ > 0) && (j++ > 0);
System.out.println(i + ", " + j); // 猜猜? → 1, 0
// i++ > 0 → 0 > 0 → false,i 变成 1
// 因为 && 短路,j++ 没被执行,j 仍然是 0
5.2 & 和 |(不短路版本)
& 和 | 既是位运算符(对整数做位运算),也可以当逻辑运算符(两边是 boolean 时),但不会短路:
int a = 5, b = 10;
boolean r1 = (a > 10) & (b++ > 0); // & 不短路,两边都算
System.out.println(b); // 11(b++ 执行了)
boolean r2 = (a > 10) && (b++ > 0); // && 短路,右边不算
System.out.println(b); // 11(没再自增)
日常开发中永远优先用 && 和 ||。用 & 和 | 做逻辑判断的唯一场景是:你确实需要两边都要执行(这种情况极少,且通常用更清晰的代码替代)。
六、三元运算符
Java 里唯一的三目运算符,语法:
类型 变量 = (条件表达式) ? 值1 : 值2;
条件为 true 返回值1,否则返回值2:
int age = 18;
String result = (age >= 18) ? "成年人" : "未成年人";
System.out.println(result); // 成年人
// 三元运算符可以嵌套,但不建议—可读性太差
int score = 85;
String grade = score >= 90 ? "优秀"
: score >= 80 ? "良好"
: score >= 60 ? "及格"
: "不及格";
System.out.println(grade); // 良好
三元运算符的本质是一个表达式,有返回值,可以嵌套在另一个表达式中。但嵌套超过两层建议换成 if-else,代码的可读性比代码长度重要得多。
附:原码、反码、补码详解
这一节本来可以单独开一篇,但因为和位运算关系最密切,放在这里一起讲了。很多初学者在这里就放弃了,但其实理解这三个概念对理解 Java 如何处理整数至关重要。
为什么需要这三种码?
计算机里所有数据最终都是二进制——0 和 1。但负数怎么表示?用最高位(符号位)表示正负?那 0 怎么办?减法怎么处理?
工程师们想出了补码的解决方案——用补码表示负数,可以把减法统一成加法,硬件实现就简单多了。
逐个击破
以 byte 类型(1字节,8位)为例:
| 概念 | 定义 | 示例(以 -5 为例) |
|---|---|---|
| 原码 | 最高位符号位(0正1负),其余位是数值的二进制 | 1000 0101 |
| 反码 | 正数不变,负数:符号位不变,其余位取反 | 1111 1010 |
| 补码 | 正数不变,负数:反码 + 1 | 1111 1011 |
正数的原码、反码、补码是一样的。正数 5:原码 0000 0101 = 反码 0000 0101 = 补码 0000 0101。
负数的关系链:
- 写出正数的二进制(原码)
- 符号位变 1,得到负数的原码
- 符号位不变,其他位取反 → 反码
- 反码 + 1 → 补码
为什么 byte 的范围是 -128 ~ 127?
看补码的全景图就懂了:
| 补码 | 值 |
|---|---|
0000 0000 | 0 |
0000 0001 | 1 |
| … | … |
0111 1111 | 127(最大正数) |
1000 0000 | -128(最小负数) |
1000 0001 | -127 |
| … | … |
1111 1111 | -1 |
注意 1000 0000 用来表示 -128,而不是 -0(0 的补码只有一个 0000 0000)。所以 byte 能表示 256 个数:-128 ~ 127。同理 int 的 -2³¹ ~ 2³¹-1 也是这么来的。
补码的实际应用:减法变加法
计算 5 - 3,计算机内部是 5 + (-3):
5 的补码:0000 0101
-3 的补码:1111 1101(3 → 0000 0011 → 取反 1111 1100 → +1 → 1111 1101)
────────────
结果: 1 0000 0010
高位溢出丢弃,得到 0000 0010 = 2 ✅
一个加法器就搞定了减法。这就是补码设计的精妙之处——不需要额外的减法电路。
位运算(你会用上补码知识的地方)
Java 提供了几个位运算符,操作的是整数的二进制位(补码):
| 运算符 | 名称 | 示例 | 结果(二进制) |
|---|---|---|---|
& | 按位与 | 5 & 3 | 0000 0101 & 0000 0011 = 0000 0001(1) |
| | 按位或 | 5 | 3 | 0000 0101 | 0000 0011 = 0000 0111(7) |
^ | 按位异或 | 5 ^ 3 | 0000 0101 ^ 0000 0011 = 0000 0110(6) |
~ | 按位取反 | ~5 | ~0000 0101 = 1111 1010(-6) |
// 常用技巧:用 & 判断奇偶(效率比 % 高一点点)
int num = 7;
System.out.println((num & 1) == 0 ? "偶数" : "奇数"); // 奇数
// 异或的小特性:a ^ b ^ b = a(同数异或两次恢复原值)
int cipher = 42;
int key = 99;
int encrypted = cipher ^ key; // 加密
int decrypted = encrypted ^ key; // 解密:42
System.out.println(decrypted); // 42
位运算在日常业务开发中不常用(除非做底层、网络协议、权限控制等),但面试中经常用来考察二进制理解深度。
结语
这一篇我们把 Java 里的运算符都过了一遍:算术运算的整数截断陷阱、赋值运算的隐式强转、自增自减的前后置区别、关系运算符的 == 和 = 的经典 bug、逻辑运算的短路机制、以及三元运算符的灵活运用。最后还深入了一把它底层的原码反码补码和位运算——以后你看到 -128 时就知道它从哪来的了。
下一篇我们来啃流程控制与分支循环语句——if-else、switch、for、while,这些代码逻辑的骨架。