Objects.equals有坑

前言

最近review別人程式碼的時候,發現有個同事,在某個業務場景下,使用Objects.equals方法判斷兩個值相等時,返回了跟預期不一致的結果,引起了我的興趣。

原本以為判斷結果會返回true的,但實際上返回了false。

記得很早之前,我使用Objects.equals方法也踩過類似的坑,所以有必要把這個問題記錄下來,分享給大家。

到底怎麼回事呢?

1. 案發現場

假設現在有這樣一個需求:判斷當前登錄的用戶,如果是我們指定的系統管理員,則發送一封郵件。系統管理員沒有特殊的欄位標識,他的用戶id=888,在開發、測試、生產環境中該值都是一樣的。

這個需求真的太容易實現了:

UserInfo userInfo = CurrentUser.getUserInfo();

if(Objects.isNull(userInfo)) {
   log.info("請先登錄");
   return;
}

if(Objects.equals(userInfo.getId(),888)) {
   sendEmail(userInfo):
}

從當前登錄用戶的上下文中獲取用戶資訊,判斷一下,如果用戶資訊為空,則直接返回。

如果獲取到的用戶資訊不為空,接下來判斷用戶id是否等於888。

  • 如果等於888,則發送郵件。
  • 如果不等於888,則啥事也不幹。

當我們用id=888的系統管理員帳號登錄之後,做了相關操作,滿懷期待的準備收郵件的時候,卻發現收了個寂寞。

後來,發現UserInfo類是這樣定義的:

@Data
public class UserInfo {
    private Long id;
    private String name;
    private Integer age;
    private String address;
}

此時,有些小夥伴可能會說:沒看出什麼問題呀。

但我要說的是這個程式碼確實有問題。

什麼問題呢?

答:UserInfo類的成員變數id=888是Long類型的,而Objects.equals方法右邊的888是int類型的,兩者不一致,導致返回的結果是false。

這算哪門子原因?

答:各位看官,別急,後面會細講的。

2. 判斷相等的方法

讓我們一起回顧一下,以前判斷兩個值是否相等的方法有哪些。

2.1 使用==號

之前判斷兩個值是否相等,最快的方法是使用==號。

int a = 1;
int b = 1;
byte c = 1;
Integer d1 = new Integer(1);
Integer d2 = new Integer(1);
System.out.println(a == b); 
//結果:true
System.out.println(a == c); 
//結果:true
System.out.println(a == d1); 
//結果:true
System.out.println(d2 == a); 
//結果:true
System.out.println(d1 == d2); 
//結果:false

不知道大家有沒有發現,java中的基本類型,包含:int、long、short、byte、char、boolean、float、double這8種,可以使用號判斷值是否相等。如果出現了基本類型的包裝類,比如:Integer,用一個基本類型和一個包裝類,使用號也能正確判斷,返回true。

Integer和int比較時,會自動拆箱,這是比較值是否相等。

但如果有兩個包裝類,比如:d1和d2,使用==號判斷的結果可能是false。

兩個Integer比較時,比較的是它們指向的引用(即記憶體地址)是否相等。

還有一個有意思的現象:

Integer d3 = 1;
Integer d4 = 1;
Integer d5 = 128;
Integer d6 = 128;
System.out.println(d3 == d4); 
//結果:true
System.out.println(d5 == d6); 
//結果:false

都是給Integer類型的參數,直接賦值後進行比較。d3和d4判斷的結果相等,但d5和d6判斷的結果卻不相等。

小夥伴們,下巴驚掉了沒?

答:因為Integer有一個常量池,-128~127直接的Integer數據直接快取進入常量池。所以1在常量池,而128不在。

然而,new的Integer對象不適用常量池。從之前d1和d2例子的比較結果,就能看出這一點。

接下來,看看字元串的判斷:

String e = "abc";
String f = "abc";
String g = new String("abc");
String h = new String("abc");
System.out.println(e == f); 
//結果:true
System.out.println(e == g); 
//結果:false
System.out.println(g == h); 
//結果:false

普通的字元串變數,使用==號判斷,也能返回正確的結果。

但如果一個普通的字元串變數,跟new出來的字元串對象使用號判斷時,返回false。這一點,跟之前說過的用一個基本類型和一個包裝類,使用號判斷的結果有區別,字元串沒有自動拆箱的功能,這一點需要特別注意。

此外,兩個new出來的字元串對象使用==號判斷時,也返回false。

2.2 使用equals方法

使用上面的==號,可以非常快速判斷8種基本數據類型是否相等,除此之外,還能判斷兩個對象的引用是否相等。

但現在有個問題,它無法判斷兩個對象在記憶體中具體的數據值是否相等,比如:

String g = new String("abc");
String h = new String("abc");
System.out.println(g == h); 
//結果:false

字元串對象g和h是兩個不同的對象,它們使用==號判斷引用是否相等時,返回的是false。

那麼,這種對象不同,但數據值相同的情況,我們該如何判斷相等呢?

答:使用equals方法。

equals方法其實是Object類中的方法:

public boolean equals(Object obj) {
    return (this == obj);
}

該方法非常簡單,只判斷兩個對象的引用是否相等。

很顯然,如果字元串類型直接使用父類(即Object類)的equals方法,去判斷對象不同,但值相同的情況,是有問題的。

所以,字元串(即String類)會重新的equals方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

它依然會先判斷兩個對象引用是否相等,如果相等返回true。接下來,會把兩個字元串的挨個字元進行比較,只有所有字元都相等才返回true。

nice,這樣就能解決g和h判斷的問題:

String e = "abc";
String f = "abc";
System.out.println(e.equals(f)); 
//結果:true

由此可見,我們使用String類重寫後的equals方法,判斷兩個字元串對象不同,但值相同時,會返回true。

3. 空指針異常

從前面我們已經知道,判斷兩個對象是否相等,可以使用==號,或者equals方法。

但如果你更深入的使用它們,會發現一個問題,即:這兩種方式判斷值相等,都可能會報空指針異常

先看看==號判斷的情況:

int a = 1;
Integer b = new Integer(1);
Integer c = null;
System.out.println(a == b); 
//結果:true
System.out.println(a == c); 
//結果:NullPointerException 

int和Integer使用==號判斷是否相等時,Integer會自動拆箱成int。

但由於c在自動拆箱的過程中,需要給它賦值int的默認值0。而給空對象,賦值0,必然會報空指針異常。

接下來,看看equals方法:

String e = null;
String f = "abc";
System.out.println(e.equals(f)); 
//結果:NullPointerException

由於字元串對象e是空對象,直接調用它的equals方法時,就會報空指針異常。

那麼,如何解決空指針問題呢?

答:在程式碼中判空。

String e = null;
String f = "abc";
System.out.println(equals(e, f));

我們抽取了一個新的equals方法:

private static boolean equals(String e, String f) {
    if (e == null) {
        return f == null;
    }
    return e.equals(f);
}

該方法可以解決空指針問題,但有沒有辦法封裝一下,變得更通用一下,也適用於Integer或者其他類型的對象比較呢?

答:有辦法,繼續往下看。

4. Objects.equals的作用

Objects類位於java.util包下,它是裡面提供了很多對象操作的輔助方法。

下面我們重點看看它的equals方法:

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

equals方法的判斷邏輯如下:

  1. 該方法先判斷對象a和b的引用是否相等,如果相等則直接返回true。
  2. 如果引用不相等,則判斷a是否為空,如果a為空則返回false。
  3. 如果a不為空,調用對象的equals方法進一步判斷值是否相等。

該方法是如何使用的?

int a = 1;
int b = 1;
Integer c = null;

System.out.println(Objects.equals(a, c)); 
//結果:false
System.out.println(Objects.equals(c, a)); 
//結果:false
System.out.println(Objects.equals(a, b)); 
//結果:true

從上面的列子看出,使用Objects.equals方法比較兩個對象是否相等,確實可以避免空指針問題。

但這個有個疑問:前面使用a==b這種方式比較引用是否相等,當時b為空時,程式直接拋了空指針異常。

而Objects.equals方法內部也使用了a==b比較引用是否相等,為啥它沒有拋異常?

答:因為而Objects類的equals方法,使用了Object類型接收參數,它的默認值是null,不用進行類型轉換,也不用像int類型對象賦值默認值0。

從上面的理論可以看出,如果我們把程式碼改成這樣,也不會拋異常:

int a = 1;
Integer c = null;
System.out.println(equals(a, c));
//結果:false

新定義了一個方法:

private static boolean equals(Object a, Object b) {
    return a == b;
}

執行之後發現,確實沒有拋空指針了。

所以Objects.equals方法再比較兩個對象是否相等時,確實是一個不錯的方法。

但它有坑,不信繼續往下看。

5. Objects.equals的坑

各位小伙們看到這裡,可能有點心急了,到底是什麼坑?

廢話不多說,直接上例子:

Integer a = 1;
long b = 1L;
System.out.println(Objects.equals(a, b));
//結果:false

什麼?返回結果是false?

而如果你直接用==號判斷:

Integer a = 1;
long b = 1L;
System.out.println(a == b);
//結果:true

返回又是true。

a和b明明都是1,為什麼使用Objects.equals方法判斷不相等呢?

這就要從Integerequals方法說起來了。

它的equals方法具體程式碼如下:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

先判斷參數obj是否是Integer類型,如果不是,則直接返回false。如果是Integer類型,再進一步判斷int值是否相等。

而上面這個例子中b是long類型,所以Integer的equals方法直接返回了false。

也就是說,如果調用了Integer的equals方法,必須要求入參也是Integer類型,否則該方法會直接返回false。

原來坑在這裡!!!

其實,如果把程式碼改成這樣:

Integer a = 1;
long b = 1L;
System.out.println(Objects.equals(b, a));
//結果:false

執行結果也是false。

因為Long的equals方法程式碼,跟之前Integer的類似:

public boolean equals(Object obj) {
    if (obj instanceof Long) {
        return value == ((Long)obj).longValue();
    }
    return false;
}

也是判斷入參,如果不是Long類型,則該方法直接返回false。

除此之外,還有Byte、Short、Double、Float、Boolean和Character也有類似的equals方法判斷邏輯。

由此可見,我們在使用Objects.equals方法,判斷兩個值是否相等時,一定要保證兩個入參的類型要一致。否則即使兩個值相同,但其結果仍然會返回false,這是一個大坑。

那麼,如何解決上面的問題呢?

可以將參數b的類型強制轉換成int。

Integer a = 1;
long b = 1L;
System.out.println(Objects.equals(a, (int)b));
//結果:true

或者將參數a的類型強制轉換成long。

Integer a = 1;
long b = 1L;
System.out.println(Objects.equals(b, (long)a));
//結果:true

有些情況也可以直接用==號判斷:

Integer a = 1;
long b = 1L;
System.out.println(a==b);
//結果:true

除了Objects.equals方法在兩個入參類型不同,而會直接返回false之外,java的8種基本類型包裝類的equals也會有相同的問題,需要小伙們特別注意。

之前,如果直接使用java基本類型包裝類的equals方法判斷相等。

Integer a = new Integer(1);
long b = 1L;
System.out.println(a.equals(b));

在idea中,如果你將滑鼠放在equals方法上,會出現下面的提示:


這時你就知道方法用錯了,趕緊修正。但如果直接用包裝類的equals方法,有個問題就是可能存在報空指針異常的風險。

如果你使用Objects.equals方法判斷相等,在idea中就並沒有錯誤提示。

除此之外,我還測試了findBug、sonar等工具,Objects.equals方法兩個參數類型不一致的問題,也沒有標識出來。

小伙們趕緊看看你們的程式碼,踩坑了沒?

常見的坑有:

  1. Long類型和Integer類型比較,比如:用戶id的場景。
  2. Byte類型和Integer類型比較,比如:狀態判斷的場景。
  3. Double類型和Integer類型比較,比如:金額為0的判斷場景。

最近無意間獲得一份阿里大佬寫的刷題筆記,一下子打通了我的任督二脈,進大廠原來沒那麼難。

關注微信公眾號:蘇三說技術,回復:進大廠,即可獲取該面試資料,非常值得一讀。