前言
作為一個Java程序員,不知道有沒有踩到一些基礎知識坑。
有時候,你查了很久一個bug,最后發現是很低級的錯誤。
有時候,在某些代碼中,這一批數據功能正常,但更改一批數據后就異常了。
有時候,你可能會看著某一行代碼,想知道:為什么這一行代碼出錯了?
今天就來說說99%的Java程序員都踩過或者即將踩過的六個坑。
1.坑比==
不知道大家有沒有在項目里看到過。有同事用==號來比較兩個整型的參數是否相等?
反正我見過,那么這種用法對嗎?
我的回答是看具體場景,不能說一定是對或錯。
一些狀態字段,例如:orderStatus,有五種狀態:-1(未訂購)、0(已訂購)、1(已付款)、2(已完成)、3(已取消)。
這時,如果用==來判斷它們是否相等:
integerorderstatus 1=new integer(1);
integerorderstatus 2=new integer(1);
System.out.println(訂單狀態1==訂單狀態2);
結果會是真的嗎?
答案:假的。
有的同學可能會反駁,不是有一個緩存的范圍是:-128-127的整數嗎?
為什么假?
先看整數的構造方法:
它實際上不使用緩存。
那么緩存用在哪里呢?
在方法的答案值中:
如果上面的判斷換成這樣:
stringorderstatus 1=new string(' 1 ');
stringorderstatus 2=new string(' 1 ');
system . out . println(integer . value of(order status 1)==integer . value of(order status 2));
結果會是真的嗎?
回答:是真的。
要養成良好的編碼習慣,盡量少用==來判斷兩個整數類型的數據是否相等,只有在上述非常特殊的場景下才相等。
相反,使用equals方法來判斷:
integerorderstatus 1=new integer(1);
integerorderstatus 2=new integer(1);
System.out.println(訂單狀態1.equals(訂單狀態2));
結果是真的。
2.物體的坑。等于
假設有一個需要判斷當前登錄的用戶,如果是我們指定的系統管理員就發郵件。系統管理員沒有特殊的字段ID,他的用戶id=888,這個值在開發、測試和生產環境中都是一樣的。
這個要求真的太容易實現了:
userinfo userinfo=current user . get userinfo();
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的判斷場景。
如果你想進一步了解Objects.equals方法的問題,可以看看我的另一篇文章《Objects.equals有坑》。
3. BigDecimal的坑
通常我們會把一些小數類型的字段(比如:金額),定義成BigDecimal
,而不是Double
,避免丟失精度問題。
使用Double時可能會有這種場景:
double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);
正常情況下預計amount2 - amount1應該等于0.01
但是執行結果,卻為:
.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類型參數,使用構造函數初始化數據,然后打印兩個參數相減后的值。
結果:
.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")
說實話,這里內容都很基礎,但越基礎的東西,越容易大意失荊州,更容易踩坑。
最后,統計一下,這些坑一個都沒踩過的同學,麻煩舉個手。
以上文章來源于公眾號-蘇三說技術 ,作者蘇三呀