ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java比较运算符详解:从==与equals的陷阱到浮点数精度问题

Java比较运算符详解:从==与equals的陷阱到浮点数精度问题 写这篇东西的起因是我上个月面了一个刚毕业的候选人。问他 Java 有哪些比较运算符他背得很顺大于、小于、大于等于、小于等于、等于、不等于。结果让他手写一个判断——如果用户输入的账号是 admin就输出欢迎语他第一行写出来的是if (username admin)。那一刻我就知道这孩子的基础还停留在“背概念”的阶段没有真正理解比较运算符和赋值运算符的区别。这个场景其实特别典型。Java 比较运算符看起来是语法里的边角料好像谁都会但真到写代码、跑程序、过面试的时候翻车的全是细节。这篇文章我不想按教科书的顺序平铺直叙而是从“到底解决什么问题”讲起把这 6 个符号逐个拆开配上可以直接复制运行的代码案例再把新手最容易踩的坑和面试官最爱问的点都摆到台面上说清楚。不管你是刚装好 JDK 准备入门还是已经写了几个月代码但总在某些边界情况上卡壳这篇文章都值得你从头到尾看一遍。1. 比较运算符到底在解决什么问题1.1 程序里的“判断”全都离不开它人的大脑每天都在做决策今天出门要不要带伞看到的数字是不是超过了心理价位程序也一样它需要根据某些条件决定走哪条分支、循环要不要继续、数据能不能通过校验。而“条件”最终落到代码层面就是一个布尔值true 或 false。比较运算符就是产生这种布尔值最直接的工具。它把两个操作数放在一起比一比然后告诉你结果成立还是不成立。比如电商下单时要判断库存是否充足游戏里要判断分数是否打破纪录登录时要判断密码是否匹配背后都是比较运算在起作用。可以说if语句是程序的分岔路口而比较运算符就是路口的指示牌没搞清楚指示牌后面的路全乱。有个生活化的类比可以帮你建立直觉。你去茶餐厅点酒服务员要判断你是否满 18 岁。这个“判断”在 Java 里写出来就是age 18它的结果是一个布尔值要么是true要么是false。服务员拿这个结果决定下一步动作给不给下单。程序里的逻辑一模一样——先用比较运算得到一个布尔值再根据这个布尔值决定要不要执行某段代码。理解了这一层你就不难明白为什么每本 Java 教材都把if和比较运算符放在一起讲了它们是天生一对。1.2 Java 中的 6 个比较运算符全家福Java 一共提供了 6 个比较运算符也叫关系运算符。它们分别是运算符含义示例什么时候结果为 true大于a ba 的数值大于 b小于a ba 的数值小于 b大于等于a ba 大于 b或者 a 等于 b小于等于a ba 小于 b或者 a 等于 b等于a ba 和 b 的值相等!不等于a ! ba 和 b 的值不相等这里先立住一个新手最容易忽略的要点在 Java 中比较运算的结果类型是boolean只有true和false两种取值。这意味着你可以把比较结果直接赋值给一个布尔变量int a 10; int b 20; boolean result a b; // result 的值是 false很多新手不知道还能这么写总以为a b只能出现在if的条件里。实际上它就是一个普通表达式有类型、有值可以被赋值、被打印、被参与逻辑运算。把这个认知建立起来后面理解复杂的条件组合会顺畅很多。另外不同数据类型的比较规则也有讲究。数值类型int、double、long、short等可以直接用任意一个比较运算符char类型也可以比较因为它在底层对应的是 Unicode 编码值boolean类型只能使用和!不能用大于小于去比因为它不是数值引用类型比如对象、数组、字符串可以使用和!但要特别小心因为它们比较的是对象的引用地址而不是对象内部的“内容”这一点在后文会重点展开。2. 6 种比较运算符的逐个拆解与实战用法2.1 大于、小于、大于等于、小于等于数值比较的基本盘这四个运算符在业务代码里的出镜率极高而且通常不是单独出现的往往伴随着范围判断。比如电商系统里判断用户是否可以领取优惠券常见的规则是“订单金额满 100 可用”写出来就是if (orderAmount 100) { // 发放优惠券 }再比如一个抽奖系统要判断用户的积分是否在某个区间内可能会写if (score 0 score 500) { System.out.println(青铜会员); } else if (score 500 score 2000) { System.out.println(白银会员); }这里有一个很容易被忽视的细节、、、这四个运算符的优先级低于算术运算符但高于逻辑运算符。什么意思呢比如a b c * 2Java 会先计算a b和c * 2然后再做大于比较所以这个写法不需要额外加括号。不过从我个人的习惯来说如果表达式里混合了算术运算和比较运算我倾向于把每一段的计算用括号显式包起来比如写成(a b) (c * 2)。原因不是为了语法正确而是为了“可读性”——过两周再回头看代码自己不用猜同事 code review 时也不用费劲推理。另外一个容易忽略的点是char类型也可以参与比较。a的 Unicode 编码是 97b是 98所以a b的结果为true。如果你做字符校验比如判断用户输入的字符是不是大写字母可以写ch A ch Z这比记忆 ASCII 表的数字要直观得多。2.2 “”和“!”等与不等的双重陷阱和!是所有比较运算符里最特殊也最容易翻车的两个。先说它们做对的时候当操作数是基本数据类型时比较的就是“值”。int a 5; int b 5;那么a b的结果是true这没任何毛病。问题出在引用类型上。当你用去比较两个对象时Java 比较的是这两个引用是否指向堆内存里的同一个对象也就是“地址”而不是对象内部的内容是否相同。举个例子字符串是工作中最常用的引用类型新手最经典的错误就是String username new String(admin); String input new String(admin); System.out.println(username input); // false明明内容都是admin但的结果是false因为它比较的是两个不同对象的地址。这就是热词里“比较运算符和赋值运算符”之外第二个高频面试点的来源比较的是什么equals()比较的又是什么。正确的做法是用equals()方法比较字符串的内容System.out.println(username.equals(input)); // true那!也有同样的坑。比较两个引用类型对象是否“内容不同”不能用!要用!字符串1.equals(字符串2)。这个知识点不只在写登录逻辑时有用在判断集合里有没有某个元素、在比对用户输入、在处理配置文件时都会反复踩到。还有一个和强相关的经典问题是包装类缓存。Integer a 127; Integer b 127;这时候a b是true因为Integer在-128 ~ 127范围内会缓存对象一旦超出这个范围写Integer a 128; Integer b 128;a b就变成了false。很多老程序员都被这个坑绊过新手面试时如果能把这个问题讲清楚会是一个很加分的亮点。2.3 运算符优先级为什么括号是你的好朋友Java 的运算符优先级是一个让新手头疼、中途也会偶尔迷糊的问题。和比较运算符相关的优先级关系你需要记住这么几条按从高到低排列算术运算符*、/、、-优先于所有比较运算符比较运算符、、、、、!优先于逻辑与逻辑与优先于逻辑或||所有比较运算符优先于赋值运算符。所以像a 0 a 100这样的表达式Java 会先算a 0再算a 100最后做运算。而a 0 || b 0 c 0则会先算b 0 c 0再和a 0做||运算这大概率不是你想要的逻辑。如果你希望先算a 0 || b 0再跟c 0组合就必须加括号。实际开发中我几乎不会完全依赖优先级表来写混合表达式。我的做法是只要表达式里同时出现两类以上的运算符就一律用括号把每个逻辑单元隔离出来。这不是因为不懂优先级而是因为代码是写给人看的其次才是给机器执行的。一个干净的分组表达式比靠记忆和推理才读得懂的写法要好维护得多。3. 从代码层面夯实概念三个可直接运行的小案例3.1 案例一商品价格区间判断为了让你真正把上面的知识用起来我准备了三个完整可运行的 Java 程序。第一个案例模拟电商系统里的商品价格区间分类输入一个价格输出它属于普通商品、中档商品还是高档商品。import java.util.Scanner; public class PriceCheck { public static void main(String[] args) { Scanner scanner new Scanner(System.in); System.out.print(请输入商品价格); double price scanner.nextDouble(); if (price 0 price 100) { System.out.println(普通商品); } else if (price 100 price 500) { System.out.println(中档商品); } else if (price 500) { System.out.println(高档商品); } else { System.out.println(价格无效); } scanner.close(); } }这个例子看起来简单里面其实藏着一个典型的边界条件思维。第一段判断写price 0 price 100它同时排除了负数、0 和大于 100 的价格第二段不用再写price 100因为能走到这个分支说明price 100已经不成立了也就是price 100必然成立这是else if帮我们隐式完成的逻辑。边界值 0、100、500 都已经被明确处理不会出现某一个价格掉进缝隙里没被任何分支接住的情况。新手自己能写出这样的分段判断说明对“比较”和“区间”已经有了正确的直觉。这个案例还有一个练手的好点子把price 0 price 100改成price 0 || price 100试试你会发现所有价格都会输出“普通商品”因为||在第一个条件为true时就直接短路了。自己动手改一改、跑一跑比死记“是并且、||是或者”要管用得多。3.2 案例二登录场景下的字符串比较第二个案例是你以后一定会写的登录校验逻辑。它专门用来巩固字符串比较的知识为什么不能用以及怎么写才是安全的。import java.util.Scanner; public class LoginDemo { public static void main(String[] args) { String correctAccount admin; String correctPassword 123456; Scanner scanner new Scanner(System.in); System.out.print(请输入账号); String account scanner.nextLine(); System.out.print(请输入密码); String password scanner.nextLine(); if (correctAccount.equals(account) correctPassword.equals(password)) { System.out.println(登录成功欢迎回来); } else { System.out.println(账号或密码错误); } scanner.close(); } }请注意这里我写的是correctAccount.equals(account)而不是account.equals(correctAccount)。把已知的字符串常量放在equals()的前面是一个很重要的防御习惯如果account是nullnull.equals()会直接抛出空指针异常导致程序崩溃但把常量放前面admin.equals(null)会返回false程序安全地走进错误分支。这个习惯在你以后解析用户输入、读取配置文件的时候会救你很多次。至于为什么不能用去比原理在第一部分已经说过Scanner.nextLine()返回的字符串是在堆里新建的对象和直接写在代码里的字符串常量不是同一个引用哪怕内容一模一样比较地址也会返回false。你可以自己把代码里的equals替换成跑一次亲眼看看登录永远失败的诡异现象这个印象会比任何理论都深。3.3 案例三学生成绩等级划分第三个案例是经典的“成绩分段”几乎每本 Java 入门教材都有类似题但很多人其实没把它写严谨。来看看这个版本import java.util.Scanner; public class GradeDemo { public static void main(String[] args) { Scanner scanner new Scanner(System.in); System.out.print(请输入成绩0-100); int score scanner.nextInt(); if (score 90 score 100) { System.out.println(优秀); } else if (score 80) { System.out.println(良好); } else if (score 70) { System.out.println(中等); } else if (score 60) { System.out.println(及格); } else if (score 0) { System.out.println(不及格); } else { System.out.println(成绩不合法); } scanner.close(); } }这里最关键的一步是第一行score 90 score 100它把 90 到 100 的完整区间框住了。后面的分支比如score 80为什么不需要再补一个score 90因为程序的执行是自上而下的如果score是 95它已经在第一个分支被拦截了根本不会走到第二个分支。能走到else if (score 80)说明score 90已经是既成事实所以良好覆盖的其实是 80 到 89 这个区间。这种“利用 else if 顺序简化区间表达”的思路在真实业务代码里非常常用。最后一个else用来处理负数或大于 100 的非法输入。你可能会问为什么不把合法性判断放在最前面单独做一个if当然可以那样逻辑也清晰。但这里我想演示的是用比较运算符写出不重不漏的分支条件本身就需要你脑子里对边界值有清晰的区间图。把 0、60、70、80、90、100 这些关键节点全部过一遍你才算真正掌握了这段代码而不是背下来的模板。4. 新手最容易踩的 5 个坑与排查思路4.1 把写成编译报错其实是保护这个坑在文首就出现过也是热词里“比较运算符和赋值运算符”最核心的矛盾点。在 Java 中是赋值运算符它的作用是把右边的值赋给左边的变量才是比较运算符。当你在条件判断里写下if (a 5)时Java 会先执行赋值操作把a变成 5然后整个赋值表达式的结果是 5也就是一个int类型。而if的条件要求必须是boolean于是编译器直接报错incompatible types。很多新手一见到这个报错就慌其实这是 Java 在保护你。有些语言里这种写法只是警告甚至能通过编译导致“永远为真”的隐蔽 bug排查起来极其痛苦。Java 直接用编译错误把问题拦在门外是好事情。这里有一个合法的例外写法值得提前认识。在读取文件的循环里你经常看到这样的代码String line; while ((line bufferedReader.readLine()) ! null) { System.out.println(line); }第一眼看上去很容易懵不是说不让在条件里写赋值吗注意看这个赋值表达式外头包了一层括号整体变成了一个字符串引用然后用! null去判断它到底为不为null。这里的不是用来替代的它是先完成“取出一行文本并赋值给 line”这个动作再拿赋值结果去做比较。括号绝不能省略否则优先级会把line (readLine() ! null)解析成把布尔值赋给字符串变量又是编译错误。这个写法和if (a 5)有本质区别但新手如果不理解“赋值表达式本身也有值”这个特性看多少遍都容易晕。4.2 用比较字符串明明内容一样却是 false这个坑在前面登录案例里已经重点讲过但它的变形版太多值得再单独列一次。除了Scanner输入你从配置文件读取的字符串、从数据库查出来的字符串、通过new String()创建的字符串和代码里直接写的字符串常量往往都不是同一个引用对象。把它们用比较结果基本都是false。排查的思路很简单看到两个字符串打印出来一模一样但是false第一个想到的应该是“它们不是同一个对象”然后改用equals()比较内容。如果你想把比较规则统一规范起来我建议在团队里约定只要比较的是字符串内容一律使用equals()不要用。唯一的例外是判断某个字符串变量是否为null比如str null这时候必须用因为equals方法在一个null引用上是无法调用的。4.3 浮点数直接比较相等0.1 加 0.2 不等于 0.3这个坑的概率极高而且原理有点反直觉。在 Java 里运行0.1 0.2 0.3结果是false。原因是浮点数在计算机内部使用二进制科学计数法存储而像0.1这样的十进制小数无法用二进制有限位数精确表示会存在一个无法消除的舍入误差。两个不精确的浮点数相加结果当然也不精确拿它去和另一个浮点常量比相等就很容易失败。正确的做法是设定一个误差范围只要两个数的差值足够小就认为它们相等double a 0.1; double b 0.2; double sum a b; double target 0.3; if (Math.abs(sum - target) 1e-9) { System.out.println(在误差范围内可以认为相等); }这个思路在金额计算、统计分析、物理模拟里都会用到。如果你在做精确的金额运算不要用double应该使用BigDecimal并用它的compareTo方法比较而不是equals因为BigDecimal的equals对精度敏感0.1和0.10会被判定为不相等但compareTo不会。这里稍微提一句给进阶的读者留个锚点。4.4 想把区间写成0 x 100结果编译不过数学里写“x 在 0 到 100 之间”很自然但 Java 不认这种连续比较。因为比较运算符是双目运算符0 x会先被计算得到的是一个布尔值true或false接下来这个布尔值去和100做运算Java 根本没有定义“布尔类型和整数做小于等于比较”的规则于是编译报错。正确的写法是分开比较再用逻辑与连接if (x 0 x 100) { // x 在 [0, 100] 区间内 }这个错误的变种是写成if (0 x 100)同样报错。新手一旦看到这类报错不要怀疑是 Java 坏了先快速把这个“连续比较不支持”的规则刻进脑子里。真正写过几年 Java 的人看到区间判断条件时会条件反射地写出两个比较加一个这就是肌肉记忆的力量。4.5 比较表达式里夹带“副作用”短路让你怀疑人生和||都有短路特性如果左边的结果已经能决定整个表达式的值右边就不再执行。这本身是优化和防御机制但它也可能制造诡异的现象。比如int count 0; if (count 0 count 1) { // 什么都不执行 } System.out.println(count); // 输出 0因为count 0是false整个表达式已经确定为false所以右边的count根本没有执行count保持为 0。如果你本来指望在判断过程中让count自增一次这里就会和你预期的不一样。我处理这种问题的原则很简单不要在比较表达式里写任何带副作用自增、自减、方法调用等的代码。比较逻辑就纯粹做比较状态变更放到判断完成之后单独写一行。这能避免绝大多数跟短路相关的坑也让代码更好读。如果真的需要“先执行某个操作再用它的结果做比较”那就老老实实拆成多个语句用中间变量承接结果别在一行里炫技。针对上面五个坑整理一个速查表方便你回头查阅现象原因正确处理if (a 5)编译报错是赋值运算符结果不是 boolean使用两个字符串内容相同却是 false比较引用地址不比较内容使用equals()0.1 0.2 0.3结果是 false浮点数二进制存储存在舍入误差使用Math.abs(sum - target) 1e-90 x 100编译失败Java 不支持连续比较语法使用x 0 x 100表达式右侧代码没有执行或||短路不要在比较表达式中写自增、方法调用等副作用5. 面试官常问的比较运算符考点清单5.1 高频面试题与答题框架比较运算符这个知识点看起来很基础但面试官特别喜欢从这个小切口往深处挖。我梳理了几个高频问题并附上最能体现你理解深度的答题框架。第一个必问题和equals()有什么区别一个比较稳妥的回答思路是分两层。第一层对基本类型比较的是值对引用类型比较的是对象地址第二层Object类里的equals()方法默认行为其实也是比较地址但很多类如String、Integer重写了它改为比较内容。如果能补一句“所以我们在业务开发中比较字符串内容要习惯用equals()而判断一个对象是否为null必须用”这个回答基本就是满分了。第二个问题Java 中浮点数0.1 0.2 0.3的结果是什么为什么先说出答案false再解释 IEEE 754 浮点数在二进制表示下无法精确表达很多十进制小数存在舍入误差最后给出解决方案——用误差范围比较或者用BigDecimal处理精确金额。这个回答能展现出你不仅知道现象还知道原因和解法面试官往往会接着往BigDecimal的equals和compareTo区别上追问这就是你展示深度的机会。第三个问题比较运算符的运算结果是什么类型这个问题看起来简单很多人脱口而出“布尔值”但要注意强调它是boolean类型的值可以直接赋值给布尔变量也可以参与、||、!等逻辑运算。这个知识点在面试里通常不会单独出现而是作为后续问题的一个铺垫。第四个问题能不能直接用比较两个字符串的大小正确答案是不可以。因为 Java 中的、、、只支持数值类型和char类型字符串是引用类型不能用这些运算符直接比较大小。如果确实需要按字典序比较字符串应该使用str1.compareTo(str2)返回值是负数、0 还是正数就对应小于、等于、大于。顺着这个方向你还可以去了解Comparator和Comparable这些在容器排序时会大量遇到。5.2 从“会用”到“讲明白”的提升路径面试题的背后其实是对基础概念通透程度的考察。很多新手背了八股文却写不出正确的代码也有人代码写得很溜但一被问“为什么”就卡壳。从“会用”到“讲明白”我建议按这个路线走。先把基础语法跑通。如果你连 Java 环境变量都没有配好那今天的所有代码都没法运行。装好 JDK 后在命令行输入java -version和javac -version能正确显示版本号说明编译环境已经就绪。然后把本文的三个案例亲手敲一遍注意是“敲”不是复制粘贴。敲的过程中你会被迫注意每一个括号、每一个分号、每一个运算符的位置这本身就是很好的肌肉记忆训练。接着做一个稍大一点的小项目练手比如猜数字游戏、简易登录注册系统。这类项目天然需要大量的条件判断猜大猜小、账号是否重复、密码强度是否达标、验证码是否正确。每一个校验逻辑都是比较运算符的实战场景。等你能把项目里的判断条件写得清晰、完整、覆盖边界时基础阶段的目标就达成了。最后面试前把几个高频追问练熟。找身边的朋友扮演面试官让他连续追问和equals区别字符串常量池知道吗Integer缓存了解吗约等于号什么时候为true浮点数精度怎么处理连环问也是面试常态平时练习得越多现场越不慌。我这些年带过不少新人有个规律百试百灵前期把比较运算符这种最基础的东西理解到位后面学循环、学集合、学 Stream 流都会顺畅得多。那些一上来就冲框架、冲中间件的人往往过几个月还得回头补基础课的债。所以别嫌“比较运算符”听起来简单它值得你认真花一个晚上搞明白。再补一个我个人很推荐的小习惯写代码时给所有混合表达式加括号包括算术和比较混在一起的情况、比较和逻辑混在一起的情况。这不是能力不够而是对读代码的人负责。宁可让表达式看起来多几个括号也不要让同事陷入“猜优先级”的泥潭。代码写出来是给人看的只是顺便交给机器执行。希望今天这篇内容能让你在 Java 这条路上走得再稳一点。
返回列表