99%的Java程序員會(huì)踩的6個(gè)坑
大家好,我是3y,又跟大家見(jiàn)面了。
前言
作為Java程序員的你,不知道有沒(méi)有踩過(guò)一些基礎(chǔ)知識(shí)的坑。
有時(shí)候,某個(gè)bug,你查了半天,最后發(fā)現(xiàn)竟然是一個(gè)非常低級(jí)的錯(cuò)誤。
有時(shí)候,某些代碼,這一批數(shù)據(jù)功能正常,但換了一批數(shù)據(jù)就出現(xiàn)異常了。
有時(shí)候,你可能會(huì)看著某行代碼目瞪口呆,心里想:這行代碼為什么會(huì)出錯(cuò)?
今天跟大家一起聊聊99%的Java程序員踩過(guò),或者即將踩的6個(gè)坑。
1. 用==號(hào)比較的坑
不知道你在項(xiàng)目中有沒(méi)有見(jiàn)過(guò),有些同事對(duì)Integer類(lèi)型的兩個(gè)參數(shù)使用==號(hào)比較是否相等?
反正我見(jiàn)過(guò)的,那么這種用法對(duì)嗎?
我的回答是看具體場(chǎng)景,不能說(shuō)一定對(duì),或不對(duì)。
有些狀態(tài)字段,比如:orderStatus有:-1(未下單),0(已下單),1(已支付),2(已完成),3(取消),5種狀態(tài)。
這時(shí)如果用==判斷是否相等:
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1 == orderStatus2);
返回結(jié)果會(huì)是true嗎?
答案:是false。
有些同學(xué)可能會(huì)反駁,Integer中不是有范圍是:-128-127的緩存嗎?
為什么是false?
先看看Integer的構(gòu)造方法:
它其實(shí)并沒(méi)有用到緩存。
那么緩存是在哪里用的?
答案在valueOf方法中:

如果上面的判斷改成這樣:
String orderStatus1 = new String("1");
String orderStatus2 = new String("1");
System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));
返回結(jié)果會(huì)是true嗎?
答案:還真是true。
我們要養(yǎng)成良好編碼習(xí)慣,盡量少用==判斷兩個(gè)Integer類(lèi)型數(shù)據(jù)是否相等,只有在上述非常特殊的場(chǎng)景下才相等。
而應(yīng)該改成使用equals方法判斷:
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1.equals(orderStatus2));
運(yùn)行結(jié)果為true。
2. Objects.equals的坑
假設(shè)現(xiàn)在有這樣一個(gè)需求:判斷當(dāng)前登錄的用戶(hù),如果是我們指定的系統(tǒng)管理員,則發(fā)送一封郵件。系統(tǒng)管理員沒(méi)有特殊的字段標(biāo)識(shí),他的用戶(hù)id=888,在開(kāi)發(fā)、測(cè)試、生產(chǎn)環(huán)境中該值都是一樣的。
這個(gè)需求真的太容易實(shí)現(xiàn)了:
UserInfo userInfo = CurrentUser.getUserInfo();
if(Objects.isNull(userInfo)) {
log.info("請(qǐng)先登錄");
return;
}
if(Objects.equals(userInfo.getId(),888L)) {
sendEmail(userInfo):
}
從當(dāng)前登錄用戶(hù)的上下文中獲取用戶(hù)信息,判斷一下,如果用戶(hù)信息為空,則直接返回。
如果獲取到的用戶(hù)信息不為空,接下來(lái)判斷用戶(hù)id是否等于888。
如果等于888,則發(fā)送郵件。 如果不等于888,則啥事也不干。
當(dāng)我們用id=888的系統(tǒng)管理員賬號(hào)登錄之后,做了相關(guān)操作,滿(mǎn)懷期待的準(zhǔn)備收郵件的時(shí)候,卻發(fā)現(xiàn)收了個(gè)寂寞。
后來(lái),發(fā)現(xiàn)UserInfo類(lèi)是這樣定義的:
@Data
public class UserInfo {
private Integer id;
private String name;
private Integer age;
private String address;
}
此時(shí),有些小伙伴可能會(huì)說(shuō):沒(méi)看出什么問(wèn)題呀。
但我要說(shuō)的是這個(gè)代碼確實(shí)有問(wèn)題。
什么問(wèn)題呢?
下面我們重點(diǎn)看看它的equals方法:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
equals方法的判斷邏輯如下:
該方法先判斷對(duì)象a和b的引用是否相等,如果相等則直接返回true。 如果引用不相等,則判斷a是否為空,如果a為空則返回false。 如果a不為空,調(diào)用對(duì)象的equals方法進(jìn)一步判斷值是否相等。
這就要從Integer的equals方法說(shuō)起來(lái)了。
它的equals方法具體代碼如下:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
先判斷參數(shù)obj是否是Integer類(lèi)型,如果不是,則直接返回false。如果是Integer類(lèi)型,再進(jìn)一步判斷int值是否相等。
而上面這個(gè)例子中b是long類(lèi)型,所以Integer的equals方法直接返回了false。
也就是說(shuō),如果調(diào)用了Integer的equals方法,必須要求入?yún)⒁彩荌nteger類(lèi)型,否則該方法會(huì)直接返回false。
除此之外,還有Byte、Short、Double、Float、Boolean和Character也有類(lèi)似的equals方法判斷邏輯。
常見(jiàn)的坑有:
Long類(lèi)型和Integer類(lèi)型比較,比如:用戶(hù)id的場(chǎng)景。 Byte類(lèi)型和Integer類(lèi)型比較,比如:狀態(tài)判斷的場(chǎng)景。 Double類(lèi)型和Integer類(lèi)型比較,比如:金額為0的判斷場(chǎng)景。
如果你想進(jìn)一步了解Objects.equals方法的問(wèn)題,可以看看我的另一篇文章《Objects.equals有坑》。
3. BigDecimal的坑
通常我們會(huì)把一些小數(shù)類(lèi)型的字段(比如:金額),定義成BigDecimal,而不是Double,避免丟失精度問(wèn)題。
使用Double時(shí)可能會(huì)有這種場(chǎng)景:
double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);
正常情況下預(yù)計(jì)amount2 - amount1應(yīng)該等于0.01
但是執(zhí)行結(jié)果,卻為:
0.009999999999999998
實(shí)際結(jié)果小于預(yù)計(jì)結(jié)果。
Double類(lèi)型的兩個(gè)參數(shù)相減會(huì)轉(zhuǎn)換成二進(jìn)制,因?yàn)镈ouble有效位數(shù)為16位這就會(huì)出現(xiàn)存儲(chǔ)小數(shù)位數(shù)不夠的情況,這種情況下就會(huì)出現(xiàn)誤差。
常識(shí)告訴我們使用BigDecimal能避免丟失精度。
但是使用BigDecimal能避免丟失精度嗎?
答案是否定的。
為什么?
BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));
這個(gè)例子中定義了兩個(gè)BigDecimal類(lèi)型參數(shù),使用構(gòu)造函數(shù)初始化數(shù)據(jù),然后打印兩個(gè)參數(shù)相減后的值。
結(jié)果:
0.0099999999999999984734433411404097569175064563751220703125
不科學(xué)呀,為啥還是丟失精度了?
Jdk中BigDecimal的構(gòu)造方法上有這樣一段描述:

大致的意思是此構(gòu)造函數(shù)的結(jié)果可能不可預(yù)測(cè),可能會(huì)出現(xiàn)創(chuàng)建時(shí)為0.1,但實(shí)際是0.1000000000000000055511151231257827021181583404541015625的情況。
由此可見(jiàn),使用BigDecimal構(gòu)造函數(shù)初始化對(duì)象,也會(huì)丟失精度。
那么,如何才能不丟失精度呢?
BigDecimal amount1 = new BigDecimal(Double.toString(0.02));
BigDecimal amount2 = new BigDecimal(Double.toString(0.03));
System.out.println(amount2.subtract(amount1));
我們可以使用Double.toString方法,對(duì)double類(lèi)型的小數(shù)進(jìn)行轉(zhuǎn)換,這樣能保證精度不丟失。
其實(shí),還有更好的辦法:
BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));
使用BigDecimal.valueOf方法初始化BigDecimal類(lèi)型參數(shù),也能保證精度不丟失。在新版的阿里巴巴開(kāi)發(fā)手冊(cè)中,也推薦使用這種方式創(chuàng)建BigDecimal參數(shù)。
4. Java8 filter的坑
對(duì)于Java8中的Stream用法,大家肯定再熟悉不過(guò)了。
我們通過(guò)對(duì)集合的Stream操作,可以實(shí)現(xiàn):遍歷集合、過(guò)濾數(shù)據(jù)、排序、判斷、轉(zhuǎn)換集合等等,N多功能。
這里重點(diǎn)說(shuō)說(shuō)數(shù)據(jù)的過(guò)濾。
在沒(méi)有Java8之前,我們過(guò)濾數(shù)據(jù)一般是這樣做的:
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;
}
通常需要另一個(gè)集合輔助完成這個(gè)功能。
但如果使用Java8的filter功能,代碼會(huì)變得簡(jiǎn)潔很多,例如:
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());
}
代碼簡(jiǎn)化了很多,完美。
但如果你對(duì)過(guò)濾后的數(shù)據(jù),做修改了:
List<User> userList = queryUser();
List<User> filterList = filterUser(userList);
for(User user: filterList) {
user.setName(user.getName() + "測(cè)試");
}
for(User user: userList) {
System.out.println(user.getName());
}
你當(dāng)時(shí)可能只是想修改過(guò)濾后的數(shù)據(jù),但實(shí)際上,你會(huì)把元素?cái)?shù)據(jù)一同修改了。
意不意外,驚不驚喜?
其根本原因是:過(guò)濾后的集合中,保存的是對(duì)象的引用,該引用只有一份數(shù)據(jù)。
也就是說(shuō),只要有一個(gè)地方,把該引用對(duì)象的成員變量的值,做修改了,其他地方也會(huì)同步修改。
如下圖所示:

5. 自動(dòng)拆箱的坑
Java5之后,提供了自動(dòng)裝箱和自動(dòng)拆箱的功能。
自動(dòng)裝箱是指:JDK會(huì)把基本類(lèi)型,自動(dòng)變成包裝類(lèi)型。
比如:
Integer integer = 1;
等價(jià)于:
Integer integer = new Integer(1);
而自動(dòng)拆箱是指:JDK會(huì)把包裝類(lèi)型,自動(dòng)轉(zhuǎn)換成基本類(lèi)型。
例如:
Integer integer = new Integer(2);
int sum = integer + 5;
等價(jià)于:
Integer integer = new Integer(2);
int sum = integer.intValue() + 5;
但實(shí)際工作中,我們?cè)谑褂米詣?dòng)拆箱時(shí),往往忘記了判空,導(dǎo)致出現(xiàn)NullPointerException異常。
5.1 運(yùn)算
很多時(shí)候,我們需要對(duì)傳入的數(shù)據(jù)進(jìn)行計(jì)算,例如:
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)));
則會(huì)直接報(bào)錯(cuò)。
5.2 傳參
有時(shí)候,我們定義的某個(gè)方法是基本類(lèi)型,但實(shí)際上傳入了包裝類(lèi),比如:
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;
}
如果出現(xiàn)add方法報(bào)NullPointerException異常,你可能會(huì)懵逼,int類(lèi)型怎么會(huì)出現(xiàn)空指針異常呢?
其實(shí),這個(gè)問(wèn)題出在:Integer類(lèi)型的參數(shù),其實(shí)際傳入值為null,JDK字段拆箱,調(diào)用了它的intValue方法導(dǎo)致的問(wèn)題。
6. replace的坑
很多時(shí)候我們?cè)谑褂米址畷r(shí),想把字符串比如:ATYSDFA*Y中的字符A替換成字符B,第一個(gè)想到的可能是使用replace方法。
如果想把所有的A都替換成B,很顯然可以用replaceAll方法,因?yàn)榉浅V庇^(guān),光從方法名就能猜出它的用途。
那么問(wèn)題來(lái)了:replace方法會(huì)替換所有匹配字符嗎?
jdk的官方給出了答案。

該方法會(huì)替換每一個(gè)匹配的字符串。
既然replace和replaceAll都能替換所有匹配字符,那么他們有啥區(qū)別呢?
replace有兩個(gè)重載的方法。
其中一個(gè)方法的參數(shù):char oldChar 和 char newChar,支持字符的替換。
source.replace('A', 'B')
另一個(gè)方法的參數(shù)是:CharSequence target 和 CharSequence replacement,支持字符串的替換。
source.replace("A", "B")
而replaceAll方法的參數(shù)是:String regex 和 String replacement,即基于正則表達(dá)式的替換。
例如對(duì)普通字符串進(jìn)行替換:
source.replaceAll("A", "B")
使用正則表達(dá)替換(將*替換成C):
source.replaceAll("\\*", "C")
順便說(shuō)一下,將*替換成C使用replace方法也可以實(shí)現(xiàn):
source.replace("*", "C")
小伙們看到看到二者的區(qū)別了沒(méi)?使用replace方法無(wú)需對(duì)特殊字符進(jìn)行轉(zhuǎn)義。
不過(guò),千萬(wàn)注意,切勿使用如下寫(xiě)法:
source.replace("\\*", "C")
這種寫(xiě)法會(huì)導(dǎo)致字符串無(wú)法替換。
還有個(gè)小問(wèn)題,如果我只想替換第一個(gè)匹配的字符串該怎么辦?
這時(shí)可以使用replaceFirst方法:
source.replaceFirst("A", "B")
說(shuō)實(shí)話(huà),這里內(nèi)容都很基礎(chǔ),但越基礎(chǔ)的東西,越容易大意失荊州,更容易踩坑。
最后,統(tǒng)計(jì)一下,這些坑一個(gè)都沒(méi)踩過(guò)的同學(xué),麻煩舉個(gè)手。
最后說(shuō)一句(求關(guān)注,別白嫖我)
如果這篇文章對(duì)您有所幫助,或者有所啟發(fā)的話(huà),幫忙關(guān)注一下,您的支持是我堅(jiān)持寫(xiě)作最大的動(dòng)力。
最近我開(kāi)通了股東服務(wù),感興趣的可戳:我開(kāi)通了付費(fèi)渠道
閱讀原文可跳轉(zhuǎn)至消息推送平臺(tái)倉(cāng)庫(kù)
