运算符
Java 初级 · 基础语法
运算符是对数据做运算的符号。表面看 a + b、i++ 很简单,但底层都对应特定字节码指令;而 && 的短路、== 与 .equals() 的区别,更是面试和实战的高频坑。本节把「怎么用」和「底层为什么」一起讲清。
一、运算符全景
| 类别 | 常见运算符 | 操作数 | 结果 |
|---|---|---|---|
| 算术 | + - * / % ++ -- | 数值 | / 整除取整,% 取余 |
| 关系 | > < >= <= == != | 数值/对象 | boolean |
| 逻辑 | && || ! | 布尔 | boolean(带短路) |
| 位运算 | & | ^ ~ << >> >>> | 整数(按位) | 整数 |
| 赋值 | = += -= *= /= %= | 变量 | 赋值 |
| 三元 | 条件 ? 值1 : 值2 | 布尔 + 两值 | 其中之一 |
二、算术运算与整数除法
/ 对整数类型是整除取整(向零截断),不是四舍五入;% 取余数。这在算法和分页里非常常用:
int a = 10 / 3; // 3,余数被丢弃
int b = 10 % 3; // 1
int pages = (total + pageSize - 1) / pageSize; // 经典「向上取整」写法整数除法陷阱
5 / 2 是 2 不是 2.5。若想要小数,至少让一侧为浮点:5.0 / 2 或 (double)5 / 2。另外 -7 / 2 得 -3(向零截断,不是 floor),做索引/分页时留意负号。
自增 ++ 的前后置(字节码视角)
这是初学者最容易错的点。i++ 先取值后加 1,++i 先加 1 后取值:
int i = 5;
int j = i++; // j = 5, i = 6(先用后加)
int k = ++i; // k = 7, i = 7(先加后用)底层对应不同字节码(简化):
i++:先iload把 5 压栈(作为表达式的值),再iinc把局部变量槽 +1。++i:先iinc把槽 +1,再iload压栈。
所以在 int x = i++ + i++; 这种表达式里,顺序和执行结果完全由「取值时机」决定,可读性极差——生产代码里别在复杂表达式里混用自增。
三、短路逻辑 && / ||:不仅是性能,更是安全
&&(与)、||(或)具有短路(short-circuit)特性:左操作数已能决定整体结果时,右操作数根本不会执行。
int x = 0;
boolean r = x != 0 && (10 / x > 1); // x!=0 为 false,右侧不执行,避免除零崩溃对比非短路的 & / |:它们会两边全算,既无短路保护,性能也略差。&/| 主要用于位运算(对整数逐位操作),别拿来当逻辑与或。
短路的字节码真相
&& 编译为条件跳转:先算左,若左为 false 直接 goto 到「结果为 false」,右侧的指令被跳过;只有左为 true 才继续。这正是「右侧不执行」的硬件级原因。
顺序写反会出 NPE / 除零
obj != null && obj.field 是对的;写成 obj.field && obj != null 就会先访问 obj.field 触发 NullPointerException。把「可能为 null / 可能除零 / 可能越界」的判断放左边,用短路保安全。
四、关系运算与「== 比的是什么」
对基本类型,== 比的是「值」是否相等;对引用类型,== 比的是「地址(是否同一个对象)」,不是内容。这是和 .equals() 的根本区别:
int x = 10, y = 10;
System.out.println(x == y); // true,比的是值
String s1 = new String("hi");
String s2 = new String("hi");
System.out.println(s1 == s2); // false!两个不同对象,地址不同
System.out.println(s1.equals(s2)); // true,比的是字符内容== 在字节码里对基本类型是 if_icmpeq(比较两 int 值),对引用类型是 if_acmpeq(比较两引用地址)。.equals() 则是一次真正的虚方法调用,由对象自己定义「怎样算相等」(详见常用类一节)。
五、位运算:直接操作二进制
位运算把整数当作一串 0/1 逐位处理,在底层、网络协议、权限掩码里很常见。
int m = 6; // 二进制 0110
int n = 3; // 二进制 0011
int and = m & n; // 2 (0010) 按位与
int or = m | n; // 7 (0111) 按位或
int xor = m ^ n; // 5 (0101) 按位异或
int inv = ~m; // -7 按位取反(补码下 ~x = -x-1)
int left = m << 1; // 12 (1100) 左移 1 位 ≡ 乘 2
int right= m >> 1; // 3 (0011) 算术右移(保留符号位)≡ 除以 2 向下取整要点:
<< n约等于「乘 2ⁿ」,>> n约等于「除以 2ⁿ」(向下取整)。但注意溢出:1 << 31在int里会变成负数(符号位被置 1)。>>>(无符号右移)与>>的区别:前者高位永远补 0,不保留符号位;对负数结果差异巨大。^(异或)有个妙用:不借助临时变量交换两数a ^= b; b ^= a; a ^= b;(了解即可,可读性不如临时变量)。
六、三元表达式与赋值
三元表达式 条件 ? 值1 : 值2 是 if-else 的「表达式版」,能直接作为一个值:
int score = 85;
String level = score >= 60 ? "及格" : "不及格"; // 整体返回一个 String复合赋值 += 隐含一次窄化强转(见变量一节):short s = 1; s += 1; 合法,但 s = s + 1; 会报错(右边是 int,需显式转回 short)。
优先级:不要靠记忆力,靠括号
a + b * c 先乘后加,!a && b || c 则容易读错。复杂表达式一律加括号:(!a && b) || c。括号不仅防错,更让半年后的你(和同事)一眼看懂意图。Java 运算符优先级表很长,记不住就分组加括号,这是工程上的成熟做法。
七、常见误区与注意点
WARNING
==比较引用类型内容:对String/对象,==比地址。比内容用.equals();Integer超 127 缓存失效后==也为false。- 混淆
=与==:if (x = 5)是赋值(表达式值为 5,在布尔上下文中恒真或报错),应为if (x == 5)。可写成if (5 == x)让编译器帮你防这种笔误。 - 自增前后置用错:
i++与++i在独立语句里等价,但在表达式里结果不同;复杂表达式里避免混用。 - 用
&/|当逻辑运算:丢失短路保护,可能 NPE/除零;逻辑判断用&&/||。 - 忽视
>>与>>>区别:负数右移时符号位处理不同,涉及无符号语义务必用>>>。
八、最佳实践
- 关系比较基本类型用
==,比较对象内容用.equals()。 - 复杂条件加括号,不依赖记忆中的优先级表。
- 善用短路:把「可能为 null / 除零 / 越界」的判断放左侧,用
&&/||保安全。 - 三元表达式只用于简单的二选一;逻辑一长就拆成
if-else,可读性优先。 - 位运算处理标志位/掩码时优先,普通算术别为了「炫技」用移位代替乘除以致难读。
📚 官方文档与参考资料
- Java SE 21 文档:https://docs.oracle.com/en/java/javase/21/
- Java 语言规范 · 运算符:https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html
- OpenJDK 项目:https://openjdk.org/
✏️ 练习与自测
- 预测
int a=5, b=++a, c=a++;执行后a、b、c的值,再用javap -c验证++a与a++字节码顺序差异(先iinc还是先iload)。 - 用三元表达式写
int max(int x, int y)返回较大值(提示:(x > y) ? x : y)。 - 解释
x != null && x.length() > 0的短路作用:若把顺序写成x.length() > 0 && x != null,输入x = null时会发生什么? - 写出
int m = 12;(二进制1100)左移 2 位和右移 2 位的结果,并说明左移溢出的风险(提示:符号位)。
本节小结
运算符分算术、关系、逻辑、位运算等。重点记住:整数除法向零截断、i++/++i 取值时机不同(== 对基本类型比值、对引用类型比地址,内容比较用 .equals)、&/| 不短路而 &&/|| 短路(字节码用条件跳转跳过右侧,既能提速又能防 NPE/除零)、位运算直接操作二进制(注意 >> 与 >>> 符号位差异)。复杂表达式一律加括号,别和优先级表较劲。