新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java String比较:==与equals的区别及字符串常量池原理

发布时间:2026/9/29 17:07:28来源:尧图网络
Java String比较:==与equals的区别及字符串常量池原理
先讲个我上个月帮同事排查的真实bug。测试环境一切正常部署到生产之后突然冒出一批登录失败的工单查日志发现是账号密码校验环节直接返回了用户名或密码错误。代码本身并不复杂if (user.getPassword() 123456) { // 放行 }同事当时很困惑密码明明就是123456日志里也打印出来了为什么判不等我一看这行代码就明白了——又是String用比较惹的祸。这个问题在线下教学、面试里被讲到快包浆了但实际工程里它仍然反复出现。今天就把String的和equals()之间的差异彻底掰开揉碎聊清楚顺带把字符串常量池、不可变性这些底层逻辑一并讲透希望能帮大家少走几趟弯路。1. 一个线上bug引发的疑问两个内容相同的String为什么判不相等1.1 事故现场登录校验为什么忽然失灵当时那个项目是个典型的Spring Boot服务用户提交的密码经过前后端传输之后在后端拿到了一个String对象。代码里写的是user.getPassword() 123456这里是一个典型的比较场景左边是运行时从数据库或请求里读出来的字符串对象右边是代码里直接写的字符串字面量。大家拿到这段代码第一反应可能是这也能错——但它就是错了。原因倒不是因为数据里有不可见字符我们特地把两个值转成字节数组比对过内容一模一样也不是编码问题日志里中文注释和各种符号都正常。真正的关键在于在Java里比较的对象是两个引用而不是两个对象的内容。左边和右边虽然长得一样但它们并不是同一个对象。1.2 排查链路从打印值到怀疑运算符再到确认引用问题当天我们的排查过程大致是这样的先看日志确认两边都在放行判断之前打印了密码。日志里显示都是123456所以初步排除数据不一致。用password.equals(123456)临时替换再跑一遍测试登录立刻恢复了正常。到这一步问题基本锁定在运算符的使用上。为了彻底搞懂为什么失败我写了一段最小复现代码把两个变量的内存地址相关特征打出来看确认它们指向的不是同一个实例。这个排查链路和很多新手自己踩坑的过程几乎一模一样。不同的是当时没时间停下来想原理先把问题解决掉回过头来值得把原理说透。在Java里到底比的是什么取决于变量的类型如果比较的是基本类型int、char、boolean等比较的是它们的值。比如3 3结果是true这个没争议。如果比较的是引用类型String、Object、数组等比较的是引用地址也就是这两个变量是否指向同一个对象实例。所以user.getPassword() 123456实际在问的是从数据库/请求解析出来的这个密码对象和代码区常量池里的那个字符串对象是不是同一个实例答案通常是否定的——前者是运行时创建的新对象后者是编译时就确定好的常量池里的对象。2. 拆开JVM内存看本质比的是地址equals()比的是内容看到这里很多人会问那equals()又是怎么回事为什么它就能比内容要回答这个问题得从String对象在JVM内存里的分布说起。2.1 两种创建String的方式内存里发生了什么Java里创建String最常见的两种方式区别非常关键。第一种是直接赋值String s1 abc; String s2 abc;这种方式声明的字符串会进入字符串常量池。JVM在加载类、执行到这行代码时会先检查常量池里有没有abc这个字符串。如果没有就创建一个对象放进常量池如果有直接复用。因此s1和s2实际上指向同一个对象用比较结果是true。第二种是new关键字String s3 new String(abc);new String(abc)强制在堆内存中创建一个全新的String对象。即使常量池里已经有abc堆里这个也是独立的副本内部char数组会引用常量池的内容但对象本身是不同的。所以s1 s3结果必然是false因为s1指向常量池里的对象s3指向堆里的对象两者根本不是一个实例。用一张通俗的图来理解常量池就像一个公共仓库直接赋值是每人发一张指向仓库里同一箱货的提货单而new是每次都从仓库里搬一箱货放到你自己的工位旁边就算货品一模一样箱子也是独立的。2.2 String不可变性与常量池的配合逻辑这里有一个很自然的疑问为什么Java要搞一个字符串常量池直接每次都用new不行吗答案和String的不可变性紧密相关。String被设计为不可变类——它的char[]是用final修饰的类本身也是final的一旦创建内容就不能再修改。正因为内容不会变常量池里的对象才可以被安全地共享复用。设想一下如果String可变两个变量指向同一个abc其中一个把它改成def另一个变量也会跟着变程序就会到处出乱子。Java通过不可变性保证了共享的安全性常量池共享也才有了根基。equals()则是另一个维度的机制。Object基类里的equals()默认用的还是但String重写了这个方法重写后的逻辑是先判断两个引用是否是同一个对象如果是直接返回true一个快速路径优化。判断传入对象是否是String类型不是直接返回false。比较两个字符串的长度长度不同直接返回false。逐个比较char数组里的字符全部相同才返回true。简单说equals()是一个内容层面的比较方法它不关心变量指向的是常量池还是堆只关心它们包含的字符序列是否一致。这也正是我们比较用户密码、用户名、订单号等业务内容时要用它的原因。3. 六种常见String比较场景实测哪些会相等哪些必然不等理论讲清楚了但不少读者可能还是会有种道理都懂上手就懵的感觉。我干脆把所有常见比较场景写成一段测试代码逐一验证结果放在一起看就一目了然了。3.1 测试代码覆盖六种典型比较场景public class StringCompareTest { public static void main(String[] args) { // 场景一两个直接赋值的字面量 String s1 abc; String s2 abc; System.out.println(场景一 s1 s2: (s1 s2)); // true // 场景二字面量 vs new创建 String s3 new String(abc); System.out.println(场景二 s1 s3: (s1 s3)); // false System.out.println(场景二 s1.equals(s3): s1.equals(s3)); // true // 场景三变量与由变量拼接得到的字符串 String s4 ab; String s5 s4 c; System.out.println(场景三 s1 s5: (s1 s5)); // false // 场景四字面量之间拼接编译期常量折叠 String s6 ab c; System.out.println(场景四 s1 s6: (s1 s6)); // true // 场景五final修饰的变量参与拼接 final String s7 ab; String s8 s7 c; System.out.println(场景五 s1 s8: (s1 s8)); // true // 场景六调用intern()后回到常量池 String s9 new String(abc).intern(); System.out.println(场景六 s1 s9: (s1 s9)); // true } }3.2 结果解读与规律总结跑完这段代码输出结果如下场景代码形态结果原因一abc与abc直接赋值true常量池复用同一个对象二abc与new String(abc)false常量池对象 vs 堆对象三abc与s4 cs4是变量false运行时拼接产生新对象四abc与ab c字面量拼接true编译期常量折叠等同于abc五abc与final String s7 ctruefinal常量在编译期可确定折叠优化六abc与new String(abc).intern()trueintern()强制返回常量池引用几个关键规律建议直接记在心里直接赋值的字面量之间比较往往是true因为它们在常量池里只有一个实例。凡是运行时生成的字符串从方法返回值、变量拼接、new出来的、从IO读取的大概率不在常量池里和字面量就会得到false。编译期能确定的拼接纯字面量相加、final变量相加会被编译器直接折叠成一个常量不会生成新对象。intern()可以把一个堆里的String注册进常量池并返回池中的引用但日常业务代码里极少真的需要手动调它它更多出现在面试题和JVM调优场景里。顺带说一句场景三的String s5 s4 c底层实际是编译器生成了类似new StringBuilder().append(s4).append(c).toString()的字节码toString()返回的是一个全新的String对象。这就是为什么内容一样但引用不同的情况在变量拼接场景下几乎必然出现。4. 写代码时的正确姿势与那些年我们踩过的坑4.1 比较字符串正确写法的优先级排序基于上面所有分析日常业务代码里比较字符串内容的正确姿势我按推荐程度排个序最推荐Objects.equals(a, b)。这个静态方法内部做了空值处理a和b都可以为null而不会抛空指针是真正省心的写法。如果你的项目的JDK版本在7以上直接用就好。次推荐固定内容.equals(变量)。把字面量放在前面调用equals()就算变量是null结果也只是false不会抛NullPointerException。这是很多老项目的惯例写法。不推荐变量.equals(固定内容)。如果变量是从数据库、网络请求等外部渠道来的它有可能是null这行代码会直接抛空指针。更不推荐变量 固定内容。除非你有十足把握变量一定来自常量池否则这就是一颗定时炸弹。// 推荐写法1Objects.equals if (Objects.equals(user.getPassword(), 123456)) { // 放行 } // 推荐写法2常量在前 if (123456.equals(user.getPassword())) { // 放行 }还有一种需要忽略大小写的场景比如校验验证码验证码一般不希望用户区分字母大小写可以调用equalsIgnoreCase()。这个方法同样建议把固定内容放前面。4.2 三个容易被忽略的关联坑除了String本身的比较我在代码审查和实际排障中还发现三个高频关联坑值得一并提醒。第一个坑包装类型的比较。Integer等包装类型和String有类似的机制。Integer对-128到127之间的值做了缓存所以Integer a 100; Integer b 100; a b结果是true但a 200时又变成false。很多同学刚刚搞懂String转头又栽在Integer上。记住一点包装类型的比较统一用equals()或者直接用基本类型做桥梁。比如拆箱成int再比较就完全没有歧义。第二个坑StringBuffer/StringBuilder的toString()结果。经常有同学问为什么new StringBuffer(abc).toString()和abc用比较是false答案和场景三原理一样——toString()是运行时才执行的方法返回的是堆里的新对象。这类场景就是典型的内容明明一样但就是false。记住只要是方法返回值就别指望它和字面量是同一个对象。第三个坑在HashMap/HashSet里当Key的字符串。有人担心既然不靠谱那HashMap用String做Key会不会出问题完全不会。HashMap查找时用的是key.hashCode()定位桶然后用key.equals()比较实际内容这也正是hashmap get和put原理 什么情况用equals比较这个热搜问题的答案。正因为equals()被String正确重写String才能安全地作为Map的Key使用。你用new String(abc)和abc分别put和get完全等价。4.3 代码审查时我会重点盯的几类写法最后分享一点个人经验。做代码审查时看到下面这几类写法我基本都会要求修改或讨论在if条件中用比较两个从外部传入的String。用new String(xxx)这种绕一圈的创建方式除非有特殊原因比如需要截取原字符串的子串并避免保留原字符数组。大量字符串拼接却没有使用StringBuilder或StringBuffer。比如在循环里用拼字符串每次循环都会生成新的StringBuilder和String对象性能堪忧。对业务数据调用intern()。除非你能明确说出内存收益否则不要在生产代码里做这种操作它可能把常量池塞满。顺带一提区分StringBuffer和StringBuilder也很简单StringBuffer的方法是synchronized修饰的线程安全但慢一点StringBuilder非线程安全但单线程场景下性能更好。平时业务代码里大多数场景用StringBuilder就对了。我在实际项目中见过最典型的误用是有人把数据库里的值取出来之后挨个和几个枚举名做比较线上数据一变就炸。究其根本还是对引用比较和内容比较两个概念的区别没有形成肌肉记忆。在Java里的语义是同一个对象而业务世界里的相等通常是同样的内容这两个概念之间隔着一个字符串常量池和一段堆内存。想明白这一点再看任何比较相关的问题思路都会清晰很多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Claude在汽车研发中的实战应用:从需求梳理到代码审查的AI提效指南 2026/9/29 18:12:38

Claude在汽车研发中的实战应用:从需求梳理到代码审查的AI提效指南

1. 研发日常的真实痛点:时间都去哪儿了干了十多年汽车研发,最深的感受不是技术难,而是杂——需求变更、会议对齐、文档编写、测试数据整理,这些事务性工作占据的时间远远超乎你的想象。真正用来构思方案、推导边界条件、优化控制策…

阅读更多 →
技术平权:一人公司如何借助OPC UA实现工业数据采集平民化 2026/9/29 18:12:38

技术平权:一人公司如何借助OPC UA实现工业数据采集平民化

1. 技术平权,把“公司”压缩进一个人过去几年我观察到一个很有意思的反常识现象:一边是大厂裁员消息不断,一边是身边越来越多朋友以“一人公司”的方式活得越来越滋润。这里的一人公司并不是法律意义上那种只有一个股东的空壳,而是…

阅读更多 →
Unity大世界地形性能优化:GPU Terrain与Compute Shader实战 2026/9/29 18:12:38

Unity大世界地形性能优化:GPU Terrain与Compute Shader实战

1. 大世界地形渲染的痛点与GPU Terrain的破局思路做Unity大世界项目的朋友,大概率都经历过这样的场景:场景里铺了几十平方公里的地形,美术刷完高度图之后,运行起来帧率直接掉到20以下,打开Profiler一看,Ter…

阅读更多 →
微信小程序BLE广播扫描与解析:Beacon展位导览完整方案 2026/9/29 18:12:38

微信小程序BLE广播扫描与解析:Beacon展位导览完整方案

年初做室内展位导览项目时,遇到一个很典型的需求:一台自研Beacon设备摆在展位旁边,用户拿出微信小程序扫码进场,页面要自动识别当前展位并推送对应内容。技术链路并不复杂——Beacon在广播数据,小程序在扫描附近的蓝牙…

阅读更多 →
Jev大语言模型从申请密钥到API接入与Codex配置完整指南 2026/9/29 18:12:30

Jev大语言模型从申请密钥到API接入与Codex配置完整指南

最近后台收到好多条消息,都在问同一个词——Jev。这个模型的热度确实上来了,我算是最早一批开始折腾它的人,从申请密钥到日常调用,再到在Codex里跑通完整流程,前前后后踩了不少坑。今天干脆把这几天积累的经验整理成一…

阅读更多 →
IOTE 2021上海站观察:物联网从卖连接到卖结果,三层架构与AIoT成主流 2026/9/29 18:12:30

IOTE 2021上海站观察:物联网从卖连接到卖结果,三层架构与AIoT成主流

1. IOTE 2021上海站现场,我看到了什么2021年4月底的上海世博展览馆,我挎着采访本在展馆里泡了整整两天。IOTE上海站算是国内物联网圈子里资历很老、覆盖面也很全的行业展会,从RFID、传感器、通信模组到云平台、行业应用方案,基本把…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉