線上問題年年有,今年特別多。記幾次線上慘痛的踩坑記錄,希望大家以史為鑒。 1. 包裝類型自動解箱導致空指針異常 public int getId() { Integer id = null; return id; } 如果調用上面的方法會發生什麼?id是Integer類型,而方法的返回值int類型, ...
線上問題年年有,今年特別多。記幾次線上慘痛的踩坑記錄,希望大家以史為鑒。
1. 包裝類型自動解箱導致空指針異常
public int getId() {
Integer id = null;
return id;
}
如果調用上面的方法會發生什麼?id是Integer類型,而方法的返回值int類型,會自動拆箱轉換,由於id是null,轉換成int類型的時候,就會報NullPointerException異常。
無論是《阿裡Java開發手冊》、《代碼整潔之道》還是《Effective Java》都建議方法返回值類型儘量寫成包裝類型,類似Integer。還有實體類、接收前端傳參類、給前端的響應類中的屬性都要寫成包裝類型,避免拆箱出錯。
2. 包裝類型用==判斷相等,導致判斷不正確
先看一段代碼運行結果:
public class IntegerTest {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b); // 輸出 true
System.out.println(c == d); // 輸出 false
}
}
很多人會很疑惑,為什麼輸出的兩個結果會不一樣?
當給Integer類型賦值時,會調用Integer.valueOf()方法
static final int low = -128;
static final int high = 127;
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
當value值在-128到127之間時,會復用緩存。當不在這個區間時,才會創建對象。
而==比較的是記憶體地址,不同的對象的記憶體地址不相同,所以就出現上述的結果。
Integer重寫了equals()方法:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
當使用equals()方法時,比較的是int值是否相等。
所以,包裝類判斷是否相等的時候,絕不能用==判斷,一定要用equals()方法判斷。
3. Switch傳參是null導致空指針異常
猜一下下麵代碼的運行結果:
public class Test {
public static void main(String[] args) {
String name = null;
switch (name) {
case "yideng":
System.out.println("一燈");
break;
default:
System.out.println("default");
}
}
}
你是不是認為會輸出default,其實代碼會拋出NullPointerException異常。
當switch比較兩個對象是否相等的時候,會調用name.hashCode()方法和name.equals()方法,因為name是null,結果就拋出了NullPointerException異常。
所以調用switch方法前,一定要對傳參進行判空。
4. 創建BigDecimal類型時精度丟失
猜一下下麵代碼的運行結果:
public class Test {
public static void main(String[] args) {
BigDecimal bigDecimal = new BigDecimal(0.1);
System.out.println(bigDecimal);
}
}
你以為會輸出0.1,其實輸出結果是:
0.1000000000000000055511151231257827021181583404541015625
What?這麼一大串是什麼東西?
為什麼會出現這種情況呢?原因是,當我們用new BigDecimal(0.1)創建對象是,會調用BigDecimal的這個構造方法:
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}
把傳參0.1當成了double類型,double計算的時候會把數值轉換成二進位,而0.1轉換成二進位是無法除盡的,所以就帶了一大串小數位。
當需要創建BigDecimal類型時,應該怎麼做呢?
可以先把數值轉換成字元串類型,再創建BigDecimal對象,類似這樣:
BigDecimal bigDecimal = new BigDecimal(String.valueOf(0.1));
又來一個問題,BigDecimal是怎麼解決精度丟失問題?
答案是BigDecimal會先把數值乘以10的整數倍,去除小數位,轉換成long類型,然後進行運算,最後把運算結果除以10的整數倍。
5. group分組時主鍵重覆,導致異常
下麵代碼的分組能成功嗎?
public class SteamTest {
static class User {
// 用戶ID
private Integer id;
// 用戶名
private String name;
}
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User(1, "Tom"),
new User(1, "Tony"),
new User(2, "Jerry")
);
// 用戶集合按id進行分組
Map<Integer, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
System.out.println(userMap);
}
}
結果報異常了,Exception in thread "main" java.lang.IllegalStateException: Duplicate key SteamTest.User(id=1, name=Tom)
原因是主鍵衝突,有兩個id=1的數據,按id進行分組時程式就不知道怎麼處理了。
可以這樣做
public class SteamTest {
static class User {
// 用戶ID
private Integer id;
// 用戶名
private String name;
}
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User(1, "Tom"),
new User(1, "Tony"),
new User(2, "Jerry")
);
// 用戶集合按id進行分組,主鍵衝突的時候,取第一個user
Map<Integer, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user, (user1, user2) -> user1));
System.out.println(userMap); // 輸出 {1:{"id":1,"name":"Tom"},2:{"id":2,"name":"Jerry"}}
}
}
6. 真假ArrayList導致添加異常
下麵的add()方法能添加成功嗎?
public class Test {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2);
list.add(3);
}
}
結果是拋異常了,Exception in thread "main" java.lang.UnsupportedOperationException
拋出了不支持這個方法的異常,為什麼呢?我們看一下Arrays.asList()方法的源碼:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
返回了一個ArrayList,為什麼還不能添加成功了?
真相是此ArrayList非彼ArrayList,跟我們常用的ArrayList只是重名,這個ArrayList只是Arrays對象一個內部類,內部並沒有實現add()方法,所以添加的時候會報錯。
這不是明擺著坑人嗎?實現了list介面,為啥不實現add()方法?
其實作者是故意這樣設計的,除了沒有實現add()方法,還沒有實現addAll()、remove()、clear()等修改方法,目的就是創建後再不讓用戶修改,這樣的集合有什麼用呢?
其實在某些不可變場景還是很實用的,比如已結束的訂單狀態集合:
List<String> list = Arrays.asList("Failure", "Cancelled","Completed");
這種集合一般不會變的,使用過程中也不允許修改,避免出錯。
7. 總結
每一次踩坑,背後都有至少一次的線上問題記錄,這些總結都是用教訓換來的,不只是自己,其他人肯定也遇到過。我們如何才能避免在以後的開發中再出現類似的問題呢?
- 站在使用者的角度,編寫詳細的單元測試,列印必要日誌,追蹤代碼執行結果
- 站在創造者的角度,探究框架的架構設計和源碼實現,理解作者的意圖
你線上上還踩過那些坑?
文章持續更新,可以微信搜一搜「 一燈架構 」第一時間閱讀更多技術乾貨。