很不起眼的6個bug,90%的程式設計師就算寫了10年程式碼也肯定都踩過!
文章來源:http://juejin.cn/post/7120570066856312839
前言
作為Java程式設計師的你,不知道有沒有踩過一些基礎知識的坑。
有時候,某個bug查了半天,最後發現竟然是一個低階錯誤。
有時候,某些程式碼,這一批資料功能正常,但換了一批資料就出現異常了。
有時候,你可能會看著某行程式碼目瞪口呆,心裡想:這行程式碼為什麼會出錯?
今天跟大家一起聊聊99%的Java程式設計師踩過,或者即將踩的6個坑。
1. 用==號比較的坑
不知道你在專案中有沒有見過,有些同事 對Integer型別的兩個 引數使用 ==號比較 是否相等?
反正我見過的,那麼這種用法對嗎?
我的回答是看具體場景,不能說一定對,或不對。
有些狀態欄位,比如:orderStatus有:-1(未下單),0(已下單),1(已支付),2(已完成),3(取消),5種狀態。
這時如果用==判斷是否相等:
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1 == orderStatus2);
返回結果會是 true 嗎?
答案:是 false 。
有些同學可能會反駁,Integer中不是有範圍是: -128-127的快取 嗎?
為什麼是false?
先看看Integer的構造方法:
它其實並沒有用到 快取 。
那麼快取是在哪裡用的?
答案在 valueOf
方法中:
如果上面的判斷改成這樣:
String orderStatus1 = new String("1");
String orderStatus2 = new String("1");
System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));
返回結果會是 true 嗎?
答案:還真是 true 。
我們要養成良好編碼習慣,儘量少用==判斷兩個Integer型別資料是否相等,只有在上述非常特殊的場景下才相等。
而應該改成使用 equals
方法判斷:
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1.equals(orderStatus2));
執行結果為 true 。
2. Objects.equals的坑
假設現在有這樣一個需求: 判斷當前登入的使用者,如果是我們指定的系統管理員,則傳送一封郵件。 系統管理員沒有特殊的欄位標識,他的使用者id=888,在開發、測試、生產環境中該值都是一樣的。
這個需求真的太容易實現了:
UserInfo userInfo = CurrentUser.getUserInfo();
if(Objects.isNull(userInfo)) {
log.info("請先登入");
return;
}
if(Objects.equals(userInfo.getId(),888L)) {
sendEmail(userInfo):
}
從當前登入使用者的上下文中獲取使用者資訊,判斷一下,如果使用者資訊為空,則直接返回。
如果獲取到的使用者資訊不為空,接下來判斷使用者id是否等於888。
-
如果等於888,則傳送郵件。
-
如果不等於888,則啥事也不幹。
當我們用id=888的系統管理員賬號登入之後,做了相關操作,滿懷期待的準備收郵件的時候,卻發現收了個寂寞。
後來,發現UserInfo類是這樣定義的:
@Data
public class UserInfo {
private Integer id;
private String name;
private Integer age;
private String address;
}
此時,有些小夥伴可能會說:沒看出什麼問題呀。
但我要說的是這個程式碼確實有問題。
什麼問題呢?
下面我們重點看看它的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方法進一步判斷值是否相等。
這就要從 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。
除此之外,還有Byte、Short、Double、Float、Boolean和Character也有類似的equals方法判斷邏輯。
常見的坑有:
-
Long型別和Integer型別比較,比如:使用者id的場景。
-
Byte型別和Integer型別比較,比如:狀態判斷的場景。
-
Double型別和Integer型別比較,比如:金額為0的判斷場景。
3. BigDecimal的坑
通常我們會把一些小數型別的欄位(比如:金額),定義成 BigDecimal
,而不是 Double
,避免丟失精度問題。
使用Double時可能會有這種場景:
double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);
正常情況下預計amount2 - amount1應該等於0.01
但是執行結果,卻為:
0.009999999999999998
實際結果小於預計結果。
Double型別的兩個引數相減會轉換成二進位制,因為Double有效位數為16位這就會出現儲存小數位數不夠的情況,這種情況下就會出現誤差。
常識告訴我們使用 BigDecimal
能避免丟失精度。
但是使用BigDecimal能避免丟失精度嗎?
答案是否定的。
為什麼?
BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));
這個例子中定義了兩個BigDecimal型別引數,使用建構函式初始化資料,然後列印兩個引數相減後的值。
結果:
0.0099999999999999984734433411404097569175064563751220703125
不科學呀,為啥還是丟失精度了?
Jdk
中 BigDecimal
的 構造方法
上有這樣一段描述:
大致的意思是此建構函式的結果可能不可預測,可能會出現建立時為0.1,但實際是0.1000000000000000055511151231257827021181583404541015625的情況。
由此可見,使用BigDecimal建構函式初始化物件,也會丟失精度。
那麼,如何才能不丟失精度呢?
BigDecimal amount1 = new BigDecimal(Double.toString(0.02));
BigDecimal amount2 = new BigDecimal(Double.toString(0.03));
System.out.println(amount2.subtract(amount1));
我們可以使用 Double.toString
方法,對double型別的小數進行轉換,這樣能保證精度不丟失。
其實,還有更好的辦法:
BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));
使用 BigDecimal.valueOf
方法初始化BigDecimal型別引數,也能保證精度不丟失。在新版的阿里巴巴開發手冊中,也推薦使用這種方式建立BigDecimal引數。
4. Java8 filter的坑
對於 Java8
中的 Stream
用法,大家肯定再熟悉不過了。
我們通過對 集合
的 Stream
操作,可以實現:遍歷集合、過濾資料、排序、判斷、轉換集合等等,N多功能。
這裡重點說說資料的過濾。
在沒有Java8之前,我們過濾資料一般是這樣做的:
public List<User> filterUser(List<User> userList) {
if(CollectionUtils.isEmpty(userList)) {
return Collections.emptyList();
}
List<User> resultList = Lists.newArrayList();
for(User user: userList) {
if(user.getId() > 1000 && user.getAge() > 18) {
resultList.add(user);
}
}
return resultList;
}
通常需要另一個集合輔助完成這個功能。
但如果使用Java8的 filter
功能,程式碼會變得簡潔很多,例如:
public List<User> filterUser(List<User> userList) {
if(CollectionUtils.isEmpty(userList)) {
return Collections.emptyList();
}
return userList.stream()
.filter(user -> user.getId() > 1000 && user.getAge() > 18)
.collect(Collectors.toList());
}
程式碼簡化了很多,完美。
但如果你對過濾後的資料,做修改了:
List<User> userList = queryUser();
List<User> filterList = filterUser(userList);
for(User user: filterList) {
user.setName(user.getName() + "測試");
}
for(User user: userList) {
System.out.println(user.getName());
}
你當時可能只是想修改過濾後的資料,但實際上,你會把元素資料一同修改了。
意不意外,驚不驚喜?
其根本原因是: 過濾後的集合中,儲存的是物件的引用,該引用只有一份資料。
也就是說,只要有一個地方,把該引用物件的 成員變數
的值,做修改了,其他地方也會同步修改。
如下圖所示:
5. 自動拆箱的坑
Java5
之後,提供了 自動裝箱
和 自動拆箱
的功能。
自動裝箱是指:JDK會把基本型別,自動變成包裝型別。
比如:
Integer integer = 1;
等價於:
Integer integer = new Integer(1);
而自動拆箱是指:JDK會把包裝型別,自動轉換成基本型別。
例如:
Integer integer = new Integer(2);
int sum = integer + 5;
等價於:
Integer integer = new Integer(2);
int sum = integer.intValue() + 5;
但實際工作中,我們在使用自動拆箱時,往往忘記了判空,導致出現 NullPointerException
異常。
5.1 運算
很多時候,我們需要對傳入的資料進行計算,例如:
public class Test2 {
public static void main(String[] args) {
System.out.println(add(new Integer(1), new Integer(2)));
}
private static Integer add(Integer a, Integer b) {
return a + b;
}
}
如果傳入了null值:
System.out.println(add(null, new Integer(2)));
則會直接報錯。
5.2 傳參
有時候,我們定義的某個方法是基本型別,但實際上傳入了包裝類,比如:
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = null;
System.out.println(add(a, b));
}
private static Integer add(int a, int b) {
return a + b;
}
如果出現add方法報 NullPointerException
異常,你可能會懵逼,int型別怎麼會出現空指標異常呢?
其實,這個問題出在:Integer型別的引數,其實際傳入值為null,JDK欄位拆箱,呼叫了它的 intValue
方法導致的問題。
6. replace的坑
很多時候我們在使用字串時,想把字串比如:ATYSDFA*Y中的字元A替換成字元B,第一個想到的可能是使用replace方法。
如果想把所有的A都替換成B,很顯然可以用replaceAll方法,因為非常直觀,光從方法名就能猜出它的用途。
那麼問題來了:replace方法會替換所有匹配字元嗎?
jdk的官方給出了答案。
該方法會替換每一個匹配的字串。
既然replace和replaceAll都能替換所有匹配字元,那麼他們有啥區別呢?
replace
有兩個 過載
的方法。
-
其中一個方法的引數:char oldChar 和 char newChar,支援字元的替換。
source.replace('A', 'B')
-
另一個方法的引數是:CharSequence target 和 CharSequence replacement,支援字串的替換。
source.replace("A", "B")
而 replaceAll
方法的引數是:String regex 和 String replacement,即基於 正則表示式
的替換。
例如對普通字串進行替換:
source.replaceAll("A", "B")
使用正則表達替換(將*替換成C):
source.replaceAll("\\*", "C")
順便說一下,將 *
替換成 C
使用replace方法也可以實現:
source.replace("*", "C")
小夥們看到看到二者的區別了沒?使用replace方法無需對特殊字元進行轉義。
不過,千萬注意,切勿使用如下寫法:
source.replace("\\*", "C")
這種寫法會導致字串無法替換。
還有個小問題,如果我只想替換第一個匹配的字串該怎麼辦?
這時可以使用 replaceFirst
方法:
source.replaceFirst("A", "B")
說實話,這裡內容都很基礎,但越基礎的東西,越容易大意失荊州,更容易踩坑。
最後,統計一下,這些坑一個都沒踩過的同學,麻煩舉個手。
歡迎掃碼加入儒猿技術交流群,每天晚上20:00都有Java面試、Redis、MySQL、RocketMQ、SpringCloudAlibaba、Java架構等技術答疑分享,更能跟小夥伴們一起交流技術
另外推薦儒猿課堂的1元系列課程給您,歡迎加入一起學習~
網際網路Java工程師面試突擊課
(1元專享)
SpringCloudAlibaba零基礎入門到專案實戰
(1元專享)
億級流量下的電商詳情頁系統實戰專案
(1元專享)
Kafka訊息中介軟體核心原始碼精講
(1元專享)
12個實戰案例帶你玩轉Java併發程式設計
(1元專享)
Elasticsearch零基礎入門到精通
(1元專享)
基於Java手寫分散式中介軟體系統實戰
(1元專享)
基於ShardingSphere的分庫分表實戰課
(1元專享)
- 用了這個IntellijIDEA外掛以後,我寫程式碼快了10倍!
- 很不起眼的6個bug,90%的程式設計師就算寫了10年程式碼也肯定都踩過!
- 別再張口閉口高併發海量資料了,Spring這些東西都會了嗎?
- 公司空降一個技術leader,居然帶我們用JavaStream寫程式碼!
- 秒殺系統面試題居然包含這麼多隱藏關卡,大意了!
- 拿到35k入職之後,我在新公司從0到1搭建了一套訂單系統!
- 大廠工程師如何給SpringBoot封裝響應資料和異常處理?
- 我一口氣說出四種介面冪等解決方案,面試官露出了姨媽笑~
- 我們一起來談談高併發和分散式系統的冪等如何處理!
- ClickHouse和Elasticsearch壓測對比,誰才是yyds?
- 什麼?!儒猿自研的中介軟體專案,被風投看上了!
- 拜託,面試不要再問我 SpringCloudAlibaba 底層原理
- 分庫分表實戰之一葉知秋:圖覽分庫分表外賣訂單專案
- 儒猿雲平臺:解決技術學習的“最後一公里”
- 公眾號狸貓技術窩遷移說明!
- 講解 Redis 的一篇深度好文!
- MySQL硬核乾貨:從磁碟讀取資料頁到Buffer Pool時,free連結串列有什麼用?
- Java 工程師必須掌握的 JVM 類載入機制!
- 【圖文講解】TCP為啥要3次握手和4次揮手?握兩次手不行嗎?
- 拼多多面試:如何用 Redis 統計獨立使用者訪問量?