最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

30個(gè)代碼優(yōu)化的小技巧

2023-07-22 09:05 作者:蘇三說(shuō)技術(shù)  | 我要投稿

前言

我之前寫過(guò)兩篇關(guān)于優(yōu)化相關(guān)的問(wèn)題:《聊聊sql優(yōu)化的15個(gè)小技巧》和《聊聊接口性能優(yōu)化的11個(gè)小技巧》,發(fā)表之后,在全網(wǎng)受到廣大網(wǎng)友的好評(píng)。閱讀量和點(diǎn)贊率都很高,說(shuō)明了這類文章的價(jià)值。

今天接著優(yōu)化這個(gè)話題,我們一起聊聊Java中代碼優(yōu)化的30個(gè)小技巧,希望會(huì)對(duì)你有所幫助。

1.用String.format拼接字符串

不知道你有沒(méi)有拼接過(guò)字符串,特別是那種有多個(gè)參數(shù),字符串比較長(zhǎng)的情況。

比如現(xiàn)在有個(gè)需求:要用get請(qǐng)求調(diào)用第三方接口,url后需要拼接多個(gè)參數(shù)。

以前我們的請(qǐng)求地址是這樣拼接的:

String?url?=?"http://susan.sc.cn?userName="+userName+"&age="+age+"&address="+address+"&sex="+sex+"&roledId="+roleId;

字符串使用+號(hào)拼接,非常容易出錯(cuò)。

后面優(yōu)化了一下,改為使用StringBuilder拼接字符串:

StringBuilder?urlBuilder?=?new?StringBuilder("http://susan.sc.cn?");
urlBuilder.append("userName=")
.append(userName)
.append("&age=")
.append(age)
.append("&address=")
.append(address)
.append("&sex=")
.append(sex)
.append("&roledId=")
.append(roledId);

代碼優(yōu)化之后,稍微直觀點(diǎn)。

但還是看起來(lái)比較別扭。

這時(shí)可以使用String.format方法優(yōu)化:

String?requestUrl?=?"http://susan.sc.cn?userName=%s&age=%s&address=%s&sex=%s&roledId=%s";
String?url?=?String.format(requestUrl,userName,age,address,sex,roledId);

代碼的可讀性,一下子提升了很多。

我們平??梢允褂?code>String.format方法拼接url請(qǐng)求參數(shù),日志打印等字符串。

但不建議在for循環(huán)中用它拼接字符串,因?yàn)樗膱?zhí)行效率,比使用+號(hào)拼接字符串,或者使用StringBuilder拼接字符串都要慢一些。

2.創(chuàng)建可緩沖的IO流

IO流想必大家都使用得比較多,我們經(jīng)常需要把數(shù)據(jù)寫入某個(gè)文件,或者從某個(gè)文件中讀取數(shù)據(jù)到內(nèi)存中,甚至還有可能把文件a,從目錄b,復(fù)制到目錄c下等。

JDK給我們提供了非常豐富的API,可以去操作IO流。

例如:

public?class?IoTest1?{
????public?static?void?main(String[]?args)?{
????????FileInputStream?fis?=?null;
????????FileOutputStream?fos?=?null;
????????try?{
????????????File?srcFile?=?new?File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/1.txt");
????????????File?destFile?=?new?File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/2.txt");
????????????fis?=?new?FileInputStream(srcFile);
????????????fos?=?new?FileOutputStream(destFile);
????????????int?len;
????????????while?((len?=?fis.read())?!=?-1)?{
????????????????fos.write(len);
????????????}
????????????fos.flush();
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????try?{
????????????????if?(fos?!=?null)?{
????????????????????fos.close();
????????????????}
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????????try?{
????????????????if?(fis?!=?null)?{
????????????????????fis.close();
????????????????}
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
}

這個(gè)例子主要的功能,是將1.txt文件中的內(nèi)容復(fù)制到2.txt文件中。這例子使用普通的IO流從功能的角度來(lái)說(shuō),也能滿足需求,但性能卻不太好。

因?yàn)檫@個(gè)例子中,從1.txt文件中讀一個(gè)字節(jié)的數(shù)據(jù),就會(huì)馬上寫入2.txt文件中,需要非常頻繁的讀寫文件。

優(yōu)化:

public?class?IoTest?{
????public?static?void?main(String[]?args)?{
????????BufferedInputStream?bis?=?null;
????????BufferedOutputStream?bos?=?null;
????????FileInputStream?fis?=?null;
????????FileOutputStream?fos?=?null;
????????try?{
????????????File?srcFile?=?new?File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/1.txt");
????????????File?destFile?=?new?File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/2.txt");
????????????fis?=?new?FileInputStream(srcFile);
????????????fos?=?new?FileOutputStream(destFile);
????????????bis?=?new?BufferedInputStream(fis);
????????????bos?=?new?BufferedOutputStream(fos);
????????????byte[]?buffer?=?new?byte[1024];
????????????int?len;
????????????while?((len?=?bis.read(buffer))?!=?-1)?{
????????????????bos.write(buffer,?0,?len);
????????????}
????????????bos.flush();
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????try?{
????????????????if?(bos?!=?null)?{
????????????????????bos.close();
????????????????}
????????????????if?(fos?!=?null)?{
????????????????????fos.close();
????????????????}
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????????try?{
????????????????if?(bis?!=?null)?{
????????????????????bis.close();
????????????????}
????????????????if?(fis?!=?null)?{
????????????????????fis.close();
????????????????}
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
}

這個(gè)例子使用BufferedInputStreamBufferedOutputStream創(chuàng)建了可緩沖的輸入輸出流。

最關(guān)鍵的地方是定義了一個(gè)buffer字節(jié)數(shù)組,把從1.txt文件中讀取的數(shù)據(jù)臨時(shí)保存起來(lái),后面再把該buffer字節(jié)數(shù)組的數(shù)據(jù),一次性批量寫入到2.txt中。

這樣做的好處是,減少了讀寫文件的次數(shù),而我們都知道讀寫文件是非常耗時(shí)的操作。也就是說(shuō)使用可緩存的輸入輸出流,可以提升IO的性能,特別是遇到文件非常大時(shí),效率會(huì)得到顯著提升。

3.減少循環(huán)次數(shù)

在我們?nèi)粘i_(kāi)發(fā)中,循環(huán)遍歷集合是必不可少的操作。

但如果循環(huán)層級(jí)比較深,循環(huán)中套循環(huán),可能會(huì)影響代碼的執(zhí)行效率。

反例

for(User?user:?userList)?{
???for(Role?role:?roleList)?{
??????if(user.getRoleId().equals(role.getId()))?{
?????????user.setRoleName(role.getName());
??????}
???}
}

這個(gè)例子中有兩層循環(huán),如果userList和roleList數(shù)據(jù)比較多的話,需要循環(huán)遍歷很多次,才能獲取我們所需要的數(shù)據(jù),非常消耗cpu資源。

正例

Map<Long,?List<Role>>?roleMap?=?roleList.stream().collect(Collectors.groupingBy(Role::getId));
for?(User?user?:?userList)?{
????List<Role>?roles?=?roleMap.get(user.getRoleId());
????if(CollectionUtils.isNotEmpty(roles))?{
????????user.setRoleName(roles.get(0).getName());
????}
}

減少循環(huán)次數(shù),最簡(jiǎn)單的辦法是,把第二層循環(huán)的集合變成map,這樣可以直接通過(guò)key,獲取想要的value數(shù)據(jù)。

雖說(shuō)map的key存在hash沖突的情況,但遍歷存放數(shù)據(jù)的鏈表或者紅黑樹(shù)時(shí)間復(fù)雜度,比遍歷整個(gè)list集合要小很多。

4.用完資源記得及時(shí)關(guān)閉

在我們?nèi)粘i_(kāi)發(fā)中,可能經(jīng)常訪問(wèn)資源,比如:獲取數(shù)據(jù)庫(kù)連接,讀取文件等。

我們以獲取數(shù)據(jù)庫(kù)連接為例。

反例

//1.?加載驅(qū)動(dòng)類
Class.forName("com.mysql.jdbc.Driver");
//2.?創(chuàng)建連接
Connection?connection?=?DriverManager.getConnection("jdbc:mysql//localhost:3306/db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8","root","123456");
//3.編寫sql
String?sql?="select?*?from?user";
//4.創(chuàng)建PreparedStatement
PreparedStatement?pstmt?=?conn.prepareStatement(sql);
//5.獲取查詢結(jié)果
ResultSet?rs?=?pstmt.execteQuery();
while(rs.next()){
???int?id?=?rs.getInt("id");
???String?name?=?rs.getString("name");
}

上面這段代碼可以正常運(yùn)行,但卻犯了一個(gè)很大的錯(cuò)誤,即:ResultSet、PreparedStatement和Connection對(duì)象的資源,使用完之后,沒(méi)有關(guān)閉。

我們都知道,數(shù)據(jù)庫(kù)連接是非常寶貴的資源。我們不可能一直創(chuàng)建連接,并且用完之后,也不回收,白白浪費(fèi)數(shù)據(jù)庫(kù)資源。

正例

//1.?加載驅(qū)動(dòng)類
Class.forName("com.mysql.jdbc.Driver");

Connection?connection?=?null;
PreparedStatement?pstmt?=?null;
ResultSet?rs?=?null;
try?{
????//2.?創(chuàng)建連接
????connection?=?DriverManager.getConnection("jdbc:mysql//localhost:3306/db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8","root","123456");
????//3.編寫sql
????String?sql?="select?*?from?user";
????//4.創(chuàng)建PreparedStatement
????pstmt?=?conn.prepareStatement(sql);
????//5.獲取查詢結(jié)果
????rs?=?pstmt.execteQuery();
????while(rs.next()){
???????int?id?=?rs.getInt("id");
???????String?name?=?rs.getString("name");
????}
}?catch(Exception?e)?{
??log.error(e.getMessage(),e);
}?finally?{
???if(rs?!=?null)?{
??????rs.close();
???}
???
???if(pstmt?!=?null)?{
??????pstmt.close();
???}
???
???if(connection?!=?null)?{
??????connection.close();
???}
}

這個(gè)例子中,無(wú)論是ResultSet,或者PreparedStatement,還是Connection對(duì)象,使用完之后,都會(huì)調(diào)用close方法關(guān)閉資源。

在這里溫馨提醒一句:ResultSet,或者PreparedStatement,還是Connection對(duì)象,這三者關(guān)閉資源的順序不能反了,不然可能會(huì)出現(xiàn)異常。

5.使用池技術(shù)

我們都知道,從數(shù)據(jù)庫(kù)查數(shù)據(jù),首先要連接數(shù)據(jù)庫(kù),獲取Connection資源。

想讓程序多線程執(zhí)行,需要使用Thread類創(chuàng)建線程,線程也是一種資源。

通常一次數(shù)據(jù)庫(kù)操作的過(guò)程是這樣的:

  1. 創(chuàng)建連接

  2. 進(jìn)行數(shù)據(jù)庫(kù)操作

  3. 關(guān)閉連接

而創(chuàng)建連接和關(guān)閉連接,是非常耗時(shí)的操作,創(chuàng)建連接需要同時(shí)會(huì)創(chuàng)建一些資源,關(guān)閉連接時(shí),需要回收那些資源。

如果用戶的每一次數(shù)據(jù)庫(kù)請(qǐng)求,程序都都需要去創(chuàng)建連接和關(guān)閉連接的話,可能會(huì)浪費(fèi)大量的時(shí)間。

此外,可能會(huì)導(dǎo)致數(shù)據(jù)庫(kù)連接過(guò)多。

我們都知道數(shù)據(jù)庫(kù)的最大連接數(shù)是有限的,以mysql為例,最大連接數(shù)是:100,不過(guò)可以通過(guò)參數(shù)調(diào)整這個(gè)數(shù)量。

如果用戶請(qǐng)求的連接數(shù)超過(guò)最大連接數(shù),就會(huì)報(bào):too many connections異常。如果有新的請(qǐng)求過(guò)來(lái),會(huì)發(fā)現(xiàn)數(shù)據(jù)庫(kù)變得不可用。

這時(shí)可以通過(guò)命令:

show?variables?like?max_connections

查看最大連接數(shù)。

然后通過(guò)命令:

set?GLOBAL?max_connections=1000

手動(dòng)修改最大連接數(shù)。

這種做法只能暫時(shí)緩解問(wèn)題,不是一個(gè)好的方案,無(wú)法從根本上解決問(wèn)題。

最大的問(wèn)題是:數(shù)據(jù)庫(kù)連接數(shù)可以無(wú)限增長(zhǎng),不受控制。

這時(shí)我們可以使用數(shù)據(jù)庫(kù)連接池。

目前Java開(kāi)源的數(shù)據(jù)庫(kù)連接池有:

  • DBCP:是一個(gè)依賴Jakarta commons-pool對(duì)象池機(jī)制的數(shù)據(jù)庫(kù)連接池。

  • C3P0:是一個(gè)開(kāi)放源代碼的JDBC連接池,它在lib目錄中與Hibernate一起發(fā)布,包括了實(shí)現(xiàn)jdbc3和jdbc2擴(kuò)展規(guī)范說(shuō)明的Connection 和Statement 池的DataSources 對(duì)象。

  • Druid:阿里的Druid,不僅是一個(gè)數(shù)據(jù)庫(kù)連接池,還包含一個(gè)ProxyDriver、一系列內(nèi)置的JDBC組件庫(kù)、一個(gè)SQL Parser。

  • Proxool:是一個(gè)Java SQL Driver驅(qū)動(dòng)程序,它提供了對(duì)選擇的其它類型的驅(qū)動(dòng)程序的連接池封裝,可以非常簡(jiǎn)單的移植到已有代碼中。

目前用的最多的數(shù)據(jù)庫(kù)連接池是:Druid。

6.反射時(shí)加緩存

我們都知道通過(guò)反射創(chuàng)建對(duì)象實(shí)例,比使用new關(guān)鍵字要慢很多。

由此,不太建議在用戶請(qǐng)求過(guò)來(lái)時(shí),每次都通過(guò)反射實(shí)時(shí)創(chuàng)建實(shí)例。

有時(shí)候,為了代碼的靈活性,又不得不用反射創(chuàng)建實(shí)例,這時(shí)該怎么辦呢?

答:加緩存。

其實(shí)spring中就使用了大量的反射,我們以支付方法為例。

根據(jù)前端傳入不同的支付code,動(dòng)態(tài)找到對(duì)應(yīng)的支付方法,發(fā)起支付。

我們先定義一個(gè)注解。

@Retention(RetentionPolicy.RUNTIME)??
@Target(ElementType.TYPE)??
public?@interface?PayCode?{??
?????String?value();????
?????String?name();??
}

在所有的支付類上都加上該注解

@PayCode(value?=?"alia",?name?=?"支付寶支付")??
@Service
public?class?AliaPay?implements?IPay?{??

?????@Override
?????public?void?pay()?{??
?????????System.out.println("===發(fā)起支付寶支付===");??
?????}??
}??

@PayCode(value?=?"weixin",?name?=?"微信支付")??
@Service
public?class?WeixinPay?implements?IPay?{??
?
?????@Override
?????public?void?pay()?{??
?????????System.out.println("===發(fā)起微信支付===");??
?????}??
}?
?
@PayCode(value?=?"jingdong",?name?=?"京東支付")??
@Service
public?class?JingDongPay?implements?IPay?{??
?????@Override
?????public?void?pay()?{??
????????System.out.println("===發(fā)起京東支付===");??
?????}??
}

然后增加最關(guān)鍵的類:

@Service
public?class?PayService2?implements?ApplicationListener<ContextRefreshedEvent>?{??
?????private?static?Map<String,?IPay>?payMap?=?null;??
?????
?????@Override
?????public?void?onApplicationEvent(ContextRefreshedEvent?contextRefreshedEvent)?{??
?????????ApplicationContext?applicationContext?=?contextRefreshedEvent.getApplicationContext();??
?????????Map<String,?Object>?beansWithAnnotation?=?applicationContext.getBeansWithAnnotation(PayCode.class);??
????????
?????????if?(beansWithAnnotation?!=?null)?{??
?????????????payMap?=?new?HashMap<>();??
?????????????beansWithAnnotation.forEach((key,?value)?->{??
?????????????????String?bizType?=?value.getClass().getAnnotation(PayCode.class).value();??
?????????????????payMap.put(bizType,?(IPay)?value);??
?????????????});??
?????????}??
?????}??
????
?????public?void?pay(String?code)?{??
????????payMap.get(code).pay();??
?????}??
}

PayService2類實(shí)現(xiàn)了ApplicationListener接口,這樣在onApplicationEvent方法中,就可以拿到ApplicationContext的實(shí)例。這一步,其實(shí)是在spring容器啟動(dòng)的時(shí)候,spring通過(guò)反射我們處理好了。

我們?cè)佾@取打了PayCode注解的類,放到一個(gè)map中,map中的key就是PayCode注解中定義的value,跟code參數(shù)一致,value是支付類的實(shí)例。

這樣,每次就可以每次直接通過(guò)code獲取支付類實(shí)例,而不用if...else判斷了。如果要加新的支付方法,只需在支付類上面打上PayCode注解定義一個(gè)新的code即可。

注意:這種方式的code可以沒(méi)有業(yè)務(wù)含義,可以是純數(shù)字,只要不重復(fù)就行。

7.多線程處理

很多時(shí)候,我們需要在某個(gè)接口中,調(diào)用其他服務(wù)的接口。

比如有這樣的業(yè)務(wù)場(chǎng)景:

在用戶信息查詢接口中需要返回:用戶名稱、性別、等級(jí)、頭像、積分、成長(zhǎng)值等信息。

而用戶名稱、性別、等級(jí)、頭像在用戶服務(wù)中,積分在積分服務(wù)中,成長(zhǎng)值在成長(zhǎng)值服務(wù)中。為了匯總這些數(shù)據(jù)統(tǒng)一返回,需要另外提供一個(gè)對(duì)外接口服務(wù)。

于是,用戶信息查詢接口需要調(diào)用用戶查詢接口、積分查詢接口 和 成長(zhǎng)值查詢接口,然后匯總數(shù)據(jù)統(tǒng)一返回。

調(diào)用過(guò)程如下圖所示:

調(diào)用遠(yuǎn)程接口總耗時(shí) 530ms = 200ms + 150ms + 180ms

顯然這種串行調(diào)用遠(yuǎn)程接口性能是非常不好的,調(diào)用遠(yuǎn)程接口總的耗時(shí)為所有的遠(yuǎn)程接口耗時(shí)之和。

那么如何優(yōu)化遠(yuǎn)程接口性能呢?

上面說(shuō)到,既然串行調(diào)用多個(gè)遠(yuǎn)程接口性能很差,為什么不改成并行呢?

如下圖所示:

調(diào)用遠(yuǎn)程接口總耗時(shí) 200ms = 200ms(即耗時(shí)最長(zhǎng)的那次遠(yuǎn)程接口調(diào)用)

在java8之前可以通過(guò)實(shí)現(xiàn)Callable接口,獲取線程返回結(jié)果。

java8以后通過(guò)CompleteFuture類實(shí)現(xiàn)該功能。我們這里以CompleteFuture為例:

public?UserInfo?getUserInfo(Long?id)?throws?InterruptedException,?ExecutionException?{
????final?UserInfo?userInfo?=?new?UserInfo();
????CompletableFuture?userFuture?=?CompletableFuture.supplyAsync(()?->?{
????????getRemoteUserAndFill(id,?userInfo);
????????return?Boolean.TRUE;
????},?executor);

????CompletableFuture?bonusFuture?=?CompletableFuture.supplyAsync(()?->?{
????????getRemoteBonusAndFill(id,?userInfo);
????????return?Boolean.TRUE;
????},?executor);

????CompletableFuture?growthFuture?=?CompletableFuture.supplyAsync(()?->?{
????????getRemoteGrowthAndFill(id,?userInfo);
????????return?Boolean.TRUE;
????},?executor);
????CompletableFuture.allOf(userFuture,?bonusFuture,?growthFuture).join();

????userFuture.get();
????bonusFuture.get();
????growthFuture.get();

????return?userInfo;
}

溫馨提醒一下,這兩種方式別忘了使用線程池。示例中我用到了executor,表示自定義的線程池,為了防止高并發(fā)場(chǎng)景下,出現(xiàn)線程過(guò)多的問(wèn)題。

8.懶加載

有時(shí)候,創(chuàng)建對(duì)象是一個(gè)非常耗時(shí)的操作,特別是在該對(duì)象的創(chuàng)建過(guò)程中,還需要?jiǎng)?chuàng)建很多其他的對(duì)象時(shí)。

我們以單例模式為例。

在介紹單例模式的時(shí)候,必須要先介紹它的兩種非常著名的實(shí)現(xiàn)方式:餓漢模式?和?懶漢模式。

8.1 餓漢模式

實(shí)例在初始化的時(shí)候就已經(jīng)建好了,不管你有沒(méi)有用到,先建好了再說(shuō)。具體代碼如下:

public?class?SimpleSingleton?{
????//持有自己類的引用
????private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();

????//私有的構(gòu)造方法
????private?SimpleSingleton()?{
????}
????//對(duì)外提供獲取實(shí)例的靜態(tài)方法
????public?static?SimpleSingleton?getInstance()?{
????????return?INSTANCE;
????}
}

使用餓漢模式的好處是:沒(méi)有線程安全的問(wèn)題,但帶來(lái)的壞處也很明顯。

private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();

一開(kāi)始就實(shí)例化對(duì)象了,如果實(shí)例化過(guò)程非常耗時(shí),并且最后這個(gè)對(duì)象沒(méi)有被使用,不是白白造成資源浪費(fèi)嗎?

還真是啊。

這個(gè)時(shí)候你也許會(huì)想到,不用提前實(shí)例化對(duì)象,在真正使用的時(shí)候再實(shí)例化不就可以了?

這就是我接下來(lái)要介紹的:懶漢模式。

8.2 懶漢模式

顧名思義就是實(shí)例在用到的時(shí)候才去創(chuàng)建,“比較懶”,用的時(shí)候才去檢查有沒(méi)有實(shí)例,如果有則返回,沒(méi)有則新建。具體代碼如下:

public?class?SimpleSingleton2?{

????private?static?SimpleSingleton2?INSTANCE;

????private?SimpleSingleton2()?{
????}

????public?static?SimpleSingleton2?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????INSTANCE?=?new?SimpleSingleton2();
????????}
????????return?INSTANCE;
????}
}

示例中的INSTANCE對(duì)象一開(kāi)始是空的,在調(diào)用getInstance方法才會(huì)真正實(shí)例化。

懶漢模式相對(duì)于餓漢模式,沒(méi)有提前實(shí)例化對(duì)象,在真正使用的時(shí)候再實(shí)例化,在實(shí)例化對(duì)象的階段效率更高一些。

除了單例模式之外,懶加載的思想,使用比較多的可能是:

  1. spring的@Lazy注解。在spring容器啟動(dòng)的時(shí)候,不會(huì)調(diào)用其getBean方法初始化實(shí)例。

  2. mybatis的懶加載。在mybatis做級(jí)聯(lián)查詢的時(shí)候,比如查用戶的同時(shí)需要查角色信息。如果用了懶加載,先只查用戶信息,真正使用到角色了,才取查角色信息。

9.初始化集合時(shí)指定大小

我們?cè)趯?shí)際項(xiàng)目開(kāi)發(fā)中,需要經(jīng)常使用集合,比如:ArrayList、HashMap等。

但有個(gè)問(wèn)題:你在初始化集合時(shí)指定了大小的嗎?

反例

public?class?Test2?{

????public?static?void?main(String[]?args)?{
????????List<Integer>?list?=?new?ArrayList<>();
????????long?time1?=?System.currentTimeMillis();
????????for?(int?i?=?0;?i?<?100000;?i++)?{
????????????list.add(i);
????????}
????????System.out.println(System.currentTimeMillis()?-?time1);
????}
}

執(zhí)行時(shí)間:

12

如果在初始化集合時(shí)指定了大小。

正例

public?class?Test2?{

????public?static?void?main(String[]?args)?{
????????List<Integer>?list2?=?new?ArrayList<>(100000);
????????long?time2?=?System.currentTimeMillis();
????????for?(int?i?=?0;?i?<?100000;?i++)?{
????????????list2.add(i);
????????}
????????System.out.println(System.currentTimeMillis()?-?time2);
????}
}

執(zhí)行時(shí)間:

6

我們驚奇的發(fā)現(xiàn),在創(chuàng)建集合時(shí)指定了大小,比沒(méi)有指定大小,添加10萬(wàn)個(gè)元素的效率提升了一倍。

如果你看過(guò)ArrayList源碼,你就會(huì)發(fā)現(xiàn)它的默認(rèn)大小是10,如果添加元素超過(guò)了一定的閥值,會(huì)按1.5倍的大小擴(kuò)容。

你想想,如果裝10萬(wàn)條數(shù)據(jù),需要擴(kuò)容多少次呀?而每次擴(kuò)容都需要不停的復(fù)制元素,從老集合復(fù)制到新集合中,需要浪費(fèi)多少時(shí)間呀。

10.不要滿屏try...catch異常

以前我們?cè)陂_(kāi)發(fā)接口時(shí),如果出現(xiàn)異常,為了給用戶一個(gè)更友好的提示,例如:

@RequestMapping("/test")
@RestController
public?class?TestController?{

????@GetMapping("/add")
????public?String?add()?{
????????int?a?=?10?/?0;
????????return?"成功";
????}
}

如果不做任何處理,當(dāng)我們請(qǐng)求add接口時(shí),執(zhí)行結(jié)果直接報(bào)錯(cuò):

what?用戶能直接看到錯(cuò)誤信息?

這種交互方式給用戶的體驗(yàn)非常差,為了解決這個(gè)問(wèn)題,我們通常會(huì)在接口中捕獲異常:

@GetMapping("/add")
public?String?add()?{
????String?result?=?"成功";
????try?{
????????int?a?=?10?/?0;
????}?catch?(Exception?e)?{
????????result?=?"數(shù)據(jù)異常";
????}
????return?result;
}

接口改造后,出現(xiàn)異常時(shí)會(huì)提示:“數(shù)據(jù)異?!保瑢?duì)用戶來(lái)說(shuō)更友好。

看起來(lái)挺不錯(cuò)的,但是有問(wèn)題。。。

如果只是一個(gè)接口還好,但是如果項(xiàng)目中有成百上千個(gè)接口,都要加上異常捕獲代碼嗎?

答案是否定的,這時(shí)全局異常處理就派上用場(chǎng)了:RestControllerAdvice

@RestControllerAdvice
public?class?GlobalExceptionHandler?{

????@ExceptionHandler(Exception.class)
????public?String?handleException(Exception?e)?{
????????if?(e?instanceof?ArithmeticException)?{
????????????return?"數(shù)據(jù)異常";
????????}
????????if?(e?instanceof?Exception)?{
????????????return?"服務(wù)器內(nèi)部異常";
????????}
????????retur?nnull;
????}
}

只需在handleException方法中處理異常情況,業(yè)務(wù)接口中可以放心使用,不再需要捕獲異常(有人統(tǒng)一處理了)。真是爽歪歪。

11.位運(yùn)算效率更高

如果你讀過(guò)JDK的源碼,比如:ThreadLocal、HashMap等類,你就會(huì)發(fā)現(xiàn),它們的底層都用了位運(yùn)算。

為什么開(kāi)發(fā)JDK的大神們,都喜歡用位運(yùn)算?

答:因?yàn)槲贿\(yùn)算的效率更高。

在ThreadLocal的get、set、remove方法中都有這樣一行代碼:

int?i?=?key.threadLocalHashCode?&?(len-1);

通過(guò)key的hashCode值,數(shù)組的長(zhǎng)度減1。其中key就是ThreadLocal對(duì)象,數(shù)組的長(zhǎng)度減1,相當(dāng)于除以數(shù)組的長(zhǎng)度減1,然后取模。

這是一種hash算法。

接下來(lái)給大家舉個(gè)例子:假設(shè)len=16,key.threadLocalHashCode=31,

于是:int i = 31 & 15 = 15

相當(dāng)于:int i = 31 % 16 = 15

計(jì)算的結(jié)果是一樣的,但是使用與運(yùn)算效率跟高一些。

為什么與運(yùn)算效率更高?

答:因?yàn)門hreadLocal的初始大小是16,每次都是按2倍擴(kuò)容,數(shù)組的大小其實(shí)一直都是2的n次方。

這種數(shù)據(jù)有個(gè)規(guī)律就是高位是0,低位都是1。在做與運(yùn)算時(shí),可以不用考慮高位,因?yàn)榕c運(yùn)算的結(jié)果必定是0。只需考慮低位的與運(yùn)算,所以效率更高。

12.巧用第三方工具類

在Java的龐大體系中,其實(shí)有很多不錯(cuò)的小工具,也就是我們平常說(shuō)的:輪子

如果在我們的日常工作當(dāng)中,能夠?qū)⑦@些輪子用戶,再配合一下idea的快捷鍵,可以極大得提升我們的開(kāi)發(fā)效率。

如果你引入com.google.guava的pom文件,會(huì)獲得很多好用的小工具。這里推薦一款com.google.common.collect包下的集合工具:Lists。

它是在太好用了,讓我愛(ài)不釋手。

如果你想將一個(gè)大集合分成若干個(gè)小集合。

之前我們是這樣做的:

List<Integer>?list?=?Lists.newArrayList(1,?2,?3,?4,?5);

List<List<Integer>>?partitionList?=?Lists.newArrayList();
int?size?=?0;
List<Integer>?dataList?=?Lists.newArrayList();
for(Integer?data?:?list)?{
???if(size?>=?2)?{
??????dataList?=?Lists.newArrayList();
??????size?=?0;
???}?
???size++;
???dataList.add(data);
}

將list按size=2分成多個(gè)小集合,上面的代碼看起來(lái)比較麻煩。

如果使用Listspartition方法,可以這樣寫代碼:

List<Integer>?list?=?Lists.newArrayList(1,?2,?3,?4,?5);
List<List<Integer>>?partitionList?=?Lists.partition(list,?2);
System.out.println(partitionList);

執(zhí)行結(jié)果:

[[1,?2],?[3,?4],?[5]]

這個(gè)例子中,list有5條數(shù)據(jù),我將list集合按大小為2,分成了3頁(yè),即變成3個(gè)小集合。

這個(gè)是我最喜歡的方法之一,經(jīng)常在項(xiàng)目中使用。

比如有個(gè)需求:現(xiàn)在有5000個(gè)id,需要調(diào)用批量用戶查詢接口,查出用戶數(shù)據(jù)。但如果你直接查5000個(gè)用戶,單次接口響應(yīng)時(shí)間可能會(huì)非常慢。如果改成分頁(yè)處理,每次只查500個(gè)用戶,異步調(diào)用10次接口,就不會(huì)有單次接口響應(yīng)慢的問(wèn)題。

如果你了解更多非常有用的第三方工具類的話,可以看看我的另一篇文章《吐血推薦17個(gè)提升開(kāi)發(fā)效率的“輪子”》。

13.用同步代碼塊代替同步方法

在某些業(yè)務(wù)場(chǎng)景中,為了防止多個(gè)線程并發(fā)修改某個(gè)共享數(shù)據(jù),造成數(shù)據(jù)異常。

為了解決并發(fā)場(chǎng)景下,多個(gè)線程同時(shí)修改數(shù)據(jù),造成數(shù)據(jù)不一致的情況。通常情況下,我們會(huì):加鎖。

但如果鎖加得不好,導(dǎo)致鎖的粒度太粗,也會(huì)非常影響接口性能。

在java中提供了synchronized關(guān)鍵字給我們的代碼加鎖。

通常有兩種寫法:在方法上加鎖?和?在代碼塊上加鎖。

先看看如何在方法上加鎖:

public?synchronized?doSave(String?fileUrl)?{
????mkdir();
????uploadFile(fileUrl);
????sendMessage(fileUrl);
}

這里加鎖的目的是為了防止并發(fā)的情況下,創(chuàng)建了相同的目錄,第二次會(huì)創(chuàng)建失敗,影響業(yè)務(wù)功能。

但這種直接在方法上加鎖,鎖的粒度有點(diǎn)粗。因?yàn)閐oSave方法中的上傳文件和發(fā)消息方法,是不需要加鎖的。只有創(chuàng)建目錄方法,才需要加鎖。

我們都知道文件上傳操作是非常耗時(shí)的,如果將整個(gè)方法加鎖,那么需要等到整個(gè)方法執(zhí)行完之后才能釋放鎖。顯然,這會(huì)導(dǎo)致該方法的性能很差,變得得不償失。

這時(shí),我們可以改成在代碼塊上加鎖了,具體代碼如下:

public?void?doSave(String?path,String?fileUrl)?{
????synchronized(this)?{
??????if(!exists(path))?{
??????????mkdir(path);
???????}
????}
????uploadFile(fileUrl);
????sendMessage(fileUrl);
}

這樣改造之后,鎖的粒度一下子變小了,只有并發(fā)創(chuàng)建目錄功能才加了鎖。而創(chuàng)建目錄是一個(gè)非常快的操作,即使加鎖對(duì)接口的性能影響也不大。

最重要的是,其他的上傳文件和發(fā)送消息功能,任然可以并發(fā)執(zhí)行。

14.不用的數(shù)據(jù)及時(shí)清理

在Java中保證線程安全的技術(shù)有很多,可以使用synchroizedLock等關(guān)鍵字給代碼塊加鎖。

但是它們有個(gè)共同的特點(diǎn),就是加鎖會(huì)對(duì)代碼的性能有一定的損耗。

其實(shí),在jdk中還提供了另外一種思想即:用空間換時(shí)間

沒(méi)錯(cuò),使用ThreadLocal類就是對(duì)這種思想的一種具體體現(xiàn)。

ThreadLocal為每個(gè)使用變量的線程提供了一個(gè)獨(dú)立的變量副本,這樣每一個(gè)線程都能獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。

ThreadLocal的用法大致是這樣的:

  1. 先創(chuàng)建一個(gè)CurrentUser類,其中包含了ThreadLocal的邏輯。

public?class?CurrentUser?{
????private?static?final?ThreadLocal<UserInfo>?THREA_LOCAL?=?new?ThreadLocal();
????
????public?static?void?set(UserInfo?userInfo)?{
????????THREA_LOCAL.set(userInfo);
????}
????
????public?static?UserInfo?get()?{
???????THREA_LOCAL.get();
????}
????
????public?static?void?remove()?{
???????THREA_LOCAL.remove();
????}
}

  1. 在業(yè)務(wù)代碼中調(diào)用CurrentUser類。

public?void?doSamething(UserDto?userDto)?{
???UserInfo?userInfo?=?convert(userDto);
???CurrentUser.set(userInfo);
???...

???//業(yè)務(wù)代碼
???UserInfo?userInfo?=?CurrentUser.get();
???...
}

在業(yè)務(wù)代碼的第一行,將userInfo對(duì)象設(shè)置到CurrentUser,這樣在業(yè)務(wù)代碼中,就能通過(guò)CurrentUser.get()獲取到剛剛設(shè)置的userInfo對(duì)象。特別是對(duì)業(yè)務(wù)代碼調(diào)用層級(jí)比較深的情況,這種用法非常有用,可以減少很多不必要傳參。

但在高并發(fā)的場(chǎng)景下,這段代碼有問(wèn)題,只往ThreadLocal存數(shù)據(jù),數(shù)據(jù)用完之后并沒(méi)有及時(shí)清理。

ThreadLocal即使使用了WeakReference(弱引用)也可能會(huì)存在內(nèi)存泄露問(wèn)題,因?yàn)?entry對(duì)象中只把key(即threadLocal對(duì)象)設(shè)置成了弱引用,但是value值沒(méi)有。

那么,如何解決這個(gè)問(wèn)題呢?

public?void?doSamething(UserDto?userDto)?{
???UserInfo?userInfo?=?convert(userDto);
???
???try{
?????CurrentUser.set(userInfo);
?????...
?????
?????//業(yè)務(wù)代碼
?????UserInfo?userInfo?=?CurrentUser.get();
?????...
???}?finally?{
??????CurrentUser.remove();
???}
}

需要在finally代碼塊中,調(diào)用remove方法清理沒(méi)用的數(shù)據(jù)。

15.用equals方法比較是否相等

不知道你在項(xiàng)目中有沒(méi)有見(jiàn)過(guò),有些同事對(duì)Integer類型的兩個(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類型數(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。

16.避免創(chuàng)建大集合

很多時(shí)候,我們?cè)谌粘i_(kāi)發(fā)中,需要?jiǎng)?chuàng)建集合。比如:為了性能考慮,從數(shù)據(jù)庫(kù)查詢某張表的所有數(shù)據(jù),一次性加載到內(nèi)存的某個(gè)集合中,然后做業(yè)務(wù)邏輯處理。

例如:

List<User>?userList?=?userMapper.getAllUser();
for(User?user:userList)?{
???doSamething();
}

從數(shù)據(jù)庫(kù)一次性查詢出所有用戶,然后在循環(huán)中,對(duì)每個(gè)用戶進(jìn)行業(yè)務(wù)邏輯處理。

如果用戶表的數(shù)據(jù)量非常多時(shí),這樣userList集合會(huì)很大,可能直接導(dǎo)致內(nèi)存不足,而使整個(gè)應(yīng)用掛掉。

針對(duì)這種情況,必須做分頁(yè)處理

例如:

private?static?final?int?PAGE_SIZE?=?500;

int?currentPage?=?1;
RequestPage?page?=?new?RequestPage();
page.setPageNo(currentPage);
page.setPageSize(PAGE_SIZE);

Page<User>?pageUser?=?userMapper.search(page);

while(pageUser.getPageCount()?>=?currentPage)?{
????for(User?user:pageUser.getData())?{
???????doSamething();
????}
???page.setPageNo(++currentPage);
???pageUser?=?userMapper.search(page);
}

通過(guò)上面的分頁(yè)改造之后,每次從數(shù)據(jù)庫(kù)中只查詢500條記錄,保存到userList集合中,這樣userList不會(huì)占用太多的內(nèi)存。

這里特別說(shuō)明一下,如果你查詢的表中的數(shù)據(jù)量本來(lái)就很少,一次性保存到內(nèi)存中,也不會(huì)占用太多內(nèi)存,這種情況也可以不做分頁(yè)處理。

此外,還有中特殊的情況,即表中的記錄數(shù)并算不多,但每一條記錄,都有很多字段,單條記錄就占用很多內(nèi)存空間,這時(shí)也需要做分頁(yè)處理,不然也會(huì)有問(wèn)題。

整體的原則是要盡量避免創(chuàng)建大集合,導(dǎo)致內(nèi)存不足的問(wèn)題,但是具體多大才算大集合。目前沒(méi)有一個(gè)唯一的衡量標(biāo)準(zhǔn),需要結(jié)合實(shí)際的業(yè)務(wù)場(chǎng)景進(jìn)行單獨(dú)分析。

17.狀態(tài)用枚舉

在我們建的表中,有很多狀態(tài)字段,比如:訂單狀態(tài)、禁用狀態(tài)、刪除狀態(tài)等。

每種狀態(tài)都有多個(gè)值,代表不同的含義。

比如訂單狀態(tài)有:

  • 1:表示下單

  • 2:表示支付

  • 3:表示完成

  • 4:表示撤銷

如果沒(méi)有使用枚舉,一般是這樣做的:

public?static?final?int?ORDER_STATUS_CREATE?=?1;
public?static?final?int?ORDER_STATUS_PAY?=?2;
public?static?final?int?ORDER_STATUS_DONE?=?3;
public?static?final?int?ORDER_STATUS_CANCEL?=?4;
public?static?final?String?ORDER_STATUS_CREATE_MESSAGE?=?"下單";
public?static?final?String?ORDER_STATUS_PAY?=?"下單";
public?static?final?String?ORDER_STATUS_DONE?=?"下單";
public?static?final?String?ORDER_STATUS_CANCEL?=?"下單";

需要定義很多靜態(tài)常量,包含不同的狀態(tài)和狀態(tài)的描述。

使用枚舉定義之后,代碼如下:

public?enum?OrderStatusEnum?{??
?????CREATE(1,?"下單"),??
?????PAY(2,?"支付"),??
?????DONE(3,?"完成"),??
?????CANCEL(4,?"撤銷");??

?????private?int?code;??
?????private?String?message;??

?????OrderStatusEnum(int?code,?String?message)?{??
?????????this.code?=?code;??
?????????this.message?=?message;??
?????}??
???
?????public?int?getCode()?{??
????????return?this.code;??
?????}??

?????public?String?getMessage()?{??
????????return?this.message;??
?????}??
??
?????public?static?OrderStatusEnum?getOrderStatusEnum(int?code)?{??
????????return?Arrays.stream(OrderStatusEnum.values()).filter(x?->?x.code?==?code).findFirst().orElse(null);??
?????}??
}

使用枚舉改造之后,職責(zé)更單一了。

而且使用枚舉的好處是:

  1. 代碼的可讀性變強(qiáng)了,不同的狀態(tài),有不同的枚舉進(jìn)行統(tǒng)一管理和維護(hù)。

  2. 枚舉是天然單例的,可以直接使用==號(hào)進(jìn)行比較。

  3. code和message可以成對(duì)出現(xiàn),比較容易相關(guān)轉(zhuǎn)換。

  4. 枚舉可以消除if...else過(guò)多問(wèn)題。

18.把固定值定義成靜態(tài)常量

不知道你在實(shí)際的項(xiàng)目開(kāi)發(fā)中,有沒(méi)有使用過(guò)固定值?

例如:

if(user.getId()?<?1000L)?{
???doSamething();
}

或者:

if(Objects.isNull(user))?{
???throw?new?BusinessException("該用戶不存在");
}

其中1000L該用戶不存在是固定值,每次都是一樣的。

既然是固定值,我們?yōu)槭裁床话阉鼈兌x成靜態(tài)常量呢?

這樣語(yǔ)義上更直觀,方便統(tǒng)一管理和維護(hù),更方便代碼復(fù)用。

代碼優(yōu)化為:

private?static?final?int?DEFAULT_USER_ID?=?1000L;
...
if(user.getId()?<?DEFAULT_USER_ID)?{
???doSamething();
}

或者:

private?static?final?String?NOT_FOUND_MESSAGE?=?"該用戶不存在";
...
if(Objects.isNull(user))?{
???throw?new?BusinessException(NOT_FOUND_MESSAGE);
}

使用static final關(guān)鍵字修飾靜態(tài)常量,static表示靜態(tài)的意思,即類變量,而final表示不允許修改。

兩個(gè)關(guān)鍵字加在一起,告訴Java虛擬機(jī)這種變量,在內(nèi)存中只有一份,在全局上是唯一的,不能修改,也就是靜態(tài)常量。

19.避免大事務(wù)

很多小伙伴在使用spring框架開(kāi)發(fā)項(xiàng)目時(shí),為了方便,喜歡使用@Transactional注解提供事務(wù)功能。

沒(méi)錯(cuò),使用@Transactional注解這種聲明式事務(wù)的方式提供事務(wù)功能,確實(shí)能少寫很多代碼,提升開(kāi)發(fā)效率。

但也容易造成大事務(wù),引發(fā)其他的問(wèn)題。

下面用一張圖看看大事務(wù)引發(fā)的問(wèn)題。

從圖中能夠看出,大事務(wù)問(wèn)題可能會(huì)造成接口超時(shí),對(duì)接口的性能有直接的影響。

我們?cè)撊绾蝺?yōu)化大事務(wù)呢?

  1. 少用@Transactional注解

  2. 將查詢(select)方法放到事務(wù)外

  3. 事務(wù)中避免遠(yuǎn)程調(diào)用

  4. 事務(wù)中避免一次性處理太多數(shù)據(jù)

  5. 有些功能可以非事務(wù)執(zhí)行

  6. 有些功能可以異步處理

關(guān)于大事務(wù)問(wèn)題我的另一篇文章《讓人頭痛的大事務(wù)問(wèn)題到底要如何解決?》,它里面做了非常詳細(xì)的介紹,如果大家感興趣可以看看。

20.消除過(guò)長(zhǎng)的if...else

我們?cè)趯懘a的時(shí)候,if...else的判斷條件是必不可少的。不同的判斷條件,走的代碼邏輯通常會(huì)不一樣。

廢話不多說(shuō),先看看下面的代碼。

public?interface?IPay?{??
????void?pay();??
}??

@Service
public?class?AliaPay?implements?IPay?{??
?????@Override
?????public?void?pay()?{??
????????System.out.println("===發(fā)起支付寶支付===");??
?????}??
}??

@Service
public?class?WeixinPay?implements?IPay?{??
?????@Override
?????public?void?pay()?{??
?????????System.out.println("===發(fā)起微信支付===");??
?????}??
}??
??
@Service
public?class?JingDongPay?implements?IPay?{??
?????@Override
?????public?void?pay()?{??
????????System.out.println("===發(fā)起京東支付===");?
?????}??
}??

@Service
public?class?PayService?{??
?????@Autowired
?????private?AliaPay?aliaPay;??
?????@Autowired
?????private?WeixinPay?weixinPay;??
?????@Autowired
?????private?JingDongPay?jingDongPay;??
???
?????public?void?toPay(String?code)?{??
?????????if?("alia".equals(code))?{??
?????????????aliaPay.pay();??
?????????}?elseif?("weixin".equals(code))?{??
??????????????weixinPay.pay();??
?????????}?elseif?("jingdong".equals(code))?{??
??????????????jingDongPay.pay();??
?????????}?else?{??
??????????????System.out.println("找不到支付方式");??
?????????}??
?????}??
}

PayService類的toPay方法主要是為了發(fā)起支付,根據(jù)不同的code,決定調(diào)用用不同的支付類(比如:aliaPay)的pay方法進(jìn)行支付。

這段代碼有什么問(wèn)題呢?也許有些人就是這么干的。

試想一下,如果支付方式越來(lái)越多,比如:又加了百度支付、美團(tuán)支付、銀聯(lián)支付等等,就需要改toPay方法的代碼,增加新的else...if判斷,判斷多了就會(huì)導(dǎo)致邏輯越來(lái)越多?

很明顯,這里違法了設(shè)計(jì)模式六大原則的:開(kāi)閉原則 和 單一職責(zé)原則。

開(kāi)閉原則:對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。就是說(shuō)增加新功能要盡量少改動(dòng)已有代碼。

單一職責(zé)原則:顧名思義,要求邏輯盡量單一,不要太復(fù)雜,便于復(fù)用。

那么,如何優(yōu)化if...else判斷呢?

答:使用?策略模式+工廠模式。

策略模式定義了一組算法,把它們一個(gè)個(gè)封裝起來(lái), 并且使它們可相互替換。工廠模式用于封裝和管理對(duì)象的創(chuàng)建,是一種創(chuàng)建型模式。

public?interface?IPay?{
????void?pay();
}

@Service
public?class?AliaPay?implements?IPay?{

????@PostConstruct
????public?void?init()?{
????????PayStrategyFactory.register("aliaPay",?this);
????}

????@Override
????public?void?pay()?{
????????System.out.println("===發(fā)起支付寶支付===");
????}
}

@Service
public?class?WeixinPay?implements?IPay?{

????@PostConstruct
????public?void?init()?{
????????PayStrategyFactory.register("weixinPay",?this);
????}

????@Override
????public?void?pay()?{
????????System.out.println("===發(fā)起微信支付===");
????}
}

@Service
public?class?JingDongPay?implements?IPay?{

????@PostConstruct
????public?void?init()?{
????????PayStrategyFactory.register("jingDongPay",?this);
????}

????@Override
????public?void?pay()?{
????????System.out.println("===發(fā)起京東支付===");
????}
}

public?class?PayStrategyFactory?{

????private?static?Map<String,?IPay>?PAY_REGISTERS?=?new?HashMap<>();

????public?static?void?register(String?code,?IPay?iPay)?{
????????if?(null?!=?code?&&?!"".equals(code))?{
????????????PAY_REGISTERS.put(code,?iPay);
????????}
????}

????public?static?IPay?get(String?code)?{
????????return?PAY_REGISTERS.get(code);
????}
}

@Service
public?class?PayService3?{

????public?void?toPay(String?code)?{
????????PayStrategyFactory.get(code).pay();
????}
}

這段代碼的關(guān)鍵是PayStrategyFactory類,它是一個(gè)策略工廠,里面定義了一個(gè)全局的map,在所有IPay的實(shí)現(xiàn)類中注冊(cè)當(dāng)前實(shí)例到map中,然后在調(diào)用的地方通過(guò)PayStrategyFactory類根據(jù)code從map獲取支付類實(shí)例即可。

如果加了一個(gè)新的支付方式,只需新加一個(gè)類實(shí)現(xiàn)IPay接口,定義init方法,并且重寫pay方法即可,其他代碼基本上可以不用動(dòng)。

當(dāng)然,消除又臭又長(zhǎng)的if...else判斷,還有很多方法,比如:使用注解、動(dòng)態(tài)拼接類名稱、模板方法、枚舉等等。由于篇幅有限,在這里我就不過(guò)多介紹了,更詳細(xì)的內(nèi)容可以看看我的另一篇文章《消除if...else是9條錦囊妙計(jì)》

21.防止死循環(huán)

有些小伙伴看到這個(gè)標(biāo)題,可能會(huì)感到有點(diǎn)意外,代碼中不是應(yīng)該避免死循環(huán)嗎?為啥還是會(huì)產(chǎn)生死循環(huán)?

殊不知有些死循環(huán)是我們自己寫的,例如下面這段代碼:

while(true)?{
????if(condition)?{
????????break;
????}
????System.out.println("do?samething");
}

這里使用了while(true)的循環(huán)調(diào)用,這種寫法在CAS自旋鎖中使用比較多。

當(dāng)滿足condition等于true的時(shí)候,則自動(dòng)退出該循環(huán)。

如果condition條件非常復(fù)雜,一旦出現(xiàn)判斷不正確,或者少寫了一些邏輯判斷,就可能在某些場(chǎng)景下出現(xiàn)死循環(huán)的問(wèn)題。

出現(xiàn)死循環(huán),大概率是開(kāi)發(fā)人員人為的bug導(dǎo)致的,不過(guò)這種情況很容易被測(cè)出來(lái)。

還有一種隱藏的比較深的死循環(huán),是由于代碼寫的不太嚴(yán)謹(jǐn)導(dǎo)致的。如果用正常數(shù)據(jù),可能測(cè)不出問(wèn)題,但一旦出現(xiàn)異常數(shù)據(jù),就會(huì)立即出現(xiàn)死循環(huán)。

其實(shí),還有另一種死循環(huán):無(wú)限遞歸

如果想要打印某個(gè)分類的所有父分類,可以用類似這樣的遞歸方法實(shí)現(xiàn):

public?void?printCategory(Category?category)?{
??if(category?==?null?
??????||?category.getParentId()?==?null)?{
?????return;
??}?
??System.out.println("父分類名稱:"+?category.getName());
??Category?parent?=?categoryMapper.getCategoryById(category.getParentId());
??printCategory(parent);
}

正常情況下,這段代碼是沒(méi)有問(wèn)題的。

但如果某次有人誤操作,把某個(gè)分類的parentId指向了它自己,這樣就會(huì)出現(xiàn)無(wú)限遞歸的情況。導(dǎo)致接口一直不能返回?cái)?shù)據(jù),最終會(huì)發(fā)生堆棧溢出。

建議寫遞歸方法時(shí),設(shè)定一個(gè)遞歸的深度,比如:分類最大等級(jí)有4級(jí),則深度可以設(shè)置為4。然后在遞歸方法中做判斷,如果深度大于4時(shí),則自動(dòng)返回,這樣就能避免無(wú)限循環(huán)的情況。

22.注意BigDecimal的坑

通常我們會(huì)把一些小數(shù)類型的字段(比如:金額),定義成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類型的兩個(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類型參數(shù),使用構(gòu)造函數(shù)初始化數(shù)據(jù),然后打印兩個(gè)參數(shù)相減后的值。

結(jié)果:

0.0099999999999999984734433411404097569175064563751220703125

不科學(xué)呀,為啥還是丟失精度了?

JdkBigDecimal構(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類型的小數(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類型參數(shù),也能保證精度不丟失。在新版的阿里巴巴開(kāi)發(fā)手冊(cè)中,也推薦使用這種方式創(chuàng)建BigDecimal參數(shù)。

23.盡可能復(fù)用代碼

ctrl + c?和?ctrl + v可能是程序員使用最多的快捷鍵了。

沒(méi)錯(cuò),我們是大自然的搬運(yùn)工。哈哈哈。

在項(xiàng)目初期,我們使用這種工作模式,確實(shí)可以提高一些工作效率,可以少寫(實(shí)際上是少敲)很多代碼。

但它帶來(lái)的問(wèn)題是:會(huì)出現(xiàn)大量的代碼重復(fù)。例如:

@Service
@Slf4j
public?class?TestService1?{

????public?void?test1()??{
????????addLog("test1");
????}

????private?void?addLog(String?info)?{
????????if?(log.isInfoEnabled())?{
????????????log.info("info:{}",?info);
????????}
????}
}
@Service
@Slf4j
public?class?TestService2?{

????public?void?test2()??{
????????addLog("test2");
????}

????private?void?addLog(String?info)?{
????????if?(log.isInfoEnabled())?{
????????????log.info("info:{}",?info);
????????}
????}
}
@Service
@Slf4j
public?class?TestService3?{

????public?void?test3()??{
????????addLog("test3");
????}

????private?void?addLog(String?info)?{
????????if?(log.isInfoEnabled())?{
????????????log.info("info:{}",?info);
????????}
????}
}

在TestService1、TestService2、TestService3類中,都有一個(gè)addLog方法用于添加日志。

本來(lái)該功能用得好好的,直到有一天,線上出現(xiàn)了一個(gè)事故:服務(wù)器磁盤滿了。

原因是打印的日志太多,記了很多沒(méi)必要的日志,比如:查詢接口的所有返回值,大對(duì)象的具體打印等。

沒(méi)辦法,只能將addLog方法改成只記錄debug日志。

于是乎,你需要全文搜索,addLog方法去修改,改成如下代碼:

private?void?addLog(String?info)?{
????if?(log.isDebugEnabled())?{
????????log.debug("debug:{}",?info);
????}
}

這里是有三個(gè)類中需要修改這段代碼,但如果實(shí)際工作中有三十個(gè)、三百個(gè)類需要修改,會(huì)讓你非常痛苦。改錯(cuò)了,或者改漏了,都會(huì)埋下隱患,把自己坑了。

為何不把這種功能的代碼提取出來(lái),放到某個(gè)工具類中呢?

@Slf4j
public?class?LogUtil?{

????private?LogUtil()?{
????????throw?new?RuntimeException("初始化失敗");
????}

????public?static?void?addLog(String?info)?{
????????if?(log.isDebugEnabled())?{
????????????log.debug("debug:{}",?info);
????????}
????}
}

然后,在其他的地方,只需要調(diào)用。

@Service
@Slf4j
public?class?TestService1?{

????public?void?test1()??{
????????LogUtil.addLog("test1");
????}
}

如果哪天addLog的邏輯又要改了,只需要修改LogUtil類的addLog方法即可。你可以自信滿滿的修改,不需要再小心翼翼了。

我們寫的代碼,絕大多數(shù)是可維護(hù)性的代碼,而非一次性的。所以,建議在寫代碼的過(guò)程中,如果出現(xiàn)重復(fù)的代碼,盡量提取成公共方法。千萬(wàn)別因?yàn)轫?xiàng)目初期一時(shí)的爽快,而給項(xiàng)目埋下隱患,后面的維護(hù)成本可能會(huì)非常高。

24.foreach循環(huán)中不remove元素

我們知道在Java中,循環(huán)有很多種寫法,比如:while、for、foreach等。

public?class?Test2?{
????public?static?void?main(String[]?args)?{
????????List<String>?list?=?Lists.newArrayList("a","b","c");
????????for?(String?temp?:?list)?{
????????????if?("c".equals(temp))?{
????????????????list.remove(temp);
????????????}
????????}
????????System.out.println(list);
????}
}

執(zhí)行結(jié)果:

Exception?in?thread?"main"?java.util.ConcurrentModificationException
?at?java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
?at?java.util.ArrayList$Itr.next(ArrayList.java:851)
?at?com.sue.jump.service.test1.Test2.main(Test2.java:24)

這種在foreach循環(huán)中調(diào)用remove方法刪除元素,可能會(huì)報(bào)ConcurrentModificationException異常。

如果想在遍歷集合時(shí),刪除其中的元素,可以用for循環(huán),例如:

public?class?Test2?{

????public?static?void?main(String[]?args)?{
????????List<String>?list?=?Lists.newArrayList("a","b","c");
????????for?(int?i?=?0;?i?<?list.size();?i++)?{
????????????String?temp?=?list.get(i);
????????????if?("c".equals(temp))?{
????????????????list.remove(temp);
????????????}
????????}
????????System.out.println(list);
????}
}

執(zhí)行結(jié)果:

[a,?b]

25.避免隨意打印日志

在我們寫代碼的時(shí)候,打印日志是必不可少的工作之一。

因?yàn)槿罩究梢詭臀覀兛焖俣ㄎ粏?wèn)題,判斷代碼當(dāng)時(shí)真正的執(zhí)行邏輯。

但打印日志的時(shí)候也需要注意,不是說(shuō)任何時(shí)候都要打印日志,比如:

@PostMapping("/query")
public?List<User>?query(@RequestBody?List<Long>?ids)?{
????log.info("request?params:{}",?ids);
????List<User>?userList?=?userService.query(ids);
????log.info("response:{}",?userList);
????return?userList;
}

對(duì)于有些查詢接口,在日志中打印出了請(qǐng)求參數(shù)和接口返回值。

咋一看沒(méi)啥問(wèn)題。

但如果ids中傳入值非常多,比如有1000個(gè)。而該接口被調(diào)用的頻次又很高,一下子就會(huì)打印大量的日志,用不了多久就可能把磁盤空間打滿。

如果真的想打印這些日志該怎么辦?

@PostMapping("/query")
public?List<User>?query(@RequestBody?List<Long>?ids)?{
????if?(log.isDebugEnabled())?{
????????log.debug("request?params:{}",?ids);
????}

????List<User>?userList?=?userService.query(ids);

????if?(log.isDebugEnabled())?{
????????log.debug("response:{}",?userList);
????}
????return?userList;
}

使用isDebugEnabled判斷一下,如果當(dāng)前的日志級(jí)別是debug才打印日志。生產(chǎn)環(huán)境默認(rèn)日志級(jí)別是info,在有些緊急情況下,把某個(gè)接口或者方法的日志級(jí)別改成debug,打印完我們需要的日志后,又調(diào)整回去。

方便我們定位問(wèn)題,又不會(huì)產(chǎn)生大量的垃圾日志,一舉兩得。

26.比較時(shí)把常量寫前面

在比較兩個(gè)參數(shù)值是否相等時(shí),通常我們會(huì)使用==號(hào),或者equals方法。

我在第15章節(jié)中說(shuō)過(guò),使用==號(hào)比較兩個(gè)值是否相等時(shí),可能會(huì)存在問(wèn)題,建議使用equals方法做比較。

反例

if(user.getName().equals("蘇三"))?{
???System.out.println("找到:"+user.getName());
}

在上面這段代碼中,如果user對(duì)象,或者user.getName()方法返回值為null,則都報(bào)NullPointerException異常。

那么,如何避免空指針異常呢?

正例

private?static?final?String?FOUND_NAME?=?"蘇三";
...

if(null?==?user)?{
??return;
}
if(FOUND_NAME.equals(user.getName()))?{
???System.out.println("找到:"+user.getName());
}

在使用equals做比較時(shí),盡量將常量寫在前面,即equals方法的左邊。

這樣即使user.getName()返回的數(shù)據(jù)為null,equals方法會(huì)直接返回false,而不再是報(bào)空指針異常。

27.名稱要見(jiàn)名知意

java中沒(méi)有強(qiáng)制規(guī)定參數(shù)、方法、類或者包名該怎么起名。但如果我們沒(méi)有養(yǎng)成良好的起名習(xí)慣,隨意起名的話,可能會(huì)出現(xiàn)很多奇怪的代碼。

27.1 有意義的參數(shù)名

有時(shí)候,我們寫代碼時(shí)為了省事(可以少敲幾個(gè)字母),參數(shù)名起得越簡(jiǎn)單越好。假如同事A寫的代碼如下:

int?a?=?1;
int?b?=?2;
String?c?=?"abc";
boolean?b?=?false;

一段時(shí)間之后,同事A離職了,同事B接手了這段代碼。

他此時(shí)一臉懵逼,a是什么意思,b又是什么意思,還有c...然后心里一萬(wàn)個(gè)草泥馬。

給參數(shù)起一個(gè)有意義的名字,是非常重要的事情,避免給自己或者別人埋坑。

正解:

int?supplierCount?=?1;
int?purchaserCount?=?2;
String?userName?=?"abc";
boolean?hasSuccess?=?false;

27.2 見(jiàn)名知意

光起有意義的參數(shù)名還不夠,我們不能就這點(diǎn)追求。我們起的參數(shù)名稱最好能夠見(jiàn)名知意,不然就會(huì)出現(xiàn)這樣的情況:

String?yongHuMing?=?"蘇三";
String?用戶Name?=?"蘇三";
String?su3?=?"蘇三";
String?suThree?=?"蘇三";

這幾種參數(shù)名看起來(lái)是不是有點(diǎn)怪怪的?

為啥不定義成國(guó)際上通用的(地球人都能看懂)英文單詞呢?

String?userName?=?"蘇三";
String?susan?=?"蘇三";

上面的這兩個(gè)參數(shù)名,基本上大家都能看懂,減少了好多溝通成本。

所以建議在定義不管是參數(shù)名、方法名、類名時(shí),優(yōu)先使用國(guó)際上通用的英文單詞,更簡(jiǎn)單直觀,減少溝通成本。少用漢子、拼音,或者數(shù)字定義名稱。

27.3 參數(shù)名風(fēng)格一致

參數(shù)名其實(shí)有多種風(fēng)格,列如:

//字母全小寫
int?suppliercount?=?1;

//字母全大寫
int?SUPPLIERCOUNT?=?1;

//小寫字母?+?下劃線
int?supplier_count?=?1;

//大寫字母?+?下劃線
int?SUPPLIER_COUNT?=?1;

//駝峰標(biāo)識(shí)
int?supplierCount?=?1;

如果某個(gè)類中定義了多種風(fēng)格的參數(shù)名稱,看起來(lái)是不是有點(diǎn)雜亂無(wú)章?

所以建議類的成員變量、局部變量和方法參數(shù)使用supplierCount,這種駝峰風(fēng)格,即:第一個(gè)字母小寫,后面的每個(gè)單詞首字母大寫。例如:

int?supplierCount?=?1;

此外,為了好做區(qū)分,靜態(tài)常量建議使用SUPPLIER_COUNT,即:大寫字母?+?下劃線分隔的參數(shù)名。例如:

private?static?final?int?SUPPLIER_COUNT?=?1;

28.SimpleDateFormat線程不安全

在java8之前,我們對(duì)時(shí)間的格式化處理,一般都是用的SimpleDateFormat類實(shí)現(xiàn)的。例如:

@Service
public?class?SimpleDateFormatService?{

????public?Date?time(String?time)?throws?ParseException?{
????????SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");
????????return?dateFormat.parse(time);
????}
}

如果你真的這樣寫,是沒(méi)問(wèn)題的。

就怕哪天抽風(fēng),你覺(jué)得dateFormat是一段固定的代碼,應(yīng)該要把它抽取成常量。

于是把代碼改成下面的這樣:

@Service
public?class?SimpleDateFormatService?{

???private?static?SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");

????public?Date?time(String?time)?throws?ParseException?{
????????return?dateFormat.parse(time);
????}
}

dateFormat對(duì)象被定義成了靜態(tài)常量,這樣就能被所有對(duì)象共用。

如果只有一個(gè)線程調(diào)用time方法,也不會(huì)出現(xiàn)問(wèn)題。

但Serivce類的方法,往往是被Controller類調(diào)用的,而Controller類的接口方法,則會(huì)被tomcat線程池調(diào)用。換句話說(shuō),可能會(huì)出現(xiàn)多個(gè)線程調(diào)用同一個(gè)Controller類的同一個(gè)方法,也就是會(huì)出現(xiàn)多個(gè)線程會(huì)同時(shí)調(diào)用time方法。

而time方法會(huì)調(diào)用SimpleDateFormat類的parse方法:

@Override
public?Date?parse(String?text,?ParsePosition?pos)?{
????...
????Date?parsedDate;
????try?{
????????parsedDate?=?calb.establish(calendar).getTime();
????????...
????}?catch?(IllegalArgumentException?e)?{
????????pos.errorIndex?=?start;
????????pos.index?=?oldStart;
????????return?null;
????}
???return?parsedDate;
}?

該方法會(huì)調(diào)用establish方法:

Calendar?establish(Calendar?cal)?{
????...
????//1.清空數(shù)據(jù)
????cal.clear();
????//2.設(shè)置時(shí)間
????cal.set(...);
????//3.返回
????return?cal;
}

其中的步驟1、2、3是非原子操作。

但如果cal對(duì)象是局部變量還好,壞就壞在parse方法調(diào)用establish方法時(shí),傳入的calendar是SimpleDateFormat類的父類DateFormat的成員變量:

public?abstract?class?DateFormat?extends?Forma?{
????....
????protected?Calendar?calendar;
????...
}

這樣就可能會(huì)出現(xiàn)多個(gè)線程,同時(shí)修改同一個(gè)對(duì)象即:dateFormat,它的同一個(gè)成員變量即:Calendar值的情況。

這樣可能會(huì)出現(xiàn),某個(gè)線程設(shè)置好了時(shí)間,又被其他的線程修改了,從而出現(xiàn)時(shí)間錯(cuò)誤的情況。

那么,如何解決這個(gè)問(wèn)題呢?

  1. SimpleDateFormat類的對(duì)象不要定義成靜態(tài)的,可以改成方法的局部變量。

  2. 使用ThreadLocal保存SimpleDateFormat類的數(shù)據(jù)。

  3. 使用java8的DateTimeFormatter類。

29.少用Executors創(chuàng)建線程池

我們都知道JDK5之后,提供了ThreadPoolExecutor類,用它可以自定義線程池。

線程池的好處有很多,下面主要說(shuō)說(shuō)這3個(gè)方面。

  1. 降低資源消耗:避免了頻繁的創(chuàng)建線程和銷毀線程,可以直接復(fù)用已有線程。而我們都知道,創(chuàng)建線程是非常耗時(shí)的操作。

  2. 提供速度:任務(wù)過(guò)來(lái)之后,因?yàn)榫€程已存在,可以拿來(lái)直接使用。

  3. 提高線程的可管理性:線程是非常寶貴的資源,如果創(chuàng)建過(guò)多的線程,不僅會(huì)消耗系統(tǒng)資源,甚至?xí)绊懴到y(tǒng)的穩(wěn)定。使用線程池,可以非常方便的創(chuàng)建、管理和監(jiān)控線程。

當(dāng)然JDK為了我們使用更便捷,專門提供了:Executors類,給我們快速創(chuàng)建線程池。

該類中包含了很多靜態(tài)方法

  • newCachedThreadPool:創(chuàng)建一個(gè)可緩沖的線程,如果線程池大小超過(guò)處理需要,可靈活回收空閑線程,若無(wú)可回收,則新建線程。

  • newFixedThreadPool:創(chuàng)建一個(gè)固定大小的線程池,如果任務(wù)數(shù)量超過(guò)線程池大小,則將多余的任務(wù)放到隊(duì)列中。

  • newScheduledThreadPool:創(chuàng)建一個(gè)固定大小,并且能執(zhí)行定時(shí)周期任務(wù)的線程池。

  • newSingleThreadExecutor:創(chuàng)建只有一個(gè)線程的線程池,保證所有的任務(wù)安裝順序執(zhí)行。

在高并發(fā)的場(chǎng)景下,如果大家使用這些靜態(tài)方法創(chuàng)建線程池,會(huì)有一些問(wèn)題。

那么,我們一起看看有哪些問(wèn)題?

  • newFixedThreadPool:允許請(qǐng)求的隊(duì)列長(zhǎng)度是Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致OOM。

  • newSingleThreadExecutor:允許請(qǐng)求的隊(duì)列長(zhǎng)度是Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致OOM。

  • newCachedThreadPool:允許創(chuàng)建的線程數(shù)是Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致OOM。

那我們?cè)撛蹀k呢?

優(yōu)先推薦使用ThreadPoolExecutor類,我們自定義線程池。

具體代碼如下:

ExecutorService?threadPool?=?new?ThreadPoolExecutor(
????8,?//corePoolSize線程池中核心線程數(shù)
????10,?//maximumPoolSize?線程池中最大線程數(shù)
????60,?//線程池中線程的最大空閑時(shí)間,超過(guò)這個(gè)時(shí)間空閑線程將被回收
????TimeUnit.SECONDS,//時(shí)間單位
????new?ArrayBlockingQueue(500),?//隊(duì)列
????new?ThreadPoolExecutor.CallerRunsPolicy());?//拒絕策略

順便說(shuō)一下,如果是一些低并發(fā)場(chǎng)景,使用Executors類創(chuàng)建線程池也未嘗不可,也不能完全一棍子打死。在這些低并發(fā)場(chǎng)景下,很難出現(xiàn)OOM問(wèn)題,所以我們需要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景選擇。

30.Arrays.asList轉(zhuǎn)換的集合別修改

在我們?nèi)粘9ぷ髦校?jīng)常需要把數(shù)組轉(zhuǎn)換成List集合。

因?yàn)閿?shù)組的長(zhǎng)度是固定的,不太好擴(kuò)容,而List的長(zhǎng)度是可變的,它的長(zhǎng)度會(huì)根據(jù)元素的數(shù)量動(dòng)態(tài)擴(kuò)容。

在JDK的Arrays類中提供了asList方法,可以把數(shù)組轉(zhuǎn)換成List。

正例

String?[]?array?=?new?String?[]?{"a","b","c"};
List<String>?list?=?Arrays.asList(array);
for?(String?str?:?list)?{
????System.out.println(str);
}

在這個(gè)例子中,使用Arrays.asList方法將array數(shù)組,直接轉(zhuǎn)換成了list。然后在for循環(huán)中遍歷list,打印出它里面的元素。

如果轉(zhuǎn)換后的list,只是使用,沒(méi)新增或修改元素,不會(huì)有問(wèn)題。

反例

String[]?array?=?new?String[]{"a",?"b",?"c"};
List<String>?list?=?Arrays.asList(array);
list.add("d");
for?(String?str?:?list)?{
????System.out.println(str);
}

執(zhí)行結(jié)果:

Exception?in?thread?"main"?java.lang.UnsupportedOperationException
at?java.util.AbstractList.add(AbstractList.java:148)
at?java.util.AbstractList.add(AbstractList.java:108)
at?com.sue.jump.service.test1.Test2.main(Test2.java:24)

會(huì)直接報(bào)UnsupportedOperationException異常。

為什么呢?

答:使用Arrays.asList方法轉(zhuǎn)換后的ArrayList,是Arrays類的內(nèi)部類,并非java.util包下我們常用的ArrayList。

Arrays類的內(nèi)部ArrayList類,它沒(méi)有實(shí)現(xiàn)父類的add和remove方法,用的是父類AbstractList的默認(rèn)實(shí)現(xiàn)。

我們看看AbstractList是如何實(shí)現(xiàn)的:

public?void?add(int?index,?E?element)?{
???throw?new?UnsupportedOperationException();
}

public?E?remove(int?index)?{
???throw?new?UnsupportedOperationException();
}

該類的addremove方法直接拋異常了,因此調(diào)用Arrays類的內(nèi)部ArrayList類的add和remove方法,同樣會(huì)拋異常。

說(shuō)實(shí)話,Java代碼優(yōu)化是一個(gè)比較大的話題,它里面可以優(yōu)化的點(diǎn)非常多,我沒(méi)辦法一一列舉完。在這里只能拋磚引玉,介紹一下比較常見(jiàn)的知識(shí)點(diǎn),更全面的內(nèi)容,需要小伙伴們自己去思考和探索。

這篇文章寫了很久,花了很多時(shí)間和心思,如果你看了文章有些收獲,記得給我點(diǎn)贊鼓勵(lì)一下喔。

最后歡迎大家加入蘇三的知識(shí)星球【Java突擊隊(duì)】,一起學(xué)習(xí)。

星球中有很多獨(dú)家的干貨內(nèi)容,比如:Java后端學(xué)習(xí)路線,分享實(shí)戰(zhàn)項(xiàng)目,源碼分析,百萬(wàn)級(jí)系統(tǒng)設(shè)計(jì),系統(tǒng)上線的一些坑,MQ專題,真實(shí)面試題,每天都會(huì)回答大家提出的問(wèn)題,免費(fèi)修改簡(jiǎn)歷,免費(fèi)回答工作中的問(wèn)題。

星球目前開(kāi)通了6個(gè)優(yōu)質(zhì)專欄:技術(shù)選型、系統(tǒng)設(shè)計(jì)、Spring源碼解讀、痛點(diǎn)問(wèn)題、高頻面試題 和 性能優(yōu)化。



  • 每一個(gè)專欄都是大家非常關(guān)心,和非常有價(jià)值的話題,我相信在專欄中你會(huì)學(xué)到很多東西,值回票價(jià)。

  • 目前僅需99元,后面應(yīng)該會(huì)漲到199元。


    加入星球如果不滿意,3天內(nèi)包退。


30個(gè)代碼優(yōu)化的小技巧的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
且末县| 六安市| 高安市| 察隅县| 长武县| 万山特区| 织金县| 浦北县| 望都县| 英吉沙县| 叙永县| 周口市| 杭州市| 高雄县| 中山市| 金华市| 疏附县| 吉安县| 景宁| 民权县| 新河县| 永福县| 蓝田县| 黄梅县| 思南县| 措勤县| 无锡市| 宁国市| 凤庆县| 霍州市| 望江县| 龙川县| 荔浦县| 公安县| 镇巴县| 包头市| 池州市| 五原县| 安徽省| 雷山县| 广东省|