第002篇 运算符与表达式:整除、短路、位运算与优先级

简介: Android面试高频考点:运算符与表达式。聚焦整除截断、溢出规避、短路求值、位运算(MeasureSpec/Intent flags)、浮点比较、优先级陷阱等真实坑点,结合源码案例讲透原理与避坑实践,助你夯实基础、展现真功夫。

在 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:分支逻辑规范写法

有任何问题欢迎在评论区留言交流。

相关文章
|
1天前
|
缓存 编译器 API
第119篇 扩展属性与扩展伴生:工具方法的进阶形态
本节详解Kotlin扩展属性与伴生对象扩展:扩展属性无backing field,本质是静态getter/setter方法,每次访问均重计算;扩展伴生对象则为第三方类“添加静态方法”,语法简洁、语义清晰。二者均属编译期静态分派,不可覆盖,是检验Kotlin底层理解的关键点。
19 2
|
1天前
|
传感器 API Android开发
第121篇 Activity 生命周期全解:七个回调的成对关系
本文深入解析Activity生命周期的两大维度:视图生命周期(6个回调)与进程生命周期(状态保存/恢复),直击面试高频考点——“为何如此设计”及“进程被杀后如何恢复”。厘清`onSaveInstanceState`触发条件、Bundle大小限制、ViewModel与状态保存的边界,并给出工程最佳实践与避坑指南。
20 1
|
1天前
|
Android开发
第128篇Service 生命周期:started 与 bound 两条线
Service本质无“运行中/已停止”状态,仅分启动模式(startService)与绑定模式(bindService)。前者靠onStartCommand响应,支持START_STICKY自动重启;后者依赖onBind/onUnbind,绑定全解即销毁。核心原则:不操作UI、不耗时、不持Activity引用,状态交由UI层订阅。
23 1
|
1天前
|
存储 缓存 安全
第099篇 属性委托实战:SharedPreferences 的现代写法
本文详解Kotlin属性委托的实战应用:何时自研委托(满足SP/DataStore存储、读写加工、多处复用三条件之一)、如何落地(内存缓存、加密、过期校验、key动态派生),并避坑SP全量加载、主线程commit、key硬编码等问题。强调委托核心价值——将状态读写策略从业务代码中解耦抽离。
15 0
|
1天前
|
缓存 Java API
第107篇 协程与线程性能对比:为什么更轻
本文深入剖析协程性能误区:协程“轻量”仅指创建/切换成本低(纳秒级、用户态),而非单任务执行更快;它优化的是高并发I/O场景的资源占用,而非CPU算力。关键区分协程(挂起单元)与线程(调度单位),并指出三大常见坑:误信协程能加速阻塞调用、盲目用于CPU密集型任务、过度细粒度切换。
18 0
|
1天前
|
安全 Java 编译器
第103篇 DSL 构建原理:type-safe builder 如何工作
Kotlin DSL 并非魔法,本质是三大特性协同:带接收者 Lambda(`T.() -&gt; Unit`)提供隐式 `this`、扩展函数封装配置逻辑、尾随语法提升可读性。编译后即普通方法调用,零运行时开销;配合 `@DslMarker` 可实现严格作用域隔离,保障类型安全与工程健壮性。
15 0
|
1天前
|
Android开发
第124篇Fragment 生命周期:与 Activity 的联动陷阱
Fragment生命周期含三套独立流程:实例、视图、上下文。关键分界是`onCreateView`至`onDestroyView`为视图生命周期,此时Fragment实例仍存活;`onDestroy`后实例才真正销毁。常见坑如重复弹窗、内存泄漏,根源在于混淆三者时机。推荐实践:视图相关操作绑定`viewLifecycleOwner`,数据交由`ViewModel`管理。
17 0
|
2天前
|
安全 Java API
第027篇 ArrayList 源码与扩容机制:1.5 倍增长的细节
ArrayList面试高频题:JDK8中无参构造不分配数组,首次add才扩容至10;后续按1.5倍增长,不足时直取所需容量,上限为Integer.MAX_VALUE-8。扩容本质是Arrays.copyOf整段拷贝。关键要懂原理、避坑(如预估容量、非线程安全、subList陷阱)并结合工程实践。
21 0
|
2天前
|
安全 Java 编译器
第024篇 泛型基础:类型擦除到底擦了什么
本文深入剖析Java泛型本质:以“问题—擦除—运行时残留—错误表现”四步公式讲清原理;透彻解析类型擦除机制、桥接方法及常见坑(raw type、new T[]、instanceof泛型);结合可运行代码与Android实战场景,助你面试答出“为什么”,而非仅“怎么写”。
25 0
|
1天前
|
缓存 Java 数据库
第078篇 Sequence 与惰性求值:大数据量集合优化
`Sequence` 的核心是**惰性求值**(操作延迟至终端才执行)与**冷流语义**(每次遍历都重新计算,不缓存)。它省内存(无中间集合),但不支持多次遍历、随机访问;适用于大集合多级过滤或无限序列截断(如 `take`)。慎用于副作用操作、未截断的无限流及需重复消费场景——应物化(`toList()`)或内联使用。
32 0

热门文章

最新文章