七大陷阱!99%的Java開發(fā)者都會(huì)遇到
環(huán)境:SpringBoot3.2.5
1. replace是否會(huì)替換所有字符?
在處理字符串時(shí),我們經(jīng)常需要替換字符串中的字符,比如在字符串 "ACDAB$%^&A*Y" 中將 A 替換為 X。首先想到的方法可能就是使用 replace 方法。
如果目的是將所有出現(xiàn)的 A 都替換為 X,那么使用 replaceAll 方法似乎很直觀。方法名本身就清楚地表明了它的用途。
于是問題來了:replace 方法會(huì)替換所有匹配的字符嗎?
JDK文檔說明:
圖片
翻譯:該方法將此字符串中每個(gè)與目標(biāo)字面量序列相匹配的子字符串替換為指定的替換字面量序列。替換操作從字符串的開頭到結(jié)尾依次進(jìn)行,例如,在字符串 "aaa" 中將 "aa" 替換為 "b" 將得到 "ba" 而不是 "ab"。
那么 replace 與 replaceAll 的區(qū)別?
replace 方法有2個(gè)重載的方法:
String str = "ACDAB$%^&A*Y" ;
System.err.println(str.replace('A', 'X')) ;
System.err.println(str.replace("A", "X")) ;
replaceAll 方法簽名:
public String replaceAll(String regex, String replacement)
可以通過正則表達(dá)式的方式進(jìn)行替換。如下示例:
String str = "ACDAB$%^&A*Y" ;
// 簡單字符串替換
System.err.println(str.replaceAll("A", "X")) ;
// 正則替換,替換 '*' 字符,需要轉(zhuǎn)義
System.err.println(str.replaceAll("\\*", "XO")) ;
如果僅僅是將 '*' 進(jìn)行替換,那么使用replace更簡單
System.err.println(str.replace("*", "XO")) ;
以上都是替換整個(gè)字符串中匹配的,如果你只希望替換第一個(gè)出現(xiàn)的,那么可以使用如下方法:
System.err.println(str.replaceFirst("A", "-")) ;
第一個(gè)參數(shù)接受的是正則表達(dá)式。
2. Integer類型不要用 "==" 判斷
這不是絕對的,需要看情況,你比較的數(shù)值大小了。如下示例:
Integer a = 1 ;
Integer b = 1 ;
System.err.printf("a == b ? %s%n", a == b) ;
a = 128 ;
b = 128 ;
System.err.printf("a == b ? %s%n", a == b) ;
a = -128 ;
b = -128 ;
System.err.printf("a == b ? %s%n", a == b) ;
a = -129 ;
b = -129 ;
System.err.printf("a == b ? %s%n", a == b) ;
輸出結(jié)果:
a == b ? true
a == b ? false
a == b ? true
a == b ? false
為什么這樣?通過javap反編譯后
圖片
當(dāng)我們將值賦給Integer類型變量時(shí)調(diào)用的是Integer#valueOf靜態(tài)方法,該方法簽名如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)] ;
return new Integer(i);
}
這里的low與high取值如下:
圖片
默認(rèn)[low, high] = [-128, 127],也就是說默認(rèn)Integer緩存了這個(gè)范圍的數(shù)字,只要取值在這個(gè)范圍,那么都將返回緩存的數(shù)據(jù)。這也就是上面輸出結(jié)果的原因了。
通過上面的源碼我們也看到了,我們是可以通過jvm參數(shù)來改變這默認(rèn)緩存大小的,運(yùn)行程序時(shí)添加如下的jvm參數(shù):
-Djava.lang.Integer.IntegerCache.high=128
這樣設(shè)置后,我們在運(yùn)行上面程序,128的比較將打印true 。
3. 使用BigDecimal能否避免精度損失?
通常,對于涉及小數(shù)(例如金額)的字段,我們會(huì)將它們定義為BigDecimal而不是Double,以避免精度損失??紤]以下使用Double的場景:
double a = 0.02;
double b = 0.03;
System.out.println(a - b);
最終結(jié)果我們期望的是0.01,但實(shí)際是:
0.009999999999999998
這是因?yàn)閮蓚€(gè)double值的減法運(yùn)算會(huì)被轉(zhuǎn)換為二進(jìn)制形式,而double的有效數(shù)字精度限制為16位,這可能導(dǎo)致小數(shù)位的存儲(chǔ)不足,從而產(chǎn)生誤差。
那么使用BigDecimal是否能解決呢?
BigDecimal a1 = new BigDecimal(0.02) ;
BigDecimal b1 = new BigDecimal(0.03) ;
System.err.println(b1.subtract(a1)) ;
執(zhí)行結(jié)果
0.0099999999999999984734433411404097569175064563751220703125
為什么?我們先看看BigDecimal的構(gòu)造函數(shù)說明:
圖片
我們看上面的第一點(diǎn)即可:
翻譯:這個(gè)構(gòu)造函數(shù)的結(jié)果可能會(huì)有些不可預(yù)測。人們可能會(huì)認(rèn)為,在Java中寫new BigDecimal(0.1)會(huì)創(chuàng)建一個(gè)完全等于0.1的BigDecimal(未縮放值為1,精度為1),但實(shí)際上它等于0.1000000000000000055511151231257827021181583404541015625。這是因?yàn)?.1在雙精度浮點(diǎn)數(shù)(或者任何有限長度的二進(jìn)制小數(shù))中無法被精確表示。因此,盡管表面上看起來如此,但傳遞給構(gòu)造函數(shù)的值并不完全等于0.1。
這也說明了,我們直接通過構(gòu)造函數(shù)傳入的double類型進(jìn)行計(jì)算是有風(fēng)險(xiǎn)的。
接著我們看第二點(diǎn):
翻譯:String 構(gòu)造函數(shù)是完全可預(yù)測的:寫 new BigDecimal("0.1") 會(huì)創(chuàng)建一個(gè)完全等于 0.1 的 BigDecimal,正如人們所期望的那樣。因此,通常建議優(yōu)先使用 String 構(gòu)造函數(shù)而不是這個(gè)(指直接使用 double 值的)構(gòu)造函數(shù)。
我們將上面的代碼改為如下:
BigDecimal aa = new BigDecimal("0.02") ;
BigDecimal bb = new BigDecimal("0.03") ;
System.err.println(bb.subtract(aa)) ;
// 0.01
輸出正確
我們還可以通過如下的方式:
aa = BigDecimal.valueOf(0.02) ;
bb = BigDecimal.valueOf(0.03) ;
System.err.println(bb.subtract(aa)) ;
// 0.01
此種方式是不是更加方便。其BigDecimal#valueOf內(nèi)如如下:
圖片
關(guān)于BigDecimal更多內(nèi)容請查看下面文章:
不想被坑?快來了解BigDecimal的陷阱。
4. 是否真的不能使用 "+" 拼接字符串?
字符串值被視為不可變的序列。這意味著一旦定義了字符串對象,其數(shù)據(jù)就不能被修改。如果需要進(jìn)行修改,則會(huì)創(chuàng)建一個(gè)新的對象。如下示例:
String a = "123" ;
String b = "456" ;
String c = a + b ;
System.out.println(c) ;
在涉及大量字符串拼接的場景中,使用String對象會(huì)創(chuàng)建許多不必要的中間對象。這不僅浪費(fèi)內(nèi)存空間,還會(huì)降低效率。
在這種情況下,我們可以使用更高效的可變字符序列,如StringBuilder或StringBuffer來定義對象。
那么,StringBuilder和StringBuffer有什么區(qū)別呢?
主要區(qū)別在于,StringBuffer在其主要方法上添加了synchronized關(guān)鍵字,而StringBuilder則沒有。因此:
- StringBuffer是線程安全的。
- StringBuilder不是線程安全的。
在大多數(shù)情況下,建議使用StringBuilder進(jìn)行字符串拼接,觸發(fā)你需要在多線程環(huán)境下進(jìn)行字符串的操作。
StringBuilder中的append方法可以在不創(chuàng)建中間對象的情況下拼接字符串,因此它更高效,而且它不是同步的。
String a = "123";
String b = "456";
StringBuilder c = new StringBuilder();
c.append(a).append(b);
System.out.println(c);
那么使用String進(jìn)行字符串拼接是否總是比使用StringBuilder效率低?
首先,我們通過javap反編譯上面使用StringBuilder的代碼:
圖片
通過反編譯,定義了2個(gè)String變量,創(chuàng)建一個(gè)StringBuilder對象,最后使用了2次append方法。
最后,我們再反編譯使用 "+" 操作符的方式:
圖片
對比下,基本一樣啊。
注意:從JDK 5開始,Java對String類型的字符串的+操作進(jìn)行了優(yōu)化。這個(gè)操作在編譯成字節(jié)碼文件時(shí),+操作會(huì)被轉(zhuǎn)換成StringBuilder的append方法調(diào)用,以提高效率。
5. isEmpty & isBlank區(qū)別
當(dāng)我們執(zhí)行字符串操作時(shí),經(jīng)常需要檢查字符串是否為空。如果我們不使用任何工具,通常會(huì)像這樣進(jìn)行檢查:
public static void check(String source) {
if (null != source && !"".equals(source)) {
System.out.println("not empty");
}
}
如果我們每次都需要進(jìn)行這樣的檢查,那可能會(huì)非常繁瑣。推薦使用Apache Commons Lang 3中的StringUtils類,它包含了許多有用的空值檢查方法:isEmpty、isBlank、isNotEmpty、isNotBlank,以及其他字符串處理方法。
接下來, 我們來看看isEmpty與isBlank的區(qū)別。
StringUtils.isEmpty(null) ;
StringUtils.isEmpty("") ;
StringUtils.isEmpty(" ") ;
StringUtils.isEmpty("bob") ;
StringUtils.isEmpty(" bob ") ;
使用isBlank
StringUtils.isBlank(null) = true
StringUtils.isBlank("") = true
StringUtils.isBlank(" ") = true
StringUtils.isBlank("bob") = false
StringUtils.isBlank(" bob ") = false
這兩種方法的關(guān)鍵區(qū)別在于,對于空字符串 " " 的情況,isEmpty 返回 false,而 isBlank 返回 true。
6. Mapper返回的集合List是否進(jìn)行Null檢查?
如下代碼,是否需要進(jìn)行null檢查?
List<User> list = userMapper.query(search);
if (CollectionUtils.isNotEmpty(list)) {
List<Long> idList = list.stream().map(User::getId).collect(Collectors.toList());
}
注:CollectionUtils使用的是commons-collections4包。內(nèi)部如下調(diào)用
public static boolean isEmpty(final Collection<?> coll) {
return coll == null || coll.isEmpty();
}
現(xiàn)在我們要確定的是如果基于MyBatis查詢返回的集合是否需要進(jìn)行null檢查呢?
查看MyBatis源碼,DefaultResultSetHandler#handleResultSets方法。
圖片
collapseSingleResultList方法
private List<Object> collapseSingleResultList(List<Object> multipleResults) {
return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
}
通過查看源碼得知,我們沒有必要進(jìn)行null的檢查,得到結(jié)果后可以直接進(jìn)行使用。
7. 正確使用indexOf方法
首先,我們先來看看下面的代碼:
String source = "#ABBXXXOOO*pack";
if (source.indexOf("#") > 0) {
System.out.println("success") ;
// TODO
}
此代碼并不會(huì)輸出任何東西。indexOf如果不存在,那么返回 -1。該方法的說明:
圖片
指定子字符串第一次出現(xiàn)的索引,如果沒有這樣的出現(xiàn),則返回-1。
indexOf方法返回指定元素在字符串中的位置,從0開始計(jì)數(shù)。在上面的例子中,#位于字符串的第一個(gè)位置,所以indexOf方法返回的值實(shí)際上是0。
所以,這里我們應(yīng)該這樣判斷
if (source.indexOf("#") > -1) {
// ...
}
但是,我覺得下面的方法更好:
if (source.contains("#")) {
System.err.println("contains success") ;
}