ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java运算符详解:从算术到位运算,避开常见陷阱

Java运算符详解:从算术到位运算,避开常见陷阱 从0开始学Java丨第三天把运算符彻底搞明白别让细节坑你三年昨天我们搞定了变量和数据类型知道了数据怎么存。那数据存下来之后要干嘛肯定是要“运算”啊。加减乘除、比较大小、判断真假、位级操作这些统统都要靠运算符来完成。可以说运算符就是Java程序对数据“动手”的第一套工具也是你在写代码时接触最频繁的东西。很多新手学到运算符这一章觉得无非就是 - * /这些东西小学数学而已于是草草略过。结果一到写项目、刷面试题的时候就傻眼了为什么5/2结果是2而不是2.5为什么两个Integer用比较有时候相等有时候不相等为什么和看起来差不多却有人因为混用被线上事故坑了一把这些全部是运算符章节的细节延伸。这一篇我打算换个讲法不全按教科书那种“六大类运算符逐个介绍”的套路来而是从实际场景出发哪些是必须背下来的规则哪些是不需要死记、理解了就能用的哪些是面试和工作中经常埋坑的。看完这篇你可以做一次自我检测把这套思维带到后续的流程控制和面向对象中去。1. 运算符全景先在心里画一张地图很多人的学习方法是一上来就对着表格死记运算符有哪些、符号怎么写、优先级是什么。我建议换个思路先把运算符当成“对数据做加工的动作”你就能很好理解它们为什么这样分类。Java里的运算符可以分两大类理解一类是对单个值做加工一元运算另一类是把两个值组合成新值二元运算。再加上一个特殊的三元运算就是我们在基础阶段要掌握的所有形态。从功能上看又可以分成下面六类算术运算符 - * / % --处理数字的基本数学计算。赋值运算符 - * / %这些把右边的值赋给左边的变量。关系运算符 ! 比较两个值的大小或是否相等。逻辑运算符 || !处理布尔值的组合判断。位运算符 | ^ ~ 直接操作二进制位。三目运算符? :也是Java里唯一一个三元运算符。这六类的关注点完全不同算术运算符核心在于类型转换和精度损失关系运算符核心在于相等判断的陷阱逻辑运算符核心在于短路行为位运算符核心在于底层理解。所以你学的时候不要平均用力要把精力放在每一类最难的那个点上。顺带说一句网上很多人问“c语言运算符和Java运算符有什么区别”其实两者的运算符设计基本一致C语言有的Java基本都有。区别主要在细节Java对类型要求更严格比如boolean类型不能像C语言那样用0和1替代if(1)这种写法在Java里直接编译报错。这正是Java在语言设计上的一种严谨。2. 算术运算符与类型转换最容易掉进去的第一个坑2.1 整数除法为什么5除以2等于2让我从最常见的一个问题说起。有读者在群里问System.out.println(5 / 2);输出的结果不是2.5而是2。这个问题的本质是整数除以整数结果一定是整数。在Java中如果除法运算符/的两边都是整数类型int、long、short、byte那么结果会直接截断小数部分而不是四舍五入。5 / 2得到299 / 10得到9-7 / 3得到-2向零取整后面细说。很多初学的人在这里不理解我希望得到2.5怎么办答案是至少让其中一个操作数成为浮点数。比如System.out.println(5.0 / 2); // 输出2.5 System.out.println(5 / 2.0); // 输出2.5 System.out.println(5.0 / 2.0); // 输出2.5这就是Java的数值类型提升规则当一个int类型和一个double类型做运算时int会自动提升为double整个运算变成浮点数运算。你可以把它类比成单位换算——1米 1厘米要先把米换算成厘米才能相加。Java里小范围类型遇到大范围类型会自动“换算”成大范围的再进行运算。这里有个操作中极其常见的坑如果你要计算平均分成绩是int分数学生人数是int你直接写totalScore / studentCount结果永远是截断后的整数。正确做法是先乘以1.0或者提前把其中一个变量转成doubledouble average (double) totalScore / studentCount;我特别强调一下“提前转换”的时机很多新手喜欢把所有数算完再强转比如(double)(totalScore / studentCount)这样等于先做了整除再补个小数点毫无意义。要转就趁早让除法那一步就是浮点数运算。2.2 取余符号跟着被除数走取余运算符%在Java里也是非常基础的工具常用来判断奇偶、控制循环换行、拆分数字等。但初学的时候最容易困惑的一个问题是负数取余结果的符号怎么定Java的规则是余数的符号和被除数左边的数一致。System.out.println(7 % 3); // 1 System.out.println(-7 % 3); // -1 System.out.println(7 % -3); // 1 System.out.println(-7 % -3); // -1你看7 % -3结果是1而不是-1这就是因为规则只跟随被除数。为什么这么设计因为Java在计算余数的时候是按照a / b先得到截断后的商然后再用a - 商 * b得到余数。拿7 % -3来说7 / -3在Java里等于-2向零取整然后7 - (-2 * -3) 7 - 6 1。做业务的时候如果你需要“余数一定是非负数”的场景比如循环数组下标就得自己处理一下int index rawValue % size; index (index size) % size; // 保证结果落在[0, size-1]这个处理手法很常见做分页、轮播、环形队列的时候都用得上建议直接记下来。2.3 自增自减到底先加还是先用和--可能是初学者在运算符阶段第一个“看着简单但老写错”的东西。核心就一个区别在前先加后用在后先用后加。举个例子int a 5; int b a; // 先把a的值赋给ba再加1 System.out.println(a); // 6 System.out.println(b); // 5 int c 5; int d c; // c先加1再把值赋给d System.out.println(c); // 6 System.out.println(d); // 6你不需要去死背口诀有一个特别直观的类比你可以把a看成一个“取完值再去自增”的动作像你先拍照再往前走一步a则是“先迈步再拍照”。不过在实际工作中我强烈建议除非是for循环里的i这种约定俗成的写法否则不要在复杂表达式中混用自增自减。比如int result i i;这种代码虽然能编译但可读性极差还容易因为求值顺序产生理解分歧。写代码是为了让别人能看懂不是为了炫技。还有一个非常重要的细节需要提醒short s 1; s s 1;这段代码编译会报错。因为s 1的结果是int类型不能自动降级赋给short。但改成s 1;就没问题。这个牵扯到复合赋值运算符的隐式转换我放到下一节讲。3. 赋值运算符与复合赋值那个看不见的类型转换3.1 赋值运算符的“右结合”特性是赋值运算符它的作用是把右边的值赋给左边的变量。很多人不知道赋值运算符的结合方向是“从右往左”的也就是右结合。这意味着你可以写这样的连等赋值int a, b, c; a b c 10;这个表达式的执行顺序是先c 10再把c的值赋给b再把b的值赋给a。整个过程三变量都变成10。这种写法在给多个变量赋初值时很省事但可读性一般实际工作中建议还是分开写除非你明确是在写“所有变量初始化成同一个值”。3.2 复合赋值运算符自带强制转换、-、*、/、%这些复合赋值运算符本质上是“赋值运算”的合体比如x 5等价于x x 5。但注意只是语义上等价底层不完全一样。关键点在于复合赋值运算符会在底层自动完成类型转换这在某些场景下会“瞒过”编译器的检查。举个例子short x 1; x 1; // 这行能编译通过 // x x 1; // 这行编译报错不兼容的类型int无法转为short为什么x 1能过因为Java规范里规定复合赋值运算等价于先把结果运算出来然后隐式地做一次类型转换把int结果强制转回short。也就是说x 1实际执行的是x (short)(x 1)。这个设计有好处写起来省事但也有隐患如果运算结果溢出你不会有任何编译期提示。比如byte b 127; b 1;结果会变成-128出现了回绕编译器也不会报错。所以当你用复合赋值运算符时心里要清楚当前数据的取值范围不要因为编译过就以为万事大吉。3.3 字符串拼接的加号和数值加法还有一个运算符相关的“超高频混用坑”既能做数值运算也能做字符串拼接。规则很简单只要两边有一个是字符串就变成字符串连接符。System.out.println(1 2 3); // 输出33 System.out.println(1 2 3); // 输出123 System.out.println(1 2 3 ); // 输出6看到了吗运算顺序是自左向右的第一个表达式先算123然后33拼接成33第二个表达式从一开始就是字符串拼接后面的数字全部转成字符串拼上去第三个先算出数值6再跟空字符串拼接变成6。这个细节在打印日志时特别容易踩坑。比如你想输出“用户ID是10086年龄25”如果写System.out.println(用户ID是 id 年龄 age);一切都正常。但如果写成System.out.println(结果: a b);你原本想要的可能是数值ab的结果结果却变成了两个数字拼接的字符串。要得到正确结果得加括号System.out.println(结果: (a b));。4. 关系运算符与逻辑运算符判断逻辑里的短线思维4.1 浮点数比较为什么0.1 0.2不等于0.3关系运算符有六个 !分别表示大于、小于、大于等于、小于等于、等于、不等于。其中前四个都比较直观真正阴险的是和!。新手最迷惑的一个点0.1 0.2 0.3的结果是false。这是浮点数在计算机底层用二进制表示时产生的精度误差它不是一个Java独有的问题而是所有编程语言都存在的通用现象。简单类比一下十进制的1/3等于0.3333...永远写不精确二进制的浮点数对很多小数也有类似的问题0.1在二进制里就是一个无限循环小数存储时被截断所以计算出来有微小误差。所以实际业务里比较浮点数几乎从来不直接用而是比较“差的绝对值是否小于一个很小的阈值”。比如double a 0.1 0.2; double b 0.3; final double EPSILON 1e-9; if (Math.abs(a - b) EPSILON) { System.out.println(可以认为相等); }这个叫“误差容差法”或者叫“机器精度比较法”。涉及金额的项目一般直接用BigDecimal不碰浮点数。这些到你做真实项目自然会有体会但运算符阶段你至少要知道浮点数不能用安全比较。4.2 短路逻辑 和 的区别远不止一个字母逻辑运算符有短路与、||短路或、!取反另外还有非短路的与和|或。它们都作用于布尔类型。入门阶段!非常简单就是“颠倒黑白”true变falsefalse变true。和||的“短路”是什么意思是一旦已经能确定整个表达式的真假就不再计算右边的部分。你可以记两条规则false 表达式结果一定是false右边的表达式不会执行。true || 表达式结果一定是true右边的表达式不会执行。为什么这个设计很重要因为它能防止程序崩溃。最常见的一个场景判断一个字符串是否为空不为空再去调用它的方法。String str null; // 下面的写法会抛NullPointerException // if (str ! null str.length() 0) { // 上面的写法不会抛异常 if (str ! null str.length() 0) { System.out.println(字符串非空且有内容); }如果写成str ! null str.length() 0两个条件都会执行那么str为null时str.length()直接空指针异常。所以实际工作中写判断条件时永远默认用和||只有极少数特殊场景比如你希望两边的表达式一定都执行才考虑和|。这个习惯可以在运算符阶段就养成。还有一个细节和执行优先级也不同的优先级低于但在大多数场景你不会显式感觉到因为有短路保护反而是更安全。4.3 逻辑表达式的优先级速记优先级是运算符里最枯燥的一部分但有两个常见的坑必须先避开第一的优先级高于||。这意味着true || false false的结果是true因为先算false false false再算true || false true。这跟很多人的直觉“从左到右依次算”不一致。第二关系运算符的优先级高于逻辑运算符但低于算术运算符。所以a b c c d先算ab再比较再逻辑运算。对于刚入门的人我不建议去背完整优先级表更建议这样做拿不准优先级时就加括号。代码写(a b) c (c d)效果一样但别人一眼看懂自己以后回头看也不会绕晕。括号是给阅读者看的不是给编译器看的。5. 位运算符从入门到进阶的分水岭5.1 先把二进制基础补上位运算符看名字就明白它是直接对二进制位进行操作的。新手看到这里总觉得“这是大佬才用的东西”其实不然理解位运算不需要你是计算机科班出身只需要你给我十几分钟先把二进制补补课。任何整数在计算机底层都是二进制。比如5的二进制是1013的二进制是011。位运算就是把两个数的每一位对齐然后按规则逐位计算。Java里支持按位与、|按位或、^按位异或、~按位取反、左移、右移、无符号右移。规则也不难两位都是1结果才是1。比如5 3101 011 001结果是1。|只要有一位是1结果就是1。比如5 | 3101 | 011 111结果是7。^两位不同结果为1相同为0。比如5 ^ 3101 ^ 011 110结果是6。~把所有位翻转0变11变0。注意~5的结果不是-5而是-6因为Java里整数是有符号的二进制补码表示导致的结果。这三个口诀可以这样记是“两者都要满足才通过”|是“有一个满足就通过”^是“两个人必须不一样才通过”。后者的“异或”非常适合做数据翻转类操作比如在不引入临时变量的情况下交换两个整数int a 3, b 5; a a ^ b; b a ^ b; a a ^ b; // 现在 a 5, b 3我见过很多初学者看到这个写法觉得很酷但我要提醒一句工作中别用这个技巧交换变量。它的可读性远不如int temp a; a b; b temp;而且现代编译器和JVM对简单交换都有自己的优化你用异或交换并不会有性能收益反而增加理解成本。这个例子你用来理解异或的对称性就好真正要写业务代码时请老实用临时变量。5.2 移位运算左移乘2右移除2移位运算分为三种左移、右移、无符号右移。二进制位整体向左移动右边补0。左移一位相当于乘以2左移n位相当于乘以2的n次方。二进制位整体向右移动左边用符号位填充负数补1正数补0。右移一位相当于除以2并向下取整。整体向右移动左边始终补0不管正负。举个例子int x 4; // 二进制100 System.out.println(x 1); // 8等价于4*2 System.out.println(x 1); // 2等价于4/2 int y -4; // 二进制补码表示 System.out.println(y 1); // -2因为负数右移保持符号 System.out.println(y 1); // 2147483646最高位变成0数值剧变这里的核心理解是是算术右移保留符号是逻辑右移不关心符号。一般情况下用就够了只有当你操作的是无符号语义的数值比如颜色值的RGB分量时才用避免符号扩展污染结果。位运算的一个经典应用场景是权限控制。很多项目的权限系统用int的每一位代表一种权限通过位运算来组合和判断比如int READ 1 0; // 0001二进制第0位 int WRITE 1 1; // 0010第1位 int DELETE 1 2; // 0100第2位 int myPermission READ | WRITE; // 0011 boolean canWrite (myPermission WRITE) ! 0; // 判断是否拥有写权限 boolean canDelete (myPermission DELETE) ! 0; // false这种做法比用一组boolean字段去存储权限要节省空间也方便扩展。你能看到很多开源框架的源码里大量使用这种模式比如Java集合类的modCount、各种状态标记位。学的时候不理解没关系先混个眼熟以后看源码看到和时能反应过来这就是位运算的权限控制套路。5.3 位运算判断奇偶比取余更快的技巧判断一个整数是奇数还是偶数最常规的写法是n % 2 0。用位运算的写法是(n 1) 0。原理很简单二进制的最后一位如果是1那这个数就是奇数如果是0就是偶数。n 1直接拿到最后一位。为什么有人喜欢用位运算因为位运算直接在二进制层面操作比取余运算在底层执行要快一些。但在现代JVM里这个差距其实已经被优化得很小很多情况下编译器会把你写的n % 2自动优化成位运算。所以我的建议是你知道这个技巧面试被问的时候能答上来平时写代码按可读性优先用n % 2完全没问题。这其实代表了学习位运算的一个正确心态位运算是一种理解计算机底层思维的途径是加分项但它不是解决日常问题的首选工具。你真正需要它的时候通常是性能敏感的基础组件开发、JDK源码阅读、以及面试聊底层原理的环节。6. 三目运算符与优先级让代码既简练又安全6.1 三目运算符IF-ELSE的压缩版Java的三目运算符格式是条件表达式 ? 值1 : 值2。怎么理解先判断条件表达式的真假为true就取值1为false就取值2。它是if-else的简写形式适合“二选一赋值”的场景。比如int score 85; String level score 60 ? 及格 : 不及格;这行代码用if-else写可能要三五行用三目运算符一句话搞定。但注意这里有个经典的类型隐患int a 1; int b 2; Object result a b ? 1 : 两个数的比较结果;你猜result最终的类型是什么答案是Object还是int还是String这个问题的坑在于如果三目运算符的两个分支分别是int和StringJava会把int强制转为String吗不会它是先把int装箱成Integer然后跟String一起提升为公共父类型Object。这类细节在日常业务中很少遇到但面试八股文里偶尔会考。更值得防范的是嵌套三目。有人喜欢写a b ? (a c ? a : c) : (b c ? b : c)这种求最大值的“精简”代码我实在不建议你这么做。三目嵌套的可读性极差一旦逻辑复杂维护成本飙升。我的原则是三目运算符最多用一层再复杂的判断就老老实实写if-else。简洁是要付出代价的而这个代价不划算。6.2 优先级记住最关键的一档就够了Java运算符优先级是一个完整的表从上到下大概有十几个层级。新手看到这种表容易焦虑觉得要全背下来。我认为不需要你只需要记住几个最强实用的规则优先级最高的是括号想改变运算顺序就用括号。一元运算符如、--、!、~优先级高于算术运算符。算术运算符* / %高于 -高于关系运算符。关系运算符高于逻辑运算符。逻辑运算符中高于||。赋值运算符优先级最低。给你一个实用的记忆锚点越像“计算”的优先级越高越像“连接动作”的优先级越低。加减乘除是最强的计算所以优先级高比较大小是稍微松散的动作优先级中等逻辑判断是一种组合连接优先级低赋值是把算好的结果存起来优先级最低。但说到底我对所有新手的一句忠告是优先级靠加括号保证不靠记忆保证。真到了复杂的表达式中哪怕你背得滚瓜烂熟别人阅读你的代码时也不一定背得熟。if ((a || b) c)比if (a || b c)多两个括号但每个读你代码的人都不需要去回想优先级表就能秒懂。6.3 运算符和表达式在项目里的思考方式我们可以从编程思维的层面再聊深一点。运算符这个东西表面上是“语法符号”但它的本质是一种计算思维的最小单元。我见过很多新人一开始以为写代码就是堆运算符一个复杂功能写出一长串表达式越写越看不懂最后自己都改不动。这其实违背了编写可维护代码的基本逻辑。更成熟的做法是把复杂表达式拆成有名字的中间变量。比如int basePrice quantity * unitPrice; boolean isVip userLevel 2 totalSpent 1000; double discount isVip ? basePrice * 0.8 : basePrice; double finalPrice discount 100 ? discount - 20 : discount;这段代码里没有一处超过两层的复杂表达式但任何人都能一眼看出计算过程。给中间结果取名字本质上是给逻辑打上了标签把让人费解的运算链变成了可视化流程。所以学运算符不要只顾着“会算”还要建立“清晰表达”的意识。同样的运算可以用不同方式表达但团队协作时别人最需要的是好读其次才是“简洁”。这一课越早建立对你后面写复杂业务帮助越大。7. 新手常见的运算符错误与排查思路这一部分是我在带人和答疑过程中反复遇到的高频问题整理成一个速查表给你遇到类似情况可以先对照自查。7.1 高频错误速查表错误现象根本原因解决方案5/2结果期望是2.5得到2整数除法截断至少一个操作数改为浮点数0.10.2 0.3输出false浮点数二进制精度误差使用误差容差比较或BigDecimalstr ! null str.length()0抛空指针用非短路导致两边都执行改用x 1结果溢出为负数复合赋值隐式强转超出范围使用更大范围类型或检查数据范围123输出123字符串拼接优先级与数值计算混用需要数值运算时用括号包裹a a结果难预测自增自减混在复杂表达式中拆开写单独自增再使用b b 1编译报错b是byte算术运算结果自动提升为int使用b 1或显式强转这个表我建议收藏回头遇到编译报错或者结果不对时一条条核对。很多时候你以为是业务逻辑的问题排查到最后其实是最基础的运算符规则没注意到。7.2 排查思路从结果反推遇到“程序结果和预期不符”时我推荐一种高效的排查思路先定位是值的问题还是类型的问题。值的问题比如结果差1、差2往往是运算顺序或边界条件搞错了比如该用却用了该取余却用了除法。类型的问题比如结果是小数变整数、字符变数字往往是类型提升和强转时机不对。你把错误归类之后再对照上面的速查表基本都能快速定位。还有一个排查小技巧善用System.out.println分段打印中间结果。别嫌这方法土它是最直接有效的。比如你怀疑某个复杂表达式算错了就把每一步的中间变量都打出来看到底哪一步开始偏差。等你以后会用调试器IDE的Debug模式了原理也一样都是“分步观察”。7.3 实践建议今天可以做的小练习光看是不行的运算符必须靠敲键盘才能长进。我给你三个练手的小题目大约半小时搞定第一题输入一个秒数输出它对应的小时、分钟、秒。比如输入3661输出1小时1分钟1秒。这个题考的就是除法和取余的配合h total / 3600m (total % 3600) / 60s total % 60。第二题给定一个四位数的整数输出它的每一位数字之和。比如1234输出10。这个题考的是用取余和除法拆位个位n % 10十位(n / 10) % 10依次类推。第三题交换两个整数变量要求不使用第三个临时变量。你可以用加减法a a b; b a - b; a a - b;也可以用异或的方法。做完再想想各自的原理和可能的坑比如加法交换可能导致整数溢出而异或不会。这三个题做完运算符这块的知识就算真正落地了。我个人在实际带新人的过程中最大的感触是运算符这一章看起来“最简单”但它恰恰是后面所有代码的地基。你后面写的每个if判断、每个循环边界、每种算法优化本质上都是在跟运算符打交道。如果你在这一章只是背了符号表而没理解规则背后的原因越往深走越容易翻车。反而是在这一步多花点时间把整数除法的截断、短路逻辑的保护作用、位运算的权限模型这些概念都吃透你后面遇到复杂问题时思考速度会明显快别人一截。最后再分享一个小技巧每天的代码练习结束后试着把你今天写的代码里所有运算符单独列出来回忆一下自己在哪些地方犹豫过、查过资料。犹豫的地方就是你还没掌握的地方第二天重点补一遍效果远好于从头到尾再学一遍。运算符这东西说白了就是“见得多了自然熟”但前提是每次见面都要弄明白它为什么这样表现而不是囫囵吞枣翻过去。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进