首頁 > Java > java教程 > 主體

Java中Objects.equals踩坑實例分析

WBOY
發布: 2023-05-04 13:28:06
轉載
785 人瀏覽過

    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物件不適用常數池。從之前d​​1和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方法的判断逻辑如下:

    • 该方法先判断对象a和b的引用是否相等,如果相等则直接返回true。

    • 如果引用不相等,则判断a是否为空,如果a为空则返回false。

    • 如果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方法判断不相等呢?

    这就要从Integer的equals方法说起来了。

    它的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方法上,会出现下面的提示:

    Java中Objects.equals踩坑實例分析

    这时你就知道方法用错了,赶紧修正。但如果直接用包装类的equals方法,有个问题就是可能存在报空指针异常的风险。

    如果你使用Objects.equals方法判断相等,在idea中就并没有错误提示。

    除此之外,我还测试了findBug、sonar等工具,Objects.equals方法两个参数类型不一致的问题,也没有标识出来。

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

    常見的坑有:

    • Long類型和Integer類型比較,例如:使用者id的場景。

    • Byte類型和Integer類型比較,例如:狀態判斷的場景。

    • Double類型和Integer類型比較,例如:金額為0的判斷場景。

    #

    以上是Java中Objects.equals踩坑實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    相關標籤:
    來源:yisu.com
    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    熱門教學
    更多>
    最新下載
    更多>
    網站特效
    網站源碼
    網站素材
    前端模板
    關於我們 免責聲明 Sitemap
    PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!