假設現在有這樣一個需求:判斷目前登入的用戶,如果是我們指定的系統管理員,則發送一封郵件。系統管理員沒有特殊的欄位標識,他的使用者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。
這算哪一門子原因?
答:各位看官,別急,後面會細講的。
讓我們一起回顧一下,先前判斷兩個值是否相等的方法有哪些。
之前判斷兩個值是否相等,最快的方法是使用==號。
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。
使用上面的==號,可以非常快速判斷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。
從前面我們已經知道,判斷兩個物件是否相等,可以使用==號,或是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或者其他类型的对象比较呢?
答:有办法,继续往下看。
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方法再比较两个对象是否相等时,确实是一个不错的方法。
但它有坑,不信继续往下看。
各位小伙们看到这里,可能有点心急了,到底是什么坑?
废话不多说,直接上例子:
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方法上,会出现下面的提示:
这时你就知道方法用错了,赶紧修正。但如果直接用包装类的equals方法,有个问题就是可能存在报空指针异常的风险。
如果你使用Objects.equals方法判断相等,在idea中就并没有错误提示。
除此之外,我还测试了findBug、sonar等工具,Objects.equals方法两个参数类型不一致的问题,也没有标识出来。
小夥子們趕緊看你們的程式碼,踩坑了沒?
常見的坑有:
Long類型和Integer類型比較,例如:使用者id的場景。
Byte類型和Integer類型比較,例如:狀態判斷的場景。
Double類型和Integer類型比較,例如:金額為0的判斷場景。
以上是Java中Objects.equals踩坑實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!