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關鍵字實例化。如下:

  • 直接賦值: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()方法,其實和 = =是相同的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章