Java常见面试题——聊一聊==和equals的区别。

引言

笔者之前参加过一些Java的面试,事先hr都会给一套笔试题做一做。而笔试题里出现频率最高的一道题就是Java里面的= =和equals相关的东西。
注:==在markdown编译器有其他意义,显示有问题,所以此处使用= =代替
出题的方式一般两种:

  • 第一种:选择题形式;给一小段代码,里面多个String变量或者Integer(int)变量定义相同的值,使用 = =或者equals进行比较然后给出各种选项判断true或者false。
  • 第二种:问答题形式;简单粗暴,考题形式如下:= =和equals的关系和区别。

无论是这两种题型的哪一种,都埋着各种坑,都不会是简单的送分题,如果对Java虚拟机数据的存储方式或者Java基础知识没有清醒的认识,可能很难回答正确。
笔者在未完全了解这两种比较方式的时候,曾经给出的答案:
== 比较的是两个变量的地址,equals比较的是两个变量的值。
当时还深以为然,不觉得有什么错。随着 知识的增进,发现自己的答案是漏洞百出,笔者会在本文最后给出一个相对完整准确的答案。

Java常量池

了解Java常量池是解答上述问题的一个关键知识点
简单回顾一下《Java虚拟机规范》中对运行时数据区的内存区域的划分

  • 程序计数器是jvm执行程序的流水线,存放一些跳转指令。
  • 本地方法栈是jvm调用操作系统方法(本地方法native)所使用的栈。
  • 虚拟机栈是jvm执行java代码所使用的栈。
  • 方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置。
  • 堆是存放对象实例和被垃圾收集器管理的内存区域。

由上我们可以得知Java常量池存在于方法区中。
Java中的常量池分为两种:静态常量池和运行时常量池。

  • 静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名;字段名称和描述符;方法名称和描述
  • 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
    运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
    String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

字符串常量池

JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。

  • 为 了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串不在池中,就会实例化一个字符串并放到池中。并在堆中创建该对象实例。
  • 如果字符串已经存在池中, 就返回池中的实例引用。不再在堆中重复创建

Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。

关于Java中的 ==

案例

话不多说,上代码

package test36;
public class Main {
    public static void main(String[] args) {
        System.out.println("**********第一组************");
        String abc = "abc";
        String def = "abc";
        System.out.println(abc == def);
        System.out.println("**********第二组************");
        String abcd = new String("def");
        String defg = new String("def");
        System.out.println(abcd == defg);
        System.out.println("**********第三组************");
        String abcde = "abcde";
        String defgh = new String("abcde");
        System.out.println(abcde == defgh);
        System.out.println("**********第四组************");
        int a=1;
        Integer b = new Integer(1);
        System.out.println(a==b);
        System.out.println("**********第五组************");
        Integer c = new Integer(1);
        Integer d = new Integer(1);
        System.out.println(c==d);
        System.out.println("**********第六组************");
        Integer e = new Integer(1);
        Integer f = 1;
        System.out.println(e==f);
        System.out.println("**********第七组************");
        Integer g = 100;
        Integer h = 100;
        System.out.println(g==h);
        System.out.println("**********第八组************");
        Integer j = 128;
        Integer k = 128;
        System.out.println(j==k);
        System.out.println("**********第九组************");
        Integer m = Integer.valueOf(128);
        Integer n = 128;
        System.out.println(m==n);
        System.out.println("**********第十组************");
        Integer q = Integer.valueOf(100);
        Integer p = 100;
        System.out.println(q==p);
    }
}

可以先试着猜一猜,后面给出运行结果
在这里插入图片描述

答案解析

第一组:Java中的字符串定义变量的方式有两种,一种是直接赋值,一种使用new关键字实例化。如下:

扫描二维码关注公众号,回复: 11167558 查看本文章
  • 直接赋值:String str = “Hello World”;
  • 构造方法实例化:String str = new String(“Hello World”);

在Java中,String不属于8种基本数据类型之一,所以其定义的变量属于对象,所以= =比较的是内存地址。
在了解完字符串常量池后,在两个String定义变量使用直接赋值的情况下,答案就很清晰了。所以为true。
第二组使用new关键字实例化,都会在堆中开辟一块内存。地址不同,所以为false。同理,第五组也是。
第三组:一个是方法区中字符串常量池中对象的地址,另一个是堆中对象的地址。因此是false。第六组同理也是。
第四组:Integer是int的包装类,int则是java的一种基本数据类型 。包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较。(基本数据类型 = = 比较的数据的值,引用数据类型使用 = =比较的对象地址的值)
第七,八,九,十组
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);
在这里插入图片描述
java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了

关于Java中的 equals

案例

package test37;


import java.util.HashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        String s1 = new String("abc");
        String s2 = new String("abc");
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
        Set<String> set01 = new HashSet<>();
        set01.add(s1);
        set01.add(s2);
        System.out.println(set01.size());
        System.out.println("===========");

        Person p1 =new Person("abc");
        Person p2 =new Person("abc");
        System.out.println(p1 == p2);
        System.out.println(p1.equals(p2));
        Set<Person> set02 = new HashSet<>();
        set02.add(p1);
        set02.add(p2);
        System.out.println(set02.size());
    }
}
package test37;

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public Person() {
    }
}

运行结果如下:
在这里插入图片描述

答案解析

s1 == s2为false已经很明确了。
在这里插入图片描述
由String的equals源码可知,它比较的是两个变量的值。所以为true。

set01的 size大小为何是1?
先瞅瞅HashSet的源码
在这里插入图片描述
在这里插入图片描述
由HashSet的add()方法源码可以得到两个结论:
1.HashSet的add()底层调的是HashMap的put方法
2.HashMap的put方法的key唯一性(不重复)的确定是以key值的hashcode为准。
所以再看看String的hashcode方法
在这里插入图片描述
相同的变量值通过公式计算出的hashcode是一样的。
所以答案就很明确了
第二组Person的“= =”也很明确了
equals方法使用的Object类的equals方法。咱们看看Object类的equals方法的源码
在这里插入图片描述
在这里插入图片描述
那答案就很明确了。

聊一聊==和equals的区别

在这里插入图片描述

结论

  • 构造方法实例化字符串会存在一部分垃圾空间,便是堆内存中重复创建的字符串字段,因此直接赋值的方式确实优于构造方法的方式,因此字符串在使用时都使用的是直接赋值的方法。
  • Java常量池存在于方法区
  • 使用new关键字实例化,都会在堆中开辟一块内存
  • 基本数据类型 = = 比较的数据的值,引用数据类型使用 = =比较的对象地址的值
  • java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。
  • HashSet的add()底层调的是HashMap的put方法
  • HashMap的put方法的key唯一性(不重复)的确定是以key值的hashcode为准。不是key值本身
  • String类重写了equals方法和hashCode()方法。因此String类的equals比较的是变量值是否相等。
  • 普通类如果未重写equals方法的话,那就使用Object类的equals()方法,其实和 = =是相同的。
原创文章 38 获赞 52 访问量 1万+

猜你喜欢

转载自blog.csdn.net/yemuxiaweiliang/article/details/105317214