【Java】字符串相等判断:从底层原理到实战避坑

大家好呀!作为一名深耕 Java 后端的开发者,今天想和大家聊聊一个看似基础却极易踩坑的问题 ——字符串相等判断。相信不少同学在刚接触 Java 时,都曾被 ==equals() 搞得晕头转向,甚至在实际开发中因为混用导致线上 Bug。这篇文章就结合 JDK 源码和实战案例,把字符串相等判断的底层逻辑和最佳实践讲透,文末还有常见面试题解析哦~

一、核心疑问:==equals() 到底差在哪?

这是字符串比较最基础也最关键的问题,我们直接从本质 + 源码双维度拆解:

1. == 操作符:比较的是「对象地址」

== 是 Java 的原生运算符,它的作用只有一个:判断两个变量是否指向同一个对象(即内存地址是否相同)

对于基本数据类型(int、char 等),== 比较的是值;但对于引用数据类型(String、自定义对象等),== 比较的是对象在堆内存中的地址。

2. equals() 方法:比较的是「对象内容」(需重写)

equals() 是 Object 类的方法,默认实现其实就是 ==

// Object类的equals()源码(JDK8/JDK17一致)
public boolean equals(Object obj) {
    return (this == obj); // 直接比较地址
}

但 String 类重写了 equals() 方法,核心逻辑是「先判地址,再判类型,最后逐字符比较内容」。我们来看关键源码:

JDK8 String.equals () 源码:

public boolean equals(Object anObject) {
    // 1. 先判断是否是同一个对象(地址相同),是则直接返回true
    if (this == anObject) {
        return true;
    }
    // 2. 判断参数是否是String类型,不是则返回false
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        // 3. 长度不同直接返回false
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 4. 逐字符比较
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

JDK17 String.equals () 源码优化:

JDK17 简化了逻辑,且针对不同字符编码(UTF16/Latin1)做了优化,但核心思想不变:

public boolean equals(Object anObject) {
    // 地址相同直接返回true
    if (this == anObject) {
        return true;
    }
    // 仅处理String类型
    if (anObject instanceof String s) {
        // 先比较编码和长度,再逐字符对比
        return cmp(s.value, s.coder) == 0;
    }
    return false;
}

二、关键前提:字符串常量池的影响

理解字符串比较,必须先搞懂「字符串常量池」—— 这是 JVM 为了优化字符串创建而设计的缓存机制,也是很多面试题的考点!

1. 字符串创建的两种方式差异:

创建方式内存分布是否复用常量池
String s = "云扬"直接从常量池获取(没有则创建)
String s = new String("云扬")堆内存新建对象,常量池可能同步创建

2. 代码示例:常量池对比较结果的影响

public class StringPoolDemo {
    public static void main(String[] args) {
        // 方式1:直接赋值,复用常量池
        String s1 = "云扬";
        String s2 = "云扬";
        
        // 方式2:new关键字,堆内存新建对象
        String s3 = new String("云扬");
        String s4 = new String("云扬");
        
        // 比较结果分析
        System.out.println(s1 == s2); // true(同一常量池对象,地址相同)
        System.out.println(s1 == s3); // false(s1在常量池,s3在堆,地址不同)
        System.out.println(s3 == s4); // false(两个不同的堆对象,地址不同)
        System.out.println(s1.equals(s3)); // true(内容相同)
    }
}

三、进阶技巧:intern() 方法的妙用

intern() 是 String 类的 native 方法,作用是「将字符串对象加入常量池(若不存在则创建),并返回常量池中的引用」。

代码示例:intern () 对比较结果的改变

public class InternDemo {
    public static void main(String[] args) {
        String s1 = new String("云扬"); // 堆对象,常量池已创建"云扬"
        String s2 = s1.intern(); // 返回常量池中的"云扬"引用
        String s3 = "云扬"; // 直接引用常量池对象
        
        System.out.println(s1 == s3); // false(s1在堆,s3在常量池)
        System.out.println(s2 == s3); // true(s2和s3都指向常量池)
    }
}

注意:JDK7 后,intern () 不会再复制字符串到永久代,而是直接记录堆对象的引用,优化了内存占用。

四、更安全的比较方案:Objects.equals () 和 contentEquals ()

实际开发中,直接使用 String.equals() 可能遇到空指针问题,而 contentEquals() 支持更多字符序列类型,我们来看这两个实用方法:

1. Objects.equals ():避免空指针的神器

Objects.equals() 是 JDK7 新增的静态方法,底层逻辑:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

代码示例:空指针场景对比

public class SafeEqualsDemo {
    public static void main(String[] args) {
        String s1 = null;
        String s2 = "云扬";
        
        // 直接使用equals():空指针异常!
        // System.out.println(s1.equals(s2)); // NullPointerException
        
        // 使用Objects.equals():安全无异常
        System.out.println(Objects.equals(s1, s2)); // false
        System.out.println(Objects.equals(s2, "云扬")); // true
        System.out.println(Objects.equals(null, null)); // true(特殊场景支持)
    }
}

2. contentEquals ():支持多字符序列类型

contentEquals() 可以比较 String 与任何实现 CharSequence 接口的对象(如 StringBuffer、StringBuilder),且会处理同步(针对 StringBuffer)。

代码示例:contentEquals () 的灵活用法

public class ContentEqualsDemo {
    public static void main(String[] args) {
        String s = "云扬的博客";
        StringBuilder sb = new StringBuilder("云扬的博客");
        StringBuffer sbf = new StringBuffer("云扬的博客");
        
        // 支持StringBuilder/StringBuffer,无需手动转String
        System.out.println(s.contentEquals(sb)); // true
        System.out.println(s.contentEquals(sbf)); // true
        
        // 对比:equals() 不支持其他类型
        System.out.println(s.equals(sb)); // false(类型不同)
    }
}

注意:contentEquals () 会先判断长度,再逐字符比较,性能略低于 equals (),非必要场景无需使用。

五、常见面试题解析(含代码验证)

结合前面的知识点,我们来拆解几道高频面试题:

题目结果核心原因
"云扬" == "云" + "扬"true编译期优化:”云”+”扬” 直接拼接为 “云扬”,与常量池对象地址相同
"云扬" == new String("云扬")false前者在常量池,后者在堆,地址不同
new String("云扬").equals("云扬")trueequals () 比较内容,与内存地址无关
new String("云扬").intern() == "云扬"trueintern () 返回常量池引用,与直接赋值的对象地址相同

代码验证:

public class InterviewDemo {
    public static void main(String[] args) {
        System.out.println("云扬" == "云" + "扬"); // true(编译期优化)
        System.out.println("云扬" == new String("云扬")); // false(地址不同)
        System.out.println(new String("云扬").equals("云扬")); // true(内容相同)
        System.out.println(new String("云扬").intern() == "云扬"); // true(intern()作用)
    }
}

六、最佳实践总结

  1. 比较字符串内容:优先使用 Objects.equals(s1, s2)(避免空指针);
  2. 确定非空场景:可直接用 s1.equals(s2)(性能略优);
  3. 比较 String 与 StringBuilder/StringBuffer:用 contentEquals()
  4. 绝对不要用 == 比较字符串内容(仅用于判断是否为同一对象);
  5. 字符串创建:优先用 String s = "xxx"(复用常量池,节省内存),避免频繁用 new String()

以上就是字符串相等判断的全部核心知识点啦!如果大家在实际开发中遇到相关问题,或者有其他想深入了解的 Java 技术点,欢迎在评论区留言交流~

Tags:

发表回复

Your email address will not be published. Required fields are marked *.

*
*