深入理解java中“==”和“equals”的区别

了解 Java 中 == 运算符和 equals() 方法之间的主要区别,并了解在 Java 编程中如何有效地使用它们进行对象比较。

ini 复制代码
public class Demo {
    public static void main(String args[]){
        String a = "a" + "b" + 1;
        String b = "ab1";
        System.out.println(a == b);
    }
}

根据上面的代码,请看下列问题:

a和b在内存中的表示是什么?

编译时有什么优化策略吗?

"=="

在Java语言中,==用于比较两个内存单元的内容是否相同。

对于诸如 byte、boolean、short、char、int、long、float 和 double 等基本数据类型,比较的是它们的值。

对于引用类型,比较的是引用的值,可以将其视为对象的逻辑地址。

当 == 运算符应用于两个引用类型时,它会比较两个对象各自的内存地址是否相同。

也就是说,如果两个引用指向同一个对象,则返回true;否则,返回 false。

typescript 复制代码
public class Demo {
    public static void main(String args[]){
        List<String> a = null;
        List<String> b = null;
        System.out.println(a == b);
    }
}


结果是:true

"equals"

下面是Object类中equals()的源代码。

typescript 复制代码
public boolean equals(Object obj) {
    return (this == obj);
}

换句话说,如果没有重写 equals() 方法,并且相应类的父类也没有重写 equals() 方法,则 equals() 默认就是比较对象的地址。

equals()方法的存在是为了鼓励子类重写它,从而实现基于值的比较。

掌握了基础知识后,现在让我们解决文章开头提出的两个问题。

a和b在内存中的表示是什么?

由于 Java 的编译优化策略,a 和 b 都引用相同的内存位置。 我们可以使用JD-GUI反编译工具来检查反编译后的代码

typescript 复制代码
public class Demo
{
  public static void main(String[] args)
  {
    String a = "ab1";
    String b = "ab1";
    System.out.println(a == b);
  }
}

结果是:true

到这里,结果应该就很清楚了。(==比较的内存地址) JVM在编译时对常量折叠进行了优化,因为常量折叠的结果是一个固定值,不需要运行时计算,所以通过这种方式进行优化。

不过,不要急于下结论。 JVM 只优化它能优化的部分。它并没有优化所有方面。

例如,前面提到的字符串连接。如果字符串的连接过程中有变量,意味着编译时不知道具体值,则 JVM 不会执行这种编译时合并。

如果你已经理解了上一段的意思,我们再来看另一个例子:

ini 复制代码
public class Demo {
    public static void main(String args[]){
        String a = "a";
        final String c ="a";

        String b = a + "b";
        String d = c + "b";
        String e = getA() + "b";
        String compare = "ab";

        System.out.println( b == compare);
        System.out.println( d == compare);
        System.out.println( e == compare);
    }
    
    private static String getA(){
        return "a";
    }
}
结果是:
false
true
false

根据我们之前的解释,很容易理解为什么 b == Compare 和 e == Compare 会产生错误结果。

这是因为 a 和 getA() 不是常量,所以并且没有对此进行编译时优化。相反,c是一个final的不可变的常量,所以进行了编译优化。

我们可以通过使用 JD-GUI 检查反编译代码来验证这一点:

ini 复制代码
import java.io.PrintStream;

public class Demo
{
  public static void main(String[] args)
  {
    String a = "a";
    String c = "a";
    
    String b = a + "b";
    String d = "ab";
    String e = getA() + "b";
    String compare = "ab";
    
    System.out.println(b == compare);
    System.out.println(d == compare);
    System.out.println(e == compare);
  }
  
  private static String getA()
  {
    return "a";
  }
}

从反编译的代码中,我们可以确认b和e没有经过JVM编译优化。

看到的是变量 d 已经被 JVM 优化了。 关键的区别在于变量 c 用 Final 修饰符修饰。 该声明对 c 的不可变性施加了严格的约束,并且由于 Final 意味着不可变性,因此编译器自然地知道结果也是不可变的。

字符串在内存里怎么存储?

字符串对象内部使用字符数组进行存储。

现在,让我们看一下以下示例:

ini 复制代码
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");

这段代码在内存中怎么分配?大致是这样的:

  • 它会分配一个长度为11的char数组,并在字符串池中创建一个由该char数组组成的字符串。然后,m 引用该字符串。
  • 使用n引用字符串池中的字符串,因此n和m引用同一个对象。
  • 生成一个新字符串,但其内部字符数组引用与 m 相同的数组。
  • 同样,创建了一个新字符串,但其内部字符数组引用了字符串池中的字符数组,即与u相同。

结论是m和n是同一个对象,而m、u和v是不同的对象,但它们都共享相同的字符数组。

使用equals()方法进行比较时,也会返回true。

我们可以使用反射来修改字符数组来验证效果。

ini 复制代码
public class Demo {
    public static void main(String args[]) throws NoSuchFieldException, IllegalAccessException {
        String m = "hello,world";
        String n = "hello,world";
        String u = new String(m);
        String v = new String("hello,world");
        Field f = m.getClass().getDeclaredField("value");
        f.setAccessible(true);
        char[] cs = (char[]) f.get(m);
        cs[0] = 'H';
        String p = "Hello,world";
        System.out.println(m.equals(p));
        System.out.println(n.equals(p));
        System.out.println(u.equals(p));
        System.out.println(v.equals(p));
    }
}
结果是:
true
true
true
true

从上面的例子中,我们可以看到,通常说字符串是不可变的时,这意味着对字符串的引用是不可更改的,类似于其他final类。

虽然String类没有暴露它的value字段,但是仍然可以通过反射来修改它。

关于String中的intern()方法

ini 复制代码
public class Demo {
    public static void main(String args[]){
        String a = "a";
        String b = a + "b";
        String c = "ab";
        String d = new String(b);
        System.out.println(b == c);
        System.out.println(c == d);
        System.out.println(c == d.intern());
        System.out.println(b.intern() == d.intern());
    }
}

结果是:
false
false
true
true

String引用指向的对象存储在常量池中,保证具有相同值的字符串全局唯一。

如何确保这种全局唯一性?

当调用 intern() 方法时,JVM 使用 equals() 方法检查常量池中是否存在具有相等值的 String。

如果找到,则返回常量池中 String 对象的引用。如果没有找到匹配的字符串,它会创建一个具有相同值的新字符串,并在常量池中返回对此新字符串的引用。

只要两个字符串相同,对它们调用 intern() 就会产生对常量池中相应 String 的引用。 因此,在调用intern()之后,可以使用相等运算符来匹配两个字符串

相关推荐
StevenLdh4 分钟前
Java Spring Boot 常用技术及核心注解
java·开发语言·spring boot
考虑考虑12 分钟前
JDK21中的Switch模式匹配
java·后端·java ee
IT利刃出鞘18 分钟前
maven--依赖的搜索顺序
java·maven
Victoria Zhu1 小时前
零基础小白如何系统学习Spring Boot
spring boot·后端·学习
Aska_Lv1 小时前
mybatis---MybatisPlus自定义insertBatchSomeColumn实现真正批量插入
后端·架构
ThisIsClark1 小时前
【gopher的java学习笔记】如何知道一个jar包对应的maven中的groupId和atrifactId
java·笔记·学习
XU磊2601 小时前
《Java SQL 操作指南:深入理解 Statement 用法与优化》
java·数据库·sql
工一木子1 小时前
【IDEA插件开发】IntelliJ IDEA 插件开发指南
java·ide·intellij-idea
Summer_star_summer1 小时前
代码随想录-回溯
java·数据结构·算法