有不少自学Java的朋友跟我说,学语法的时候最容易卡住的地方不是if、for这些流程控制,而是运算符这一块。原因也简单:运算符的符号看着都认识,但组合起来运算规则特别绕,尤其自增自减、短路、类型提升这些细节,教材上一带而过,做题的时候全暴露出来了。特别是准备面试、练蓝桥杯这类算法题的朋友,如果运算符的优先级和运算时机没吃透,读题都费劲,更别说写对代码。
这篇内容我围绕Java语法里的运算符体系,按算术、赋值、比较、逻辑、三元、优先级、类型转换这几条主线逐个拆,每个知识点都配上实际代码和一些我自己踩过的坑。不管你是刚把Hello World跑通的小白,还是正刷题的备考生,都可以把它当作一份能直接照着练的笔记来用。
1. 为什么运算符值得单独花一整篇来练
很多初学者觉得运算符就是几个符号,会用就行,没必要专门练。但实际做项目、刷题、面试的时候,运算符反而是最容易丢分和出bug的地方。
首先,Java的运算符不是一个孤立的知识点,它和数据类型、类型转换、方法调用、流程控制全部挂钩。比如int a = 3; double b = a / 2;你猜b等于多少?不了解整数除法规则的人会以为是1.5,实际是1.0,因为整数相除先截断再做类型转换。这种细节,不写代码验证很难真正形成记忆。
其次,面试和算法题里,运算符是高频考察点。像i++和++i的区别、&&与&的差异、String用==比较为什么会有问题,这些几乎每年都会出现在笔试题里。蓝桥杯的填空题也有不少是专门考表达式求值,运算符优先级没背熟,一道题的答案就会差出十万八千里。
再者,运算符是读懂别人代码的基础。开源项目、公司老代码里经常有那种一行塞了好几个运算符的表达式,比如result = (a > b ? a : b) * 2 + c--;。如果你不熟悉运算顺序和副作用时机,根本没法快速理解这段代码在干嘛,更别说维护和调试。
所以我建议把运算符当成语法篇的一个专项来学,不光是认识符号,而是要能精确说出每一步的运算结果和类型变化。这篇内容按实操的方式来组织,你可以在IDE里跟着敲,也可以自己改参数做扩展练习。
2. 算术运算符:看起来全是小学内容,坑却一个都不少
算术运算符包括+ - * / %这五个,加、减、乘、除、取余。后面还有个自增自减++ --,严格说也属于算术类,我单独放一节讲,因为它的坑最多。
2.1 整数除法不是你想的那种除法
先看最经典的坑。两个整数相除,结果还是整数,小数部分直接丢掉,不是四舍五入,是截断。
int a = 7; int b = 2; System.out.println(a / b); // 输出3,不是3.5 System.out.println(a % b); // 输出1这里7 / 2结果是3,因为两个操作数都是int,Java会执行整数除法。如果你想让结果是3.5,至少要有一个操作数是浮点数:
System.out.println(a / (double) b); // 输出3.5 System.out.println((double) a / b); // 输出3.5我自己早期写代码的时候,就因为在计算平均值时用了整数除法,导致百分比全部归零,查了好久才发现问题是出在类型上。这个规则在蓝桥杯等算法题里也特别常见,比如统计一组数据中大于某个值的数据比例,如果直接用count / total,结果永远是0,必须先把其中一个转成double再除。
还有一点要注意,取余运算%在Java里对负数也有定义。结果是第一个操作数的符号决定:
System.out.println(7 % 3); // 1 System.out.println(-7 % 3); // -1 System.out.println(7 % -3); // 1这个和数学里的模运算不太一样,很多人以为-7 % 3应该是2,但Java里结果是-1。它遵循的规则是:a % b = a - (a / b) * b,其中除法是整数除法。了解了这个公式,正负号的问题就不会再混淆了。
2.2 自增自减:什么时候加、什么时候减,必须形成条件反射
++和--是写的代码最多、但出错也最多的运算符。核心区别只有一句话:i++先返回值再自增,++i先自增再返回值。
int i = 3; int a = i++; // a = 3,i变成4 int b = ++i; // i先变成5,b = 5这里最需要注意的是,在同一个表达式中连续对同一个变量做多次自增自减,Java的求值结果和C语言还不完全一样,而且可读性极差。我给的练习建议是:不要在一个表达式里对同一个变量使用多个自增自减,比如int result = i++ + i++;这种代码,不同的编译器或不同语言可能给出不同结果。虽然Java规范对求值顺序有明确说明,但实际开发里写这种代码基本等于给自己埋雷。面试题喜欢考,但工作中没人这么写,你能看懂陷阱就行,不要去模仿。
自增自减还有一个很容易忽视的场景是循环里:for (int i = 0; i < n; i++)。新手会纠结用i++还是++i,老实说在for循环的增量位置两者效果完全一样,因为增量表达式的返回值没有被使用。真要说效率,在Java里现代编译器做了充分优化,没有性能差异。你只要保持一致就好。
2.3 字符和字符串的加法容易让人误会
char类型参与算术运算时,会先提升为int。比如:
char ch = 'A'; // 字母A的Unicode值是65 System.out.println(ch + 1); // 输出66 System.out.println((char) (ch + 1)); // 输出B很多教材在讲ASCII或者Unicode的时候都会提到这一点,但实际操作中还是会有人直接拿char做加法,结果输出一个数字,自己都愣住了。
字符串用+做拼接是最常见的用法之一,但它有个隐藏陷阱:+在字符串上下文中是拼接,不是数学加法。如果一个表达式里既有数字又有字符串,运算顺序是从左到右:
System.out.println(1 + 2 + "3"); // 输出33 System.out.println("1" + 2 + 3); // 输出123第一行先算了1+2=3,然后字符串拼接变成"33"。第二行先是"1"+2拼成"12",再拼3变成"123"。这个顺序一旦记错,写日志和拼SQL的时候就会出莫名其妙的问题。
3. 赋值运算符:不仅仅是“等于”,它还有返回值
赋值运算符是=,以及它的复合形式+=、-=、*=、/=、%=。这一块看着简单,但有三个点值得展开。
3.1=是赋值不是“相等判断”,这关必须过
几乎所有初学者都犯过把if (a = 1)写成判断的错。在Java里这个语法是不被允许的,因为if条件要求的是boolean,而a = 1返回int,类型不匹配直接编译报错。这一点比C语言友好,C里这种写法还能通过编译,容易留下隐患。Java在编译阶段就把你拦住了,算是一种保护。
但要注意,复合赋值运算符返回的值也会被隐式使用,比如:
int a = 5; a += 3; // 等价于 a = a + 3,a变成8实际上a += b在Java里的完整等价是a = (type of a) (a + b),它做了一个隐式类型转换。举例说明:
byte a = 10; a = a + 5; // 报错:byte + int 结果是int,不能直接赋给byte a += 5; // 不报错:复合赋值运算符自动做了强转这一点非常关键。如果没有这层隐式转换,a += 5也要编译失败。很多人在刷题时遇到byte、short类型的运算报错,就是搞不清这个规则。
3.2 链式赋值的执行顺序
Java允许a = b = c = 10;这种链式赋值。它的执行顺序是从右往左:先c = 10,把结果10赋给b,再把结果赋给a。因为赋值表达式本身有值,所以能链起来。
但实际工作中我建议少用链式赋值。原因很简单:可读性差。别人读代码的时候要停下来想一下最后每个变量是什么值,还容易出现多个变量指向同一份可变对象的问题——不过对象引用赋值又是另一个坑了,这里先不说。对自学者来说,练习的时候知道链式赋值怎么算结果即可,写业务代码时一行一行写反而更安全。
3.3 赋值运算符的返回值和条件判断的结合
在循环里有一种常见写法:
String line; while ((line = reader.readLine()) != null) { // 处理line }这里line = reader.readLine()先执行赋值,返回读到的字符串对象,然后和null比较。如果不加最外层的括号,就会变成line = (reader.readLine() != null),类型变成boolean,语义完全不同。这种“在条件里做赋值再比较”的模式,在读取文件、数据库结果集时特别常见。掌握赋值运算符有返回值这个特性,你才能理解为什么这里需要双重括号。
4. 比较运算符:相等判断里有Java最大的一个坑
比较运算符包括==、!=、> < >= <=。比较运算的结果都是boolean,这个没什么好说的,但==用在引用类型上会出大问题。
4.1 基本类型比数值,引用类型比地址
对基本类型来说,==比较的是数值本身:
int a = 100; int b = 100; System.out.println(a == b); // true对引用类型来说,==比较的是内存地址,不是内容。两个内容相同的String对象,用==可能得到false:
String s1 = new String("hello"); String s2 = new String("hello"); System.out.println(s1 == s2); // false System.out.println(s1.equals(s2)); // true这就是Java面试里非常经典的String比较问题。另外,字符串字面量有字符串常量池的机制:
String s3 = "hello"; String s4 = "hello"; System.out.println(s3 == s4); // true,因为都指向常量池里的同一个对象很多初学者在这里会彻底懵掉:为什么有时候==是true,有时候是false?就是因为一个是常量池复用,一个是堆上新对象。做练习的时候建议这样总结:字符串内容比较一律用equals,不要依赖==。
4.2 浮点数比较不能直接比
0.1 + 0.2 == 0.3的结果经常是false,这不是Java特有的问题,而是二进制浮点数本身的精度限制。为了解决这个,比较浮点数一般用差值范围:
double a = 0.1 + 0.2; double b = 0.3; double epsilon = 1e-10; System.out.println(Math.abs(a - b) < epsilon); // true算法题里如果碰到浮点数判断,尤其是涉及几何计算、概率统计的题目,精度控制往往是关键。很多答案不是大逻辑错了,而是比较浮点数的方式不对。
4.3 比较运算的链式直觉是错的
数学里写3 > 2 > 1表示3大于2且2大于1。但Java里你不能直接写3 > 2 > 1,因为3 > 2的结果是boolean,boolean再和1比较类型不匹配,编译报错。正确写法是3 > 2 && 2 > 1。
这个看起来是语法问题,但反映的是一种思维模式:比较运算符的结果是boolean,boolean可以参与逻辑运算,却不能继续和数字比较。在实际解题时,要判断一个数是否在区间内,很多人会想当然地写if (0 < x < 10),这在Java里直接报错。你需要写if (x > 0 && x < 10)。
5. 逻辑运算符:短路特性才是它的灵魂
Java的逻辑运算符有&&(逻辑与)、||(逻辑或)、!(逻辑非),还有个按位与&和按位或|也经常被拿来和逻辑运算符对比。这一节的干货集中在短路求值上。
5.1 什么是短路求值
&&和||都是短路运算:如果左侧的结果已经能确定整个表达式的值,右侧就不再计算。这个特性不仅是性能优化,更是很多代码正确性的关键。
if (list != null && list.size() > 0) { // 安全处理list }如果list是null,list != null为false,整个&&表达式已经确定为false,右侧的list.size()不会执行,从而避免了空指针异常。如果用成&,两边都会执行,list.size()就会崩。
同理:
if (value == 0 || 100 / value > 10) { // ... }当value为0时,value == 0为true,||整个表达式确定为true,右侧100 / value不会执行,避免了除零异常。
在蓝桥杯这类算法题里,短路求值经常被当成考点。最常见的是类似这道题:int x = 0; if (x != 0 && 10 / x > 2) { ... },问是否会发生ArithmeticException。答案是不会,因为x != 0为false,后面直接跳过。反过来把&&换成&就会爆炸。
5.2 位运算符与逻辑运算符的分工
&和|如果操作数是boolean,按位与和按位或也能用在逻辑判断里,但它不会短路,会老老实实把两边都算完。在纯逻辑判断的场景,不要用&和|去代替&&和||。只有在真正需要按位运算——操作数是整数时——才用它们:
int flags = 0b1010; int mask = 0b1100; System.out.println(flags & mask); // 0b1000,按位与 System.out.println(flags | mask); // 0b1110,按位或现在很多算法题里会用到位运算来判断奇偶、交换两个数、取出二进制位,这些都属于进阶用法。新手阶段先把短路逻辑运算符吃透,位运算可以等学到位运算专项再深入。
另外还要提一个^(按位异或),它在Java里既能做整数的按位异或,也能用在boolean上做“逻辑异或”:两边不同才为true。写代码时不常用,但面试题里出现频率不低。比如用异或交换两个数,或者找一个数组里只出现一次的数字,那种解法好看但不容易想到。
5.3 逻辑非的优先级
!的优先级很高,只作用于紧跟着的表达式。比如!a && b等价于(!a) && b,不是!(a && b)。这个在写条件时容易搞混。如果你要表达“不是(a且b)”,必须写成!(a && b)。很多bug就是因为少了一对括号,逻辑完全反了。
判断一个年份是否为闰年的经典条件是:能被400整除,或者能被4整除但不能被100整除。用逻辑运算符写出来:
boolean isLeap = (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);这个表达式是练习逻辑运算符优先级的一个极好案例,因为括号用与不用,结果差很多。建议初学者亲手在IDE里改一改括号的位置,看看结果变化,加深理解。
6. 三元运算符:简化条件判断的利器,但不能滥用
三元运算符的语法是条件 ? 表达式1 : 表达式2,如果条件为true,整个表达式的值是表达式1,否则是表达式2。它本质上是if-else的表达式版本,因为有返回值,所以可以直接嵌入赋值、传参和打印语句中。
6.1 基本用法和类型统一问题
int max = (a > b) ? a : b; System.out.println("结果是:" + (score >= 60 ? "及格" : "不及格"));一个比较隐蔽的坑是:三元运算符的两个分支返回类型必须兼容,否则会做类型提升。比如:
Object obj = condition ? 1 : "text";这个表达式里int和String之间没有关系,最终会编译成Object类型,所以能赋值给Object变量。但如果分支类型是int和double,两个分支会被统一提升为double:
double result = condition ? 1 : 2.5;初学者最容易踩坑的是写下面这种代码:
System.out.println(condition ? 1 : 2.5);输出可能是1.0而不是1,因为第二个操作数2.5是double,整个条件表达式的类型被提升为double,1也变成1.0输出。这种细节在打印和序列化时可能导致前后端数据类型不一致,需要留意。
6.2 嵌套三元可读性差,建议拆开
昨天一个朋友发我一串代码,里面三层三元嵌套,他问我说输出结果为什么和自己推的差了一个值。我让他先拆成if-else,他马上就找到了问题,是自己优先级看错了。经验之谈:两层的三元运算还能接受,三层以上就算能读懂逻辑,也不适合在团队项目里出现。代码的首要读者不是你,而是三个月后的你和其他同事。
如果真的需要多层判断,建议用if-else或者switch表达式。Java 12以后也支持switch表达式了,可读性比嵌套三元高很多。当然面试和笔试题偶尔会故意给多层嵌套的三元表达式来考察优先级,你能读懂就行,常规项目里为了可维护性尽量少写。
6.3 三元运算符的常见面试变体
有一种常见的考察方式,是把三元运算符和赋值运算符混在一起。比如:
int x = 5; System.out.println(x > 3 ? "big" : "small"); // 没问题,输出big再比如:
int a = 1; int b = 2; System.out.println(a++ > b ? a : b++); System.out.println("a=" + a + ", b=" + b);这道题要分两步理解:先比较a++ > b,比较时a还是1,1 > 2为false,比较后a自增为2;然后走b++分支,返回b自增前的值2,最后b变成3。所以输出一次2,再输出a=2, b=3。这种题就是训练你看清楚自增时机和三元求值顺序的。
7. 优先级与类型转换:运算符练习里最硬核的一部分
很多初学者背不住运算符优先级表,于是遇到复杂表达式就靠猜。更稳妥的思路有两个:能加括号就加括号,这样当然最安全;但如果你想真正看懂那些没加括号的代码,还是得掌握优先级的核心规律。
7.1 优先级的核心记忆框架
Java运算符优先级从高到低,最常用的可以简化为一句话记忆:一括号、二自增、三乘除、四加减、五移位、六比较、七相等、八与、九异或、十或、十一逻辑与、十二逻辑或、十三三元、十四赋值。具体到平时的代码,最常见的几个层级是:
- 括号、取成员、方法调用优先级最高
- 自增自减、一元负号次之
- 乘除、取余高于加减
- 移位高于关系比较,关系比较高于相等判断
&&高于||- 三元运算符优先级低
- 赋值运算符优先级最低
看一个实际例子:
int result = a + b * c > d || e < f && g;这个表达式等价于int result = ((a + (b * c)) > d) || ((e < f) && g);。你没有看错,&&的优先级高于||,所以e < f && g会先结合,然后整体再和前面的>结果做||。
不过我要强调一句:在业务代码里写出这种表达式是灾难,读起来太累,最好拆开。练习优先级是为了看懂题、看懂老代码,不是为了在项目里炫技。
7.2 表达式的类型提升规则
算术运算里有一个隐式的类型提升规则,需要重点掌握。核心规律是:byte、short、char运算时先提升为int;如果操作数里有long,结果就是long;有float,结果就是float;有double,结果就是double。
short s = 10; s = s + 1; // 报错,s+1结果是int,不能赋回short s += 1; // 不报错,复合赋值自带强转字符串和任何类型做+,结果必然是String:
String str = "value:" + 123 + true + 3.14; // str = "value:123true3.14"这个特性在拼接日志、调试信息时非常实用,但也容易让人忽视,比如把String和数字用+拼起来参与比较,会出现很多边界问题。
7.3 括号是最后的防线,也是最优解
我做了这么多年开发,见到复杂表达式时的习惯是:先把括号补齐再读。一直强调括号不是能力不行的表现,恰恰是表达清晰的手段。练习时可以给自己定一条规则:凡是不确定优先级的地方,一律加括号。写过一阵子之后,你自然会发现那些常见的优先级组合已经形成肌肉记忆,需要加括号的地方越来越少。
8. 运算符综合练习题:从填空题到实战的过渡
为了把前面这些规则串起来,我整理了几道不同类型的练习题,可以动手练一练。建议每道题都先在纸上算出答案,再在IDE里跑一遍验证,重点是对比“自己以为的结果”和“实际输出”之间差在哪里。
8.1 表达式求值填空题
int a = 10; int b = 3; int c = a % b * 2 + a / b; // 问:c是多少?这个按优先级顺序走:先算a % b = 1,a / b = 3,然后乘2再加3,结果是5。注意%和*是同一优先级,从左到右结合。
int i = 1; int j = i++ + ++i; // 问:j是多少?i最终是多少?这道题非常经典,关键是理清运算顺序。先计算左侧i++,返回1,i变成2;再计算右侧++i,i先变3,返回3;总和是4,最终i是3。网上经常有人说Java对这类表达式有特定求值顺序,实际运行结果确实是4。但再次提醒:实际开发别这么写。
8.2 短路陷阱题
int x = 5; boolean flag = (x > 3) || (x++ > 5); // 问:x最终是多少?flag是多少?因为x > 3为true,||短路,右侧x++ > 5不执行。所以flag是true,x仍然是5。如果把||换成|,x会自增为6。
这一类题在面试和蓝桥杯里都很常见,检验的就是你有没有真正形成“短路”这个概念。
8.3 三元和类型的结合题
int score = 85; String level = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "D"; // 问:level是多少?嵌套三元从右往左结合,85分先比较score >= 90为false,再往下走,score >= 80为true,结果是"B"。这题难度不大,但可以帮你习惯多分支的表达方式。
8.4 实际场景里的运算符组合
比如一个经典的判断逻辑:年份是闰年的条件。
int year = 2000; boolean leap = (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0); System.out.println(leap); // true2000年能被400整除,直接成立。如果是1900年,能被100整除但不能被400整除,不是闰年,这个判断会输出false。通过改年份来验证自己的理解是最好的练习方式。
我再给一个稍微实际点的场景:判断一个学生是否具备奖学金申请资格——平均分不低于85,且没有挂科记录,并且出勤率不低于90%。
double avgScore = 87.5; boolean hasFailed = false; double attendRate = 0.95; boolean eligible = avgScore >= 85 && !hasFailed && attendRate >= 0.90; System.out.println(eligible); // true这题不难,但包含了比较、逻辑非、逻辑与、浮点数比较等多种运算符的组合,适合作为综合练习题。你可以试着修改attendRate的值到0.88,看看eligible如何变化。
8.5 运算符与字符串拼接的综合坑
int num = 5; System.out.println("num + 1 = " + num + 1); System.out.println("num + 1 = " + (num + 1));第一行输出的结果是num + 1 = 51,第二行是num + 1 = 6。原因就是字符串拼接的优先级从左到右,如果把数值运算放进去,必须用括号明确计算范围。这种细节在写日志和拼接SQL时经常出现,值得养成随手加括号的习惯。
9. 我在运算符学习上的一些经验和建议
最后说几句实操层面的东西。
第一,运算符的坑,见过一次比背十遍规则都管用。不要只看课本上的优先级表,去做题,去写代码,去故意写错几次,把这些坑变成自己的经验。
第二,IDE的调试器是你理解运算符的最佳工具。可以单步执行,观察变量值随运算的变化,尤其在看自增自减、短路求值时,调试器的变量窗口会给你非常直观的反馈。如果光靠脑子推容易晕,用调试器一步步跟下去,很快就能建立正确的直觉。
第三,养成写代码时优先保证可读性的习惯。表达式一复杂就用括号,自增自减不要和其他语句挤在一行,嵌套三元拆成if-else。这些习惯会让你的代码质量提升一个档次,面试官也更喜欢读这样的代码。
第四,Java运算符是后面学流程控制、方法重载、面向对象的基础,但不宜在这里停留太久。我的建议是:把基础规则过一遍,做20到30道综合题,就开始往下学。后面遇到运算符的坑再回来查,比一次性死磕要高效得多。
我给自己带的练习生布置任务时,经常说这样一句话:学运算符的目标不是背会优先级表,而是做到光看表达式就能大致预测结果,并且能说出为什么。达到这个状态后,你再看下一章的流程控制,会觉得顺很多。