
最近在帮团队做代码评审时我发现自己反复在提醒同一个问题很多人不是框架不熟也不是业务逻辑不懂而是在最基础的运算符上栽了跟头。i i到底输出什么和一字之差为什么一个会空指针一个不会%在负数场景下到底取正还是取负这些知识点Java基础教程里都有但真正落到代码里往往变成线上故障和面试卡壳的根源。所以我决定把Java运算符好好“探寻”一遍不是照搬八股文的大表格而是从二进制、类型转换、短路语义和工程实践几个角度串出一条能直接用来排查问题和准备面试的完整思路。这篇的东西覆盖面比较广适合正在看Java面试题的朋友也适合写过一段时间代码、想回头夯实基础的开发者。1. 运算符在Java里到底扮演什么角色——先建立完整认知1.1 从表达式到运算规则运算符如何驱动数据加工运算符是连接操作数的符号1 2里是运算符1和2是操作数三者共同构成一个表达式。很多人觉得这只是语法的一部分其实运算符对应的就是计算机最底层的动作把数据从内存取出来做算术逻辑运算再决定是否写回。Java没有指针也不允许用户自行重载运算符所以它的运算符体系相对C/C更“克制”但也因此更容易被轻视。在JVM层面一条表达式会先被拆成一条条字节码指令比如整数加法对应iadd比较运算对应if_icmpne赋值对应putfield或istore。理解这一点之后再看a和a的区别就会直观很多这两个表达式在字节码里的执行顺序不同导致取到的返回值不同。运算符不是简单符号而是真实的数据加工指令。1.2 Java运算符家族不是只有加减乘除Java的运算符大概可以分成这几类算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、条件运算符还有一个特殊的instanceof。我用一个表格把它们列出来方便对照。分类运算符主要作用算术 - * / % --数学运算、自增自减、取余关系 !比较大小、相等性逻辑 || !布尔逻辑运算支持短路位 | ^ ~ 按位与、或、异或、取反、移位赋值 - * / % | ^赋值和复合赋值三元? :条件表达式其他instanceof类型检查这里有个容易混淆的点、|、^既可以当位运算符操作整数也可以当逻辑运算符操作布尔值。操作数是int时就是按位运算操作数是boolean时就是不短路的逻辑运算。这个双重身份是很多基础差的程序员栽跟头的地方。相比之下和||只能操作布尔值没有位运算语义。1.3 为什么运算符值得专门钻研从面试题到线上故障举个实际的例子我之前在Review时看到有人写了这么一行if (str ! null str.length() 0) { // ... }代码能编译但线上运行时会空指针。原因很简单不短路即使str为null右侧的str.length()依然会执行。这就是把写成的典型后果。类似的故障还有很多比如把位运算的|当成逻辑或的||或者在移位时把优先级算错。面试题里那些“为什么”并不是纸上谈兵它们直接映射到代码的稳定性和可维护性。所以研究和运算符相关的原理本质上是在降低日常写错代码的概率。接下来我从最基础的算术运算符开始逐个拆解那些“反直觉”的细节。2. 算术运算是基础但“反直觉”的细节最多2.1i和i先取值还是先自增一句话说不清自增自减是Java里最常见的运算符之一也是面试中最高频的考点。核心区别就一句话i这个表达式的值是自增之前的值i是自增之后的值。但光知道这句话还不够真正的坑藏在复合表达式里。看这个经典例子int i 0; i i; System.out.println(i); // 输出0很多人不理解为什么结果是0。拆开看i的表达式值是旧值0副作用是让i变成1之后赋值操作把0赋给i所以最终i被覆盖成0。Java语言规范里没有模糊地带这个结果就是确定的。而在C里i i属于未定义行为不同编译器可能给出不同结果这也是把两者对比着考的一个热门方向。再来看一个稍微复杂点的int i 0; int j i i; System.out.println(j); // 输出2求值过程是从左到右的第一个i表达式的值是0此时i变成1第二个i表达式的值是自增后的2此时i变成2两者相加得到2。这个例子能帮人建立“表达式返回值”和“变量当前值”之间的区分建议手动多拆几个练习。2.2 取余运算符%负数取余结果符号看谁取余运算符的坑没有自增那么常见但一旦遇到负数很多人就会懵。Java的规则是a % b的结果符号与被除数a的符号相同。举几个例子就清楚了System.out.println(5 % 2); // 1 System.out.println(-5 % 2); // -1 System.out.println(5 % -2); // 1 System.out.println(-5 % -2); // -1为什么-5 % 2是-1而不是1因为Java里的%是“取余”不是数学意义上的“取模”。它满足公式a (a / b) * b a % b而Java的整数除法是向零截断的所以-5 / 2 -2余数就是-5 - (-2 * 2) -1。如果业务上需要“取模”语义尤其在做环形数组、哈希散列时要注意%可能返回负数。Java在Math类里提供了floorMod例如Math.floorMod(-5, 2)的结果是1结果符号跟除数一致某些场景下比%更合适。2.3 整数除法、除零和溢出一个不留神就算错整数除法默认向零截断也就是说7 / 2等于3-7 / 2等于-3不是-4。这在很多语言里都一样但Java对“除零”的处理有个有趣的区分整数除以0会抛ArithmeticException浮点数除以0不会抛异常结果反而是Infinity或NaN。System.out.println(1 / 0); // ArithmeticException System.out.println(1.0 / 0); // Infinity System.out.println(0.0 / 0); // NaN另一个容易忽略的问题是整数溢出。Java的int和long都是固定位数的超过范围后会回绕比如Integer.MAX_VALUE 1会变成Integer.MIN_VALUE而且不报错。在算法题和数值计算里这种溢出特别隐蔽。如果需要检测溢出可以使用Math.addExact、Math.multiplyExact等方法溢出时会抛异常而不是静默得到一个错误结果。2.4 类型提升为什么3 / 2不是1.5这个知识点几乎每个面试官都爱问但真正写代码时还是有人犯错。Java在二元算术运算中会先把操作数按规则提升如果有double则另一个转为double否则如果有float则转float否则如果有long则转long否则两个操作数都会提升为int。所以System.out.println(3 / 2); // 1整数除法 System.out.println(3 / 2.0); // 1.5因为有double参与 System.out.println(3d / 2); // 1.5类型提升还会影响赋值。比如byte b1 10; byte b2 20; byte b3 b1 b2; // 编译错误因为 b1 b2 结果是int必须显式强转(byte)(b1 b2)才能编译。很多人以为“两个byte相加还是byte”其实JVM里对byte、short、char做算术运算时都会当成int来处理。这种隐性提升在写底层工具、字节流处理时尤为常见值得留心。3. 比较与逻辑运算符为什么区分引用和值、短路和不短路这么关键3.1比较基本类型equals比较内容关系运算符里最常被拎出来考的是。对于基本类型直接比较值对于引用类型比较的是引用地址而不是对象内容。所以String s1 new String(hello); String s2 new String(hello); System.out.println(s1 s2); // false两个对象地址不同 System.out.println(s1.equals(s2)); // true内容相同但这里有个很容易让新手困惑的例外字符串直接赋值会走常量池所以String s3 hello; String s4 hello; System.out.println(s3 s4); // true来自常量池同一对象Integer也有类似机制。Integer的valueOf会缓存-128到127之间的对象因此Integer a 127; Integer b 127; System.out.println(a b); // true命中缓存 Integer c 128; Integer d 128; System.out.println(c d); // false超出缓存范围所以比较对象内容时无脑用equals基本不会错。比较浮点数时更要小心0.1 0.2 0.3在Java里是false因为浮点数无法精确表示某些十进制小数。正确做法是差值绝对值小于某个阈值或者用BigDecimal处理精度敏感场景。3.2和、||和|短路与不短路的本质差异这部分我觉得是运算符里最值得展开的。和||是短路运算符如果左侧已经能够确定整个表达式的值右侧就不会执行。和|用于布尔运算时不短路左右两侧都会执行。一个非常经典的场景if (str ! null str.length() 0) { // 安全str为null时右侧不会执行 } if (str ! null str.length() 0) { // 危险str为null时右侧照样执行空指针 }短路不只是为了避免空指针它还可能影响业务逻辑。比如boolean updated cache ! null cache.put(key, value);如果cache为nullput方法不会被调用但如果写成put就会被调用空值检查形同虚设。所以凡是右侧带有方法调用、赋值等副作用的逻辑表达式一定要想清楚自己是需要短路还是不短路。绝大多数业务场景下短路都是想要的。3.3 三目运算符右结合到底怎么理解三目运算符? :是Java里唯一的三元运算符。它的优先级比较低而且结合性是“从右往左”也就是右结合。来看这个表达式int n condition1 ? 1 : condition2 ? 2 : 3;它等价于int n condition1 ? 1 : (condition2 ? 2 : 3);也就是说先处理冒号右边的条件表达式再作为外层表达式的一部分。这个“从右往左”的规则很多人面试时被问到“三目运算符为什么从右往左”其实就是语言设计时的语法规则条件表达式的: 表达式2部分可以是另一个条件表达式递归地向右展开。三目运算符还有一个人人容易踩的坑类型自动拆箱。看这个Integer a condition ? 1 : null; // 可以null赋给Integer引用 int b condition ? 1 : null; // 运行期可能NPE如果condition为falseint b这一行会尝试把null拆箱成int直接抛空指针。使用三目表达式时两个分支的类型要尽量保持一致否则会因为自动类型转换产生意外行为。3.4 布尔表达式里的空指针和副作用除三目表达式外普通的布尔表达式也可能触发拆箱NPE。比如Boolean condition null; if (condition true) { // 这里会自动拆箱condition为null时NPE }安全写法是用Boolean.TRUE.equals(condition)这样即使condition为null也只会得到false不会抛异常。这个细节在阅读开源框架代码时很常见框架作者往往用Boolean.TRUE.equals(x)来规避拆箱风险。另外逻辑表达式右侧的副作用一定要谨慎。比如boolean result list ! null list.add(item);这里的意图可能是“如果list不为空就把item加进去”。但如果list本来为空右边不会执行item也就不会加进去。这种写法虽然很多人用但可读性很差一旦逻辑复杂了很难排查。我更推荐拆成普通if块把条件判断和副作用分开代码更直白。4. 位运算Java里最被低估的“高性能工具”4.1 按位运算全家福不只是面试才用位运算操作的是二进制的每一位包括按位与、按位或|、按位异或^、按位取反~、左移、带符号右移、无符号右移。这些运算符在面试题和底层框架中高频出现很多人会觉得“现实开发用不到”事实正好相反权限系统、状态压缩、哈希散列、序列号生成、图形处理到处都是位运算的身影。需要特别说明的是位运算符的操作对象是整数。、|、^也能用于boolean但那是逻辑运算不是位运算。~只能对整数使用移位运算符也只能对整数使用。4.2 补码、算术右移和无符号右移符号位怎么处理Java的整数使用补码表示最高位是符号位。这就导致右移要区分两种情况是算术右移移位后高位补的是符号位。负数右移后还是负数。是无符号右移移位后高位统一补0不考虑符号位。看个例子int a -8; System.out.println(a 1); // -4因为高位补1 System.out.println(a 1); // 2147483644因为高位补0-8的二进制是11111111 11111111 11111111 11111000算术右移一位变成11111111 11111111 11111111 11111100也就是-4无符号右移一位变成01111111 11111111 11111111 11111100也就是2147483644。这里还有一个隐藏的坑对byte、short、char做移位运算时系统会先把它们提升为int所以结果可能与直接脑补的不一致。比如对一个byte变量的负数做会先转成int符号扩展后得到32位的结果再赋值回去时容易出错。4.3 位运算的实战场景权限、取模、状态压缩权限系统是一个经典例子。用位值表示权限项public static final int READ 1 0; // 1 public static final int WRITE 1 1; // 2 public static final int EXEC 1 2; // 4合并权限用|判断有没有某个权限用切换权限用^int permission READ | WRITE; // 3可读可写 boolean canRead (permission READ) ! 0; // true boolean canExec (permission EXEC) ! 0; // false这样的好处是一个int就能表示一组布尔开关适合数据库存一个整数、内存做标记位的场景。另一个高频场景是取模。在HashMap源码里定位bucket用的就是static int indexFor(int h, int length) { return h (length - 1); }当length是2的幂时h (length - 1)等价于h % length而且位运算比取模快得多。这就是为什么HashMap的容量总是2的幂次方。理解了这一点再去看集合类源码会顺畅很多。4.4 为什么位运算能优化性能以及工程里怎么克制位运算确实快因为它直接对应CPU的位操作指令一个周期内就能完成而除法、取模往往需要更多周期。在性能敏感的计算场景比如哈希运算、网络协议解析、压缩算法里位运算是常用手段。但性能优化不是滥用位运算的理由。早期我看过一些代码为了炫技把简单的状态判断写成一大串位操作结果后面维护的人根本读不懂改一个需求要花几小时。我的建议是位运算优先用在“语义清晰且约定明确”的场景比如权限位、标志位、算法题如果只是普通的业务判断用可读性更高的方式写。多想想“三个月后的自己能不能看懂”比在无谓的地方提升性能更有价值。5. 赋值、复合赋值和优先级代码的可读性之战5.1 复合赋值隐藏的强制类型转换赋值运算符除了最简单的还有、-、*、/、%、、|、^、移位赋值等一堆复合形式。复合赋值有一个容易被忽略的语义它自带一次强制类型转换。看这个例子short s 1; s 1; // 编译通过 s s 1; // 编译错误为什么s 1可以通过而s s 1不行因为s 1的结果是int直接赋给short需要强转。复合赋值运算符自动帮你做了这个强转等价于s (short)(s 1);所以在使用复合赋值时要清楚它可能悄无声息地截断数据。比如byte b 127; b 1;最终b是-128因为127加1后强转成byte溢出了。这个知识点在面试里考过无数次但更多时候是埋在日常代码里的雷。5.2 优先级表记住这些就够用了运算符优先级决定了一个表达式里谁先计算。Java里常用的优先级从高到低大致可以这样记后缀、一元、乘除、加减、移位、关系、相等、按位与、异或、按位或、逻辑与、逻辑或、三元、赋值。我整理了一个简化表格优先级运算符高() [] . -- ! ~* / % - instanceof !^||? :低 - * / % ...注意Java的优先级和C/C并不完全一致尤其是位运算符和相等运算符的相对位置。所以不要直接照搬其他语言的经验最好先查一下当前语言的优先级表。5.3 几个常见的优先级陷阱优先级最坑的地方在于写了代码却得到完全意外的结果。举个例子int x 1 2 3;因为加号的优先级高于左移所以这个表达式实际上是1 (2 3)结果是32而不是(1 2) 3的7。如果意图是后者必须加括号。再比如int a 5; boolean flag a 1 0;这行代码编译都过不了。因为的优先级高于所以会被解析成a (1 0)也就是int boolean类型不匹配。正确的写法是(a 1) 0。这种低级错误一旦混在复杂条件里排查起来非常痛苦。还有逻辑与、逻辑或的优先级差异的优先级高于||所以a || b c等价于a || (b c)。如果业务上期望(a || b) c不加括号就会出错。5.4 我的代码卫生建议运算符和可读性这几个建议是我自己踩坑后总结的。第一不要依赖记忆中的优先级拿不准就加括号括号不丢人丢人的是线上故障。第二组合逻辑条件尽量不要写得太长超过三个条件就拆成有名字的boolean变量。第三嵌套三目运算符能不用就不用分支逻辑复杂时直接上if-else。第四位运算相关的魔法数字一定要用常量命名否则后续维护的人完全不知道0x10是什么意思。这些原则看起来很简单但真正写代码时人们往往图一时方便最后付出更大的排查成本。6. 面试高频运算符考点拆解从八股到变体6.1 为什么i i在Java里是0在C里可能不是这个在上文详细拆过算是Java特定的“规定动作”。简单复述i的表达式值先被保存为旧值然后变量自增最后赋值操作把旧值写回变量所以最终结果是0。而C中同一个表达式是未定义行为语言标准不保证结果。这个差异经常被拿来考察应聘者是否真正理解Java的求值顺序而不是背结论。6.2Integer用比较为什么会“时灵时不灵”这题的背后是Integer缓存机制。Integer在自动装箱时调用valueOf而valueOf对-128到127范围内的整数返回缓存对象范围外每次新建对象。所以Integer a 100; Integer b 100; System.out.println(a b); // true Integer c 200; Integer d 200; System.out.println(c d); // false在开发中两个包装类型比较值的时候建议一律用equals或先调用intValue()不要赌缓存范围。缓存范围虽然可以通过JVM参数调整但默认值就是这样依赖默认值的代码经不起环境变化。6.33 5能算3 5直接编译失败说明什么3 5是位运算把3和5都转成二进制011 101 001结果是1。而只能用于布尔表达式3 5中3和5都不是boolean所以编译直接失败。这说明Java是强类型语言逻辑运算符和位运算符的边界非常清晰。面试里用这道题其实是在考察候选人有没有把和的性质真正分开。6.4 运算符在源码分析里的价值以HashMap的取模为例HashMap在计算下标时使用了(n - 1) hash其中n是数组长度且一定是2的幂。这个表达式等价于hash % n但位运算更快。为什么长度必须保证2的幂正是因为2^n - 1的二进制全是1与运算能正确保留低n位结果范围在0到n-1之间等价于取模。你能看出运算符的价值了吗很多看似复杂的开源代码底层不过就是几个基础运算符的组合。如果连位运算、短路、优先级这些都不熟悉看源码就只能看到表面看不到设计者为什么这么写。6.5 综合练习判断一个数是不是2的幂最后给一个综合性的小练习这也是面试里常见的位运算变体题用一行代码判断一个正整数是否为2的幂。boolean isPowerOfTwo(int n) { return n 0 (n (n - 1)) 0; }原理是一个2的幂次方数在二进制里只有一个1比如4是1008是1000。减1之后这一位变为0右边的低阶位全变成1例如4减1是011。与原数做按位与结果为0非2的幂次方数则不会。这个题目综合了逻辑短路、位运算和优先级能一次考察多个知识点。我自己在招人时也喜欢出这类带一点运算符味道的小题因为它比死记硬背的八股更能看出一个人有没有真正写过代码。毕竟运算符是Java最底层、最绕不开的零件把它搞透了理解框架和排查问题的速度都会上一个台阶。