1. 先搞懂运算符与表达式在Java中的位置:这块基础为什么没人敢跳过
很多新手学Java时有个错觉:运算符不就加减乘除嘛,没啥好学的。真到了写代码的时候才发现,一个表达式能把人绕晕的永远不是加减乘除,而是那堆看起来不起眼的双等号、三元问号、位运算符号,以及编译器默默做的类型提升。你去看Java面试题,随便翻一份基础卷,几乎有一半的题都在拿运算符和表达式做文章,比如i++和++i的区别、&&和&的差别、a = b = c到底怎么赋值,甚至有些公司笔试里直接让手算表达式求值。
为什么这些基础题这么高频?因为运算符和表达式是描述“程序行为”的最小语言单元。你写业务逻辑,本质就是在写一堆赋值、比较、判断、拼接,而这些全部要靠运算符串起来。你可以不记得某个集合类的API,但不能不理解int x = a > b ? a : b这句话在JVM层面到底发生了什么。这个东西如果理解不透,后面学分支、循环、方法重载、泛型、Lambda表达式都会卡壳,因为那些语法里到处都藏着表达式的影子。
这篇内容适合谁看?两类人。一类是刚开始学Java、对运算符停留在“会算”层面的初学者,我帮你把底层原理和易错点一次讲透;另一类是准备校招或者跳槽面试的开发者,我把面试里经常出现的运算符与表达式题目、坑点、排查思路整理成了速查结构,你按着过一遍,基本不会在基础题上丢分。
我自己带过不少新人,也看过不少线上代码,发现很多真实故障并不是复杂架构导致的,而是一个表达式写飘了:比如位运算优先级算错导致条件判断失效、i++在同一个表达式里被用了两次导致结果跟预期完全对不上。所以这篇不是干巴巴地列运算符表,而是把“为什么这么设计”“实际用的时候容易踩什么坑”“线上排查怎么看这类问题”一起讲清楚。
2. 运算符分类逐个拆:从算术到位运算的一张全景表
2.1 先建立一个整体框架:Java里到底有哪些运算符
Java里的运算符按功能来分,大致有这几类:算术运算符、关系运算符、逻辑运算符、赋值运算符、三元运算符、位运算符,还有一个经常被忽略的字符串连接运算符+。很多人数不全,是因为平时只用了其中一半,另一半只在特定场景出现,比如做状态位取值、做权限掩码、做hash散列的时候才碰到位运算。
我建议你把运算符当成一个“工具箱”来看,每个工具解决一类问题。算术运算符解决数值计算,关系运算符解决大小比较,逻辑运算符解决多个条件组合,赋值运算符解决变量存储,三元运算符解决简单的二选一,位运算符解决二进制层面的操作。用这个框架去记忆,比死背符号表靠谱得多。
下面这张表是全景总览,建议新手先看个眼熟,后面每一类我都会拆开细讲:
| 分类 | 符号 | 作用简述 | 典型用法 |
|---|---|---|---|
| 算术 | + - * / % | 数学计算 | a + b、a % b |
| 自增自减 | ++ -- | 变量加一或减一 | i++、--i |
| 关系 | > < >= <= == != | 比较大小或相等 | a > b、x == y |
| 逻辑 | && || ! | 布尔逻辑组合 | a && b、!flag |
| 短路与 | & | ^ | 布尔或整数位运算 | flag & mask |
| 赋值 | = += -= *= /= %= | 变量赋值 | a += 5 |
| 三元 | ? : | 条件二选一 | a > b ? a : b |
| 位运算 | << >> >>> ~ & | ^ | 二进制偏移与位操作 | value << 2 |
| 字符串拼接 | + | 连接字符串 | "hello" + name |
有一点要特别提醒:关系运算符里最容易写错的是==。在实际开发中,基本类型直接用==比较没问题,但两个对象用==比较的是引用地址,不是内容。很多初学者用str1 == str2去判断两个字符串是否相等,结果一会儿对一会儿错,原因就是字符串常量池和对象引用在作祟。判断字符串内容相等,老老实实用equals方法,这是面试高频考点,也是线上bug重灾区。
2.2 算术运算符和取余:负数和浮点数的两个隐藏细节
算术运算符大家都会用,但有两个细节值得单独拿出来讲。
第一个是取余运算%的符号问题。Java的取余运算结果的符号和被除数(左边的数)保持一致,而不是跟除数一致。比如-7 % 3的结果是-1,7 % -3的结果是1。为什么要强调这个?因为有些业务里需要判断两个数是否整除,或者算某个数在数组里的循环下标,如果没考虑负数情况,很容易出现越界或者结果错误。比如:
int index = -7 % 3; // 结果是 -1,而不是 2这在做循环队列、哈希散列的时候特别容易踩坑。如果希望无论如何都得到非负余数,通常需要自己做修正,例如((n % m) + m) % m这种写法,就是为了避免负余数影响逻辑。
第二个是浮点数的运算精度问题。0.1 + 0.2在Java里并不等于0.3,而是0.30000000000000004。这个坑无数人踩过,原因是浮点数在计算机里是按二进制科学计数法存储的,十进制小数转二进制很多时候是无限循环小数,计算机只能截断存储,于是产生了误差。如果你在写金额计算、百分比计算,一定要记住:浮点数不精确,不要直接用==比较浮点结果,也不要依赖浮点运算做精确的金额累计。面试时如果被问到“浮点数如何比较”,标准回答是先设定一个误差范围,比如Math.abs(a - b) < 1e-9,或者使用BigDecimal。
2.3 自增自减:++i和i++不是只差一个位置
i++和++i是Java里辨识度极高又极其容易出错的点。先说结论:i++是先使用i的当前值,再把i加1;++i是先把i加1,再使用新值。单独写一行的时候,两者没区别,但放在表达式里就完全不同了。
int a = 5; int b = a++; // b = 5,a = 6 int c = ++a; // a = 7,c = 7这段代码里,b取到的是a自增之前的5,然后a才变成6;到了c这里,a先变成7,再把7赋给c。这个顺序如果理解错了,写循环、写数组下标、写计数器的时候都会出现莫名其妙的“差一”。
线上最常见的一个坑是list.get(i++)和list.get(++i)混用。前者是先取出当前元素再移动下标,后者是先移动下标再取元素,只要逻辑一混淆,数据顺序就乱了。另外,多个自增在同一个表达式里出现时,代码会变得几乎不可读,比如int result = i++ + i++ + ++i,我劝大家不要在生产代码里写这种表达式,因为你很难一眼看出结果,而且不同JDK版本对这类表达式的求值顺序虽然有一致规范,但读代码的人是很容易看晕的。工作中保持表达式的简单和明确,比炫技重要得多。
2.4 逻辑运算符:短路的含义,不是所有课程都讲透了
逻辑运算符&&、||和!是写条件判断的主力。它们有对应的位运算版&和|,看起来只差一个字符,行为却差很多。关键区别就在“短路”上。
&&的短路含义是:如果左边已经能确定整个表达式为false,那么右边的表达式根本不会执行。||同理,左边是true,整个表达式已经为true,右边也不会执行。举个例子:
String s = null; if (s != null && s.length() > 0) { // 安全访问,s为空时不会执行 s.length() }这里s != null先执行,如果s是null,结果为false,整个条件短路,后面的s.length()根本不会调用,也就不会抛空指针。这是Java写判空条件的经典写法。如果你把&&换成&,逻辑含义虽然相似,但不会短路,s.length()照样会执行,空指针直接炸。所以实际开发中,判断条件几乎只用&&和||,位运算版&和|则用在布尔位掩码的场合。
面试题里有个经典陷阱:if (a || (b++ > 5))和if (a | (b++ > 5))的区别。前者如果a为true,b++不会执行;后者不管a是什么,b++都会执行。这种题不是在考你会不会计算,而是在考你对“求值时机”的理解。我见过网上有段子调侃:程序员半夜被叫起来修复生产事故,发现是&&写成了&,虽说是段子,但现实中这类错误导致的空指针罚单确实不少。
2.5 三元运算符和赋值运算符:代码简洁的代价是什么
三元运算符? :本质是“单行if-else”。a > b ? a : b的意思就是如果a > b成立就返回a,否则返回b。它最大的价值是让简单选择变得简练,比如求最大值:
int max = x > y ? x : y;而不是写三行if-else。但三元运算符有个典型坑点:冒号两边如果一个是基本类型一个是包装类型,或者一个是int一个是double,结果会按照二进制数值提升规则自动转换,很容易出现“你以为返回的是包装类,结果拿到了另一个类型”的情况。
我举个例子,Object result = condition ? 1 : 2.0,因为一个是Integer一个是Double,最终结果会被提升成Double。这种情况下,如果后续代码按整数来强转,就会直接报类型转换异常。所以使用三元运算符,尤其是两边类型不一致时,一定要留意结果类型。
赋值运算符的坑要少一些,但有几个点值得说。+=``-=这类复合赋值运算符,其实隐含着一次强转。比如short s = 1; s += 1;是合法的,因为+=自带了强制转换,而s = s + 1;却会编译报错,因为s + 1结果是int,不能自动赋给short。这个细节面试里也经常考。
2.6 位运算:权限处理、标志位组合里的瑞士军刀
位运算符包括<<、>>、>>>、&、|、^、~。日常业务代码里用得少,但一旦涉及高性能计算、网络协议解析、状态位合并,位运算是不可替代的。
先理解几个基本概念。<< n相当于乘以2的n次方,>> n相当于除以2的n次方,但要注意,右移分两种:>>是有符号右移,最高位补符号位;>>>是无符号右移,最高位补0。这个区别在处理负数时特别明显:
int a = -8; int b = a >> 1; // 结果是 -4,符号位保留 int c = a >>> 1; // 结果是很大的正数,因为最高位补0面试题里让你手写判断一个整数是不是2的幂次方,标准解法和位运算强相关:n > 0 && (n & (n - 1)) == 0。原理很简单,2的幂次方的二进制表示只有一个1,减一之后和原数做按位与,会把唯一的1消掉,结果一定是0。这种解法比循环除以2快得多,而且很直观。
我再举个例子,用位运算做权限掩码。假设一个系统有读、写、执行三种权限,可以用三个二进制位表示:读1、写2、执行4。组合权限就是按位或,比如同时给读和写权限是int perm = 1 | 2;也就是3,判断是否有写权限就是(perm & 2) != 0。这种写法的好处是把多个布尔状态塞进一个int里,在数据库里存一个字段就能搞定,比建三张关联表高效得多。我第一次看到项目里这么处理权限时也觉得复杂,真正跑起来才发现,位运算在大批量权限判断场景下性能优势很明显。
注意:位运算的优先级比关系运算符低,比逻辑运算符高,具体优先级我放到下一章统一讲。如果你拿不准,写位运算表达式时多用括号包起来。代码可读性永远排在第一位。
3. 优先级与结合性:表达式求值顺序里最容易翻车的3个坑
3.1 优先级速记法:一张表记住不用背
Java运算符优先级一直是让人头疼的部分。其实不需要背全表,只需要记住几个关键层级就能覆盖绝大多数场景。我把从高到低的顺序简化为下面这张速记表:
| 优先级 | 类别 | 符号 | 说明 |
|---|---|---|---|
| 高 | 后缀 | expr++、expr-- | 自增自减后缀 |
| 一元 | ++expr、--expr、!、~、+expr、-expr | 强制转换等 | |
| 乘法 | * / % | 算术乘除 | |
| 加法 | + - | 算术加减、字符串拼接 | |
| 移位 | << >> >>> | 位偏移 | |
| 关系 | < > <= >= instanceof | 大小比较 | |
| 相等 | == != | 相等比较 | |
| 按位与 | & | 位与 | |
| 按位异或 | ^ | 位异或 | |
| 按位或 | | | 位或 | |
| 逻辑与 | && | 短路与 | |
| 逻辑或 | || | 短路或 | |
| 三元 | ? : | 条件 | |
| 低 | 赋值 | = += -= *= /= %= ... | 赋值 |
记住几个关键优先级关系就行:自增和一元运算最高,然后是乘除加减,然后是关系比较,然后是相等判断,然后是位运算,最后是逻辑运算、三元和赋值。日常写代码碰到不确定的优先级,直接加括号是最稳妥的,别赌记忆,也别总希望别人“能看懂你的简洁”。
3.2 优先级翻车案例一:&和==到底谁先算
我见过一次真实的事故排查,像这样的一行代码:
if (flags & MASK == 1) { // 期望判断 flags 与 MASK 按位与后是否等于1 }这行代码在Java里会怎么执行?因为==的优先级高于&,所以实际编译出来是flags & (MASK == 1),也就是先比较MASK == 1得到一个布尔值,再用这个布尔值和flags做按位与。布尔值参与位运算时会被转成1或0,最终的判断结果和你预期的完全不同。这段代码能编译通过,但逻辑完全错了,而且错得极隐蔽。正确的写法是:
if ((flags & MASK) == 1) { // 先做按位与,再比较结果 }这个例子充分说明:位运算的优先级比很多人直觉中的更低,尤其是和==混用时,括号不是可选项,是必须项。我在排查这种问题时的经验是,先看表达式里有没有混合不同类别的运算符,如果有,就先把优先级表翻出来对照一遍,不要凭“我印象里是先算这个”去猜。
3.3 优先级翻车案例二:三目运算符和赋值的纠缠
三目运算符的优先级比赋值运算符高,但比逻辑或低。同一个表达式里如果既有?:又有=,很容易让人觉得“这不可能编译通过吧”,但它就是能编译。比如:
int a = condition ? b : c = 10;这种写法是把condition ? b : c的结果赋给a吗?不是。实际解析可能是混乱的。为了不出这种怪代码,我的建议很简单:凡是有三元运算符的表达式,全部用括号把真值分支和假值分支包起来,比如int result = (condition) ? b : c;,确实多两个字符,但换来的是不会有人纠结这行代码到底怎么算。
3.4 结合性与表达式求值顺序:左侧先行还是右侧先行
优先级决定的是先算谁,而结合性决定的是同优先级下从左算还是从右算。Java大部分运算符是左结合,也就是从左往右算。赋值运算符是右结合,所以a = b = c会把c赋给b,再把b的值赋给a。
但有两点要分清:“结合性”和“求值顺序”不是一回事。Java保证的是,一个表达式中的子表达式,计算结果通常是先算左边再算右边,除了赋值和三元等少数情况。比如int x = a() + b();,Java规范保证先调用a()再调用b(),即使a()和b()的顺序互换不影响最终加法结果,但这决定了“副作用”的发生顺序。网上有一段经典例子:
int i = 0; int result = i++ + i++;这里先取左边的i为0参与加法,i变成1,再取右边的i为1参与加法,i变成2,最终结果是1。如果反过来先算右边,结果就是2。这种差异面试官特别喜欢拿来考。我的建议是,遇到对执行顺序敏感的自增写法,就把它拆成多行写,别让阅读代码的人去猜。
4. 表达式求值过程:类型提升、赋值细节和那些反向踩坑的写法
4.1 二元运算之前,类型悄悄发生的自动提升
Java表达式求值时会先做类型提升。简单来说,两个不同类型的值做运算,小的类型会自动转成大的类型,比如int + long会先把int转成long再相加,结果类型是long。float + double会把float转成double,结果是double。但short和short相加却会被提升成int,结果也是int,这是Java规范里容易被忽略的一条规则:任何凡是可以转成int的操作数,都会先转成int再做整型运算。
这个规则带来的后果很直接:
short a = 1; short b = 2; short c = a + b; // 编译报错,a + b 结果是 int short d = (short) (a + b); // 必须强转所以很多初学者在写short、byte、char类型相加时报错时,第一反应是“不是很正常的加法吗”,实际上这是类型提升在起作用。理解这个机制,对排查编译错误很有帮助。面试题里经常出现的“byte + byte结果是什么类型?”答案就是int。
4.2 字符串连接符+:唯一一个能“重载”的运算符
Java不支持运算符重载,但+是个例外。它在遇到字符串时会变成连接操作。这个“特殊性”带来一个老生常谈又必备的知识点:字符串拼接时,左右两边只要有一边是字符串,+就会把另一边转成字符串。
常见面试题这样出:
System.out.println(1 + 2 + "3"); // 输出 "33" System.out.println("1" + 2 + 3); // 输出 "123"第一行先算1 + 2得到3,再拼接字符"3",变成"33"。第二行先遇到字符串"1",然后+ 2变成"12",再+ 3变成"123"。结果差异的根源就是:+是左结合的,而且“遇到字符串就转成字符串拼接”这件事是从左到右发生的。
实际开发中,字符串拼接大量存在,尤其是在日志输出、SQL拼接、动态提示信息场景。性能上要注意:如果在一个循环里反复用+拼字符串,会产生大量临时对象,推荐用StringBuilder或StringBuffer来代替。不过单次的、几行的拼接直接用+完全没问题,可读性还好,不必为了所谓的“性能”把所有+都改成StringBuilder,那属于过度优化。
4.3 复合赋值运算符:自带强转,也可能埋坑
复合赋值运算符包括+=、-=、*=、/=、%=等。刚才提过short s = 1; s += 1;能编译而s = s + 1;不能编译,原因是复合赋值运算符会隐式强转。
这个隐式强转在某些场景下反而会带来意想不到的结果。比如:
int a = 200; a += 100; // 结果 300,没问题但如果类型比较窄:
byte b = 100; b += 50; // b = -106,发生了溢出的强转因为b += 50先按int计算得到150,再强转成byte,byte取值范围是-128到127,150直接溢出变成-106。这种代码不报错,但结果完全不符合预期。在处理字节流、解析协议头时,很容易踩这种数值溢出坑。修复方式很简单,遇到可能越界的运算,用更宽的类型存储,并做范围校验。
4.4 三元运算符的类型规则:不匹配时编译器按什么规则转
之前我提到三元运算符的两边类型不一致时会按类型提升规则自动转换。这里深入讲一下。对于condition ? expr1 : expr2,如果expr1和expr2类型不同,编译器会尽量将两者提升到“可以赋给目标类型”的公共类型。
一个高频面试题是这样的:
Object o1 = true ? 1 : 2.0; System.out.println(o1);输出是1.0,因为Integer和Double的公共类型是Double,1被提升成1.0。但如果写Object o2 = true ? 1 : "text";,这时一个是Integer一个是String,它们没有数值提升关系,编译器会把结果类型处理成Number或Serializable等公共父接口,实际运行时o2才是指向Integer的对象。这题考察的就是混合类型三元的类型推导,理解了提升规则,基本不会再错。
这个知识在写通用代码时特别有用,比如写一个工具方法,根据条件返回不同数值类型,如果不注意统一类型,很可能返回数据被隐式改变了。我见过一个报表导出功能,原来返回值类型是Object,某个分支用true ? 1 : 1.0f写,结果所有整数都带了.0,前端展示直接多了小数位。
5. 动手练:用4个小题目验证理解程度,代码直接抄
5.1 题一:写一个判断闰年的表达式,用多运算符组合
不做任何算法优化,就要求你用最普通的表达式把判断写出来。闰年的规则是:能被4整除且不能被100整除,或者能被400整除。用运算符组合翻译就是:
boolean isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);这道题是练习优先级和逻辑运算符搭配的好例子。注意观察我写的括号:把&&的两个子条件放在同一个括号里,再把||两边用括号分开。实际上不加括号也能正确执行,因为&&优先级高于||,但加了括号的意图更清晰。我在代码审查里经常看到类似逻辑,只要不违反语义,我建议把判断部分的括号写全,不要让同事去心算优先级。
5.2 题二:两个变量的交换,不用临时变量
考察位运算和赋值运算符的实际使用。通过异或运算可以直接交换两个整数:
int a = 3; int b = 5; a = a ^ b; b = a ^ b; a = a ^ b;异或交换的原理是:任何一个数和另一个数异或两次会还原。第一步a ^ b存入a,第二步a ^ b实际上变成了原来的a,第三步再异或得到原来的b。这个写法看起来很酷,但在真实项目中不推荐,主要原因是可读性差,而且对于现代CPU来说,用临时变量交换并不慢,反而更安全。面试时如果被问,你能讲出原理就行,实际干活还是用int tmp = a; a = b; b = tmp;。
5.3 题三:用位运算判断一个整数是不是2的幂次方
前面已经给出表达式:n > 0 && (n & (n - 1)) == 0。这里的&&也很有讲究,它保证了当n小于等于0时,不需要执行后面的位运算。如果换成&,当n是0或者负数时,后面的位运算虽然不会抛异常,但结果可能变成1或其它值,判断逻辑就完全失效了。
我自己试过分别用&&和&跑一组数据,n = 0时n > 0 && ...直接得到false,而n > 0 & ...会把0 & -1算出来是0,进而0 == 0为true,于是0被判成2的幂,这显然是错的。所以这道题表面是在考位运算,深一层是在考短路逻辑。
5.4 题四:字符串连接符与算术运算符的优先级实战
把下面的表达式手算出来,再写进代码验证:
int num = 10; String msg = "result is " + (num + 5) + num;输出是result is 1510。过程是这样:第一个+遇到字符串"result is ",所以num + 5因为有括号提前算出15,拼接成"result is 15",紧接着再和num也就是10拼接,得到"result is 1510"。注意如果我把括号去掉,写成"result is " + num + 5 + num,因为+从左到右结合,会变成"result is " + 10得到"result is 10",再+5得到"result is 105",再+num得到"result is 10510",结果完全不同。这就是求值顺序和运算符优先级的综合体现。
6. 面试高频题与排查速查表:背会这些心里就有底
6.1 高频面试题汇总
这里整理了我收集到的,以及我在面试别人时喜欢问的运算符与表达式相关题目,全是基础高频,但答全对的候选人比例并不高。
第一题:int i = 1; int j = i++ + ++i;问j是多少。答案是:i++先取1参与加法,i变成2,++i先变成3再参与加法,所以1 + 3 = 4,i最终是3。这道题考的是自增前后缀的执行顺序。
第二题:&&和&的区别。一要答出短路性,二要答出应用场景:短路适合判空保护、条件前置,&适合位掩码。注意&同时也可以作用于整数做位运算,而&&只能作用于布尔值。
第三题:short和short相加结果是什么类型。答案是int。因为Java会把小于int的类型自动提升到int再计算。这个知识点在写文件解析、协议解析代码时特别重要,因为整个字节流里经常是short和byte在运算。
第四题:"a" + 1 + 2输出什么。答案是"a12"。如果要得到"a3",得写"a" + (1 + 2)。这题只需要30秒就能回答,但能区分出你是否真的理解字符串拼接的执行顺序。
第五题:2 << 3等于多少。答案是16。本质是2乘以2的三次方。
第六题:如何在不使用Math库的情况下判断一个数是不是奇数。正确写法是(n & 1) == 1。但要注意负数,Java里负奇数的低位也是1,所以对于负数这样判断也正确。如果用n % 2 == 1判断负奇数会出错,因为-3 % 2等于-1,和1比较为false。很多人的代码里用n % 2 == 1判断奇数,在负数场景下会漏判,这是我见过的一个真实线上bug。
6.2 线上排查式问题:看一行表达式知道错在哪里
除了背题,实际工作中更看重排查能力。我总结过几个典型的“表达式定位法”,写在这里供参考。
第一类:空指针。看到if (a != null && a.getX() > 0)这种等于是标准安全写法。如果排查时空指针还发生在&&后面,那大概率不是表达式问题,而是前面的条件逻辑本身不成立,或者有人把&&误写成&。
第二类:边界溢出。如果一段计算金额的逻辑偶尔出现巨大负数,先检查参与运算的变量类型是不是int,再看有没有复合赋值隐式强转。比如total += price * count;里如果price * count已经超过int范围,赋值给更大的total类型也不能避免中间结果的溢出,解决办法是把其中一项先转成long,比如total += (long) price * count;。
第三类:比较失败。字符串内容比较用了==,或者浮点数比较用了==,是两类典型的比较失败原因。排查思路很简单:先看比较的对象是什么类型,再决定用什么运算符。基本类型且没有精度问题用==,对象内容用equals,浮点用误差范围。
6.3 避坑技巧速查表:我踩过之后总结的几条经验
下面的速查表是长期实操攒出来的,照着做能规避大部分运算符相关的问题:
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 判空后访问属性 | obj != null && obj.getX() > 0 | obj.getX() > 0 && obj != null |
| 整数除2或乘2 | n << 1(面试可用) | 业务代码里直接用移位做乘除(可读性差) |
| 字符串内容比较 | str.equals(target) | str == target |
| 金额计算 | BigDecimal或long按分存储 | double直接累加 |
| 复合赋值越界 | 用更宽的类型先计算再赋值 | 直接依赖隐式强转 |
| 不确定优先级 | 加括号 | 赌编译器行为和同事的理解 |
| 多个自增在同一表达式 | 拆成多行 | 写i++ + i++ + ++i这种炫技代码 |
| 位运算混入比较 | (flags & MASK) == 1 | flags & MASK == 1 |
6.4 一点个人经验:把表达式当作可读性契约来写
最后分享一个我自己的习惯。我写代码时,会把表达式当成一份“可读性契约”:凡是自己看一眼不能立刻说出执行结果的表达式,就立即拆开重写。这不是说我不信任自己对运算符的理解,而是说代码最终是给人读的,不能为了少写两行让后面维护的人猜谜。
写项目三年以上的同学应该都有体会,真正让人头疼的永远不是if、else逻辑,而是那些看起来“很简洁”但实际要求读者大脑预编译的表达式。反过来,如果你能在代码审查里敏锐指出某个表达式因为优先级问题会算错,在面试里能清楚解释i++和++i的字节码差异,这些才是把“基础”学透之后带来的长期红利。运算符与表达式这套东西,花一个下午认真过一遍,值回票价。