在 Android 面试里,运算符与表达式是紧跟在基础类型之后的第二组高频考题。一道「5 / 2 等于几」,就能让不少候选人露出破绽——有人脱口而出 2.5,被指出不对之后还很委屈:「这不是小学数学吗?」
这个回答的问题在于:把 Java 的运算符当成了数学课本,没搞清楚整数除法的截断规则。这类情况在 Android 面试里太常见了——很多工程师自认为懂了,但面试官追问到细节时才发现根本没吃透。这篇就把这些坑一次讲透,每一处都对应一道真实的高频考题。
算术运算符:不只是加减乘除那么简单
加减乘除谁都会,但在 Java 里,两个 int 相除,结果还是 int,小数部分直接砍掉。5 / 2 的结果是 2,不是 2.5;想要 2.5,至少一边得是浮点数,5.0 / 2 或者 (double) a / b 都行。
别小看这条规则。一个流传很广的案例:某实习生写下载进度条,progress = done / total * 100。done 是 3,total 是 7,期待的结果是 42,实际结果是 0——因为 3 / 7 先整除得 0,0 再乘 100 还是 0。正确写法是 done * 100 / total,先乘后除。进度条停在 0 的 bug,排查了半天,根因就一行。
比截断更隐蔽的是溢出。求两个数的平均值,很多人顺手写 (a + b) / 2。当 a 和 b 都接近 int 上限时,a + b 先溢出,结果直接变成负数。经典修复是 a + (b - a) / 2,先减后除,规避掉中间结果越界。面试官让候选人手写二分查找,mid 那一行怎么写,很大程度就是在看有没有这个意识——二分写对了,是基本功;写错了,说明之前都是复制粘贴过来的。
还有一个隐藏强转的细节:byte b = 127; b += 1 编译通过,b 变成 -128;但 b = b + 1 编译报错,因为 b + 1 已经提升为 int,装不回 byte。复合赋值运算符里藏着一次自动强转,这是面试官爱考的「反直觉」题,答上来就能体现对类型提升的真理解。
顺手说一个兄弟问题:除零。整数除以 0 会直接抛 ArithmeticException;浮点除以 0 却不抛异常,而是得到 Infinity 或者 NaN。NaN 更阴险——它跟任何值比较都返回 false,包括跟它自己比。判断一个浮点结果是不是 NaN,要用 Double.isNaN(),不能写 == Double.NaN。线上数值计算一旦出了 NaN,会顺着链路一路传染,最后界面显示一串诡异的数,回头排查,源头往往就是一次不起眼的零除。
关系与逻辑运算符:短路求值能救你的命
a == b 看起来再简单不过,但拿来比较浮点数,很可能出 bug。动画插值算出来的进度是 0.9999999,拿它和 1.0 画等号,不会相等。拖拽吸附的坐标判断同理——理论上该重合的两个浮点数,实际可能差在小数点后七八位。
正确的做法是判断差值是否小于一个很小的数:Math.abs(a - b) < 1e-6。epsilon 取多大取决于场景,普通 UI 判断 1e-6 基本够用,高精度计算还要再收紧。JDK 也提供了 Double.compare(a, b),纯粹比较大小而不是判等的时候,用它比手写差值语义更清晰。面试时能主动提到浮点比较这个坑,面试官心里会默默加分——这说明候选人写过真正会出误差的代码。
&& 和 || 的短路求值,是另一件「天天在用却没细想」的事。if (view != null && view.isAttachedToWindow())——view 是 null 的时候,前半句已经判 false,后半句根本不会执行,空指针就这样被挡在了门外。要是把条件写反,或者手滑写成非短路的 &,后半句照样执行,程序当场崩溃。
不少团队的新人都踩过这个坑:把条件写成了 view.isAttachedToWindow() && view != null,单测偶发崩溃,查了两天才发现是顺序问题。面试官问「短路求值有什么好处」,把这个例子讲出来——先判空、再解引用,短路求值就是天然的空指针防线——比背一百遍「可以提高效率」都强。
位运算符:Android 源码里的常客
&、|、^、~、<<、>>,这些符号在业务代码里出场率不高,但在 Android 源码里遍地都是。讲一个最经典的:View 的 MeasureSpec。
测量一个 View 的时候,模式和尺寸被合并装进一个 int:高两位是模式,低三十位是尺寸。取模式是 spec & 0x3,取尺寸是 spec & ~0x3;反过来装包,就是 (mode & 0x3) | (size & ~0x3)。一套组合全是位运算。面试讲自定义 View 的测量流程,MeasureSpec 的拆包与装包绕不开,本质上就是在考位运算——这也是为什么位运算被称为「源码里的常客」:不认识它,源码就读不下去。
Intent 的 flags 也是同一套路。FLAG_ACTIVITY_NEW_TASK 这些标志位,用 | 添加,用 (flags & FLAG) != 0 检查,用 flags & ~FLAG 清除。天天调用的 startActivity 传参,底层就是这些位操作。
再送两个实用技巧:n & 1 判断奇偶,比取模更「懂行」;n & (n - 1) == 0 判断一个数是不是 2 的幂,原理是 2 的幂在二进制里只有一个 1,减一之后借位,两者相与必为 0。还有一道高频追问:HashMap 定位下标为什么写 (n - 1) & hash 而不是 hash % n?因为 n 是 2 的幂时两者等价,而位运算快得多。这题答上来,面试官就知道候选人真读过源码。
最后提一个容易混的:>> 和 >>>。负数在计算机里按补码存储,>> 右移补符号位,负数右移完还是负数;>>> 是无符号右移,高位一律补 0。拿 -8 做实验,两个运算符的结果完全不同。这是位运算部分的经典收尾题,也是区分「背过」和「算过」的分水岭。
运算符优先级:那些容易被忽略的细节
a + b * c,先算哪个?小学生都知道先算乘法。但 Java 里运算符有十几个优先级,没有人能全记住。所以业界的共识是:涉及不同运算符组合,一律加括号。
有一个必须记住:== 的优先级比 & 高。判断标志位,if (flags & FLAG == FLAG) 实际执行的是 flags & (FLAG == FLAG)。FLAG == FLAG 恒为 true,也就是 1,整个表达式变成 flags & 1——只有 flags 的最低位是 1 时条件才成立,跟本意八竿子打不着。
一个真实的排查案例:某团队判断某个状态位是否设置,条件不成立,功能表现为「偶发失效」,排查了整整一天。代码能编译,逻辑上念起来也通顺,就是结果不对——本来想写的是 (flags & FLAG) == FLAG。这类 bug 特别隐蔽,因为编译器不报错、代码评审也容易放过。所以工程实践中的建议始终是:不要依赖优先级,多加括号。代码是给人看的,不是给编译器看的,写得清楚一点,后面维护的人——包括三个月后的自己——会感谢当初的写代码的人。
自增自减也顺带说一句:i++ 表达式的值是自增前的旧值,++i 取新值。int x = 5; int y = x++ + 1,y 是 6,x 也是 6。面试爱问,但工程实践里不建议把自增嵌进复杂表达式——一行只做一件事,出了问题才好查。
// 002:整除、短路、位运算与优先级,一行一个坑
int progress = done * 100 / total; // 先乘后除,防整除归零
int mid = lo + (hi - lo) / 2; // 防溢出求中点(二分手写必查)
if (view != null && view.isAttached()) {
} // 短路保护,顺序不能反
int mode = spec & 0x3; // MeasureSpec:高 2 位模式
int size = spec & ~0x3; // 低 30 位尺寸
boolean isPow2 = n > 0 && (n & (n - 1)) == 0; // 2 的幂判定
给正在准备面试的工程师几点建议
运算符和表达式,看起来是最基础的东西,但面试里考的恰恰是这些基础。
不少面试官都喜欢在运算符上出题。不是因为难,而是能看出基本功扎不扎实:能把整除陷阱讲清楚,说明写过真正的业务代码;能把短路求值的空指针保护说出场景,说明排查过线上问题;能主动提到优先级的坑,说明踩过坑、长过记性。
怎么复习这部分?把运算符表过一遍,重点看整除截断、隐式强转、浮点比较、短路求值、优先级这五个地方;然后去读 Android 源码里 MeasureSpec 和 Intent flags 的用法,把位运算的「活例子」看熟;最后自己动手写一遍——把二分的 mid 写对,把进度条的比例算对。纸上得来终觉浅,绝知此事要躬行。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:变量与基本类型:整型、浮点与包装类型
下一篇预告:流程控制-if-else-与-switch:分支逻辑规范写法
有任何问题欢迎在评论区留言交流。