分享 45 個(gè)實(shí)用的代碼優(yōu)化小技巧!
近期文章精選 :
Java面試指南網(wǎng)站:javaguide.cn
不知道大家有沒(méi)有經(jīng)歷過(guò)維護(hù)一個(gè)已經(jīng)離職的人的代碼的痛苦,一個(gè)方法寫(xiě)老長(zhǎng),還有很多的 if else ,根本無(wú)法閱讀,更不知道代碼背后的含義,最重要的是沒(méi)有人可以問(wèn),此時(shí)只能心里默默地問(wèn)候這個(gè)留坑的兄弟。。
其實(shí)造成這些原因的很大一部分原因是由于代碼規(guī)范的問(wèn)題,如果寫(xiě)的規(guī)范,注釋好,其實(shí)很多問(wèn)題也就解決了。所以本文我就從代碼的編寫(xiě)規(guī)范,格式的優(yōu)化,設(shè)計(jì)原則和一些常見(jiàn)的代碼優(yōu)化的技巧等方面總結(jié)了了 45 個(gè)小技巧分享給大家,如果不足,歡迎指正。

1、規(guī)范命名
命名是寫(xiě)代碼中最頻繁的操作,比如類(lèi)、屬性、方法、參數(shù)等。好的名字應(yīng)當(dāng)能遵循以下幾點(diǎn):
見(jiàn)名知意
比如需要定義一個(gè)變量需要來(lái)計(jì)數(shù)
int i = 0;
名稱(chēng) i 沒(méi)有任何的實(shí)際意義,沒(méi)有體現(xiàn)出數(shù)量的意思,所以我們應(yīng)當(dāng)指明數(shù)量的名稱(chēng)
int count = 0;
能夠讀的出來(lái)
如下代碼:
private String sfzh;
private String dhhm;
這些變量的名稱(chēng),根本讀不出來(lái),更別說(shuō)實(shí)際意義了。
所以我們可以使用正確的可以讀出來(lái)的英文來(lái)命名
private String idCardNo;
private String phone;
2、規(guī)范代碼格式
好的代碼格式能夠讓人感覺(jué)看起來(lái)代碼更加舒適。
好的代碼格式應(yīng)當(dāng)遵守以下幾點(diǎn):
合適的空格 代碼對(duì)齊,比如大括號(hào)要對(duì)齊 及時(shí)換行,一行不要寫(xiě)太多代碼
好在現(xiàn)在開(kāi)發(fā)工具支持一鍵格式化,可以幫助美化代碼格式。
3、寫(xiě)好代碼注釋
在《代碼簡(jiǎn)潔之道》這本書(shū)中作者提到了一個(gè)觀點(diǎn),注釋的恰當(dāng)用法是用來(lái)彌補(bǔ)我們?cè)谟么a表達(dá)意圖時(shí)的失敗。換句話說(shuō),當(dāng)無(wú)法通過(guò)讀代碼來(lái)了解代碼所表達(dá)的意思的時(shí)候,就需要用注釋來(lái)說(shuō)明。
作者之所以這么說(shuō),是因?yàn)樽髡哂X(jué)得隨著時(shí)間的推移,代碼可能會(huì)變動(dòng),如果不及時(shí)更新注釋?zhuān)敲醋⑨尵腿菀桩a(chǎn)生誤導(dǎo),偏離代碼的實(shí)際意義。而不及時(shí)更新注釋的原因是,程序員不喜歡寫(xiě)注釋。(作者很懂?。?/p>
但是這不意味著可以不寫(xiě)注釋?zhuān)?dāng)通過(guò)代碼如果無(wú)法表達(dá)意思的時(shí)候,就需要注釋?zhuān)热缛缦麓a
for (Integer id : ids) {
if (id == 0) {
continue;
}
//做其他事
}
為什么 id == 0 需要跳過(guò),代碼是無(wú)法看出來(lái)了,就需要注釋了。
好的注釋?xiě)?yīng)當(dāng)滿足一下幾點(diǎn):
解釋代碼的意圖,說(shuō)明為什么這么寫(xiě),用來(lái)做什么 對(duì)參數(shù)和返回值注釋?zhuān)雲(yún)⒋硎裁矗鰠⒋硎裁?/section> 有警示作用,比如說(shuō)入?yún)⒉荒転榭?,或者代碼是不是有坑 當(dāng)代碼還未完成時(shí)可以使用 todo 注釋來(lái)注釋
4、try catch 內(nèi)部代碼抽成一個(gè)方法
try catch 代碼有時(shí)會(huì)干擾我們閱讀核心的代碼邏輯,這時(shí)就可以把 try catch 內(nèi)部主邏輯抽離成一個(gè)單獨(dú)的方法
如下圖是 Eureka 服務(wù)端源碼中服務(wù)下線的實(shí)現(xiàn)中的一段代碼

整個(gè)方法非常長(zhǎng),try 中代碼是真正的服務(wù)下線的代碼實(shí)現(xiàn),finally 可以保證讀鎖最終一定可以釋放。
所以這段代碼其實(shí)就可以對(duì)核心的邏輯進(jìn)行抽取。
protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
read.lock();
doInternalCancel(appName, id, isReplication);
} finally {
read.unlock();
}
// 剩余代碼
}
private boolean doInternalCancel(String appName, String id, boolean isReplication) {
//真正處理下線的邏輯
}
5、方法別太長(zhǎng)
方法別太長(zhǎng)就是字面的意思。一旦代碼太長(zhǎng),給人的第一眼感覺(jué)就很復(fù)雜,讓人不想讀下去;同時(shí)方法太長(zhǎng)的代碼可能讀起來(lái)容易讓人摸不著頭腦,不知道哪一些代碼是同一個(gè)業(yè)務(wù)的功能。
我曾經(jīng)就遇到過(guò)一個(gè)方法寫(xiě)了 2000+行,各種 if else 判斷,我光理清代碼思路就用了很久,最終理清之后,就用策略模式給重構(gòu)了。
所以一旦方法過(guò)長(zhǎng),可以嘗試將相同業(yè)務(wù)功能的代碼單獨(dú)抽取一個(gè)方法,最后在主方法中調(diào)用即可。
6、抽取重復(fù)代碼
當(dāng)一份代碼重復(fù)出現(xiàn)在程序的多處地方,就會(huì)造成程序又臭又長(zhǎng),當(dāng)這份代碼的結(jié)構(gòu)要修改時(shí),每一處出現(xiàn)這份代碼的地方都得修改,導(dǎo)致程序的擴(kuò)展性很差。
所以一般遇到這種情況,可以抽取成一個(gè)工具類(lèi),還可以抽成一個(gè)公共的父類(lèi)。
7、多用 return
在有時(shí)我們平時(shí)寫(xiě)代碼的情況可能會(huì)出現(xiàn) if 條件套 if 的情況,當(dāng) if 條件過(guò)多的時(shí)候可能會(huì)出現(xiàn)如下情況:
if (條件1) {
if (條件2) {
if (條件3) {
if (條件4) {
if (條件5) {
System.out.println("三友的java日記");
}
}
}
}
}
面對(duì)這種情況,可以換種思路,使用 return 來(lái)優(yōu)化
if (!條件1) {
return;
}
if (!條件2) {
return;
}
if (!條件3) {
return;
}
if (!條件4) {
return;
}
if (!條件5) {
return;
}
System.out.println("三友的java日記");
這樣優(yōu)化就感覺(jué)看起來(lái)更加直觀
8、if 條件表達(dá)式不要太復(fù)雜
比如在如下代碼:
if (((StringUtils.isBlank(person.getName())
|| "三友的java日記".equals(person.getName()))
&& (person.getAge() != null && person.getAge() > 10))
&& "漢".equals(person.getNational())) {
// 處理邏輯
}
這段邏輯,這種條件表達(dá)式乍一看不知道是什么,仔細(xì)一看還是不知道是什么,這時(shí)就可以這么優(yōu)化
boolean sanyouOrBlank = StringUtils.isBlank(person.getName()) || "三友的java日記".equals(person.getName());
boolean ageGreaterThanTen = person.getAge() != null && person.getAge() > 10;
boolean isHanNational = "漢".equals(person.getNational());
if (sanyouOrBlank
&& ageGreaterThanTen
&& isHanNational) {
// 處理邏輯
}
此時(shí)就很容易看懂 if 的邏輯了
9、優(yōu)雅地參數(shù)校驗(yàn)
當(dāng)前端傳遞給后端參數(shù)的時(shí)候,通常需要對(duì)參數(shù)進(jìn)場(chǎng)檢驗(yàn),一般可能會(huì)這么寫(xiě)
@PostMapping
public void addPerson(@RequestBody AddPersonRequest addPersonRequest) {
if (StringUtils.isBlank(addPersonRequest.getName())) {
throw new BizException("人員姓名不能為空");
}
if (StringUtils.isBlank(addPersonRequest.getIdCardNo())) {
throw new BizException("身份證號(hào)不能為空");
}
// 處理新增邏輯
}
這種寫(xiě)雖然可以,但是當(dāng)字段的多的時(shí)候,光校驗(yàn)就占據(jù)了很長(zhǎng)的代碼,不夠優(yōu)雅。
針對(duì)參數(shù)校驗(yàn)這個(gè)問(wèn)題,有第三方庫(kù)已經(jīng)封裝好了,比如 hibernate-validator 框架,只需要拿來(lái)用即可。
所以就在實(shí)體類(lèi)上加 @NotBlank、@NotNull 注解來(lái)進(jìn)行校驗(yàn)
@Data
@ToString
private class AddPersonRequest {
@NotBlank(message = "人員姓名不能為空")
private String name;
@NotBlank(message = "身份證號(hào)不能為空")
private String idCardNo;
//忽略
}
此時(shí) Controller 接口就需要方法上就需要加上 @Valid 注解
@PostMapping
public void addPerson(@RequestBody @Valid AddPersonRequest addPersonRequest) {
// 處理新增邏輯
}
10、統(tǒng)一返回值
后端在設(shè)計(jì)接口的時(shí)候,需要統(tǒng)一返回值
{
"code":0,
"message":"成功",
"data":"返回?cái)?shù)據(jù)"
}
不僅是給前端參數(shù),也包括提供給第三方的接口等,這樣接口調(diào)用方法可以按照固定的格式解析代碼,不用進(jìn)行判斷。如果不一樣,相信我,前端半夜都一定會(huì)來(lái)找你。
Spring 中很多方法可以做到統(tǒng)一返回值,而不用每個(gè)方法都返回,比如基于 AOP,或者可以自定義 HandlerMethodReturnValueHandler 來(lái)實(shí)現(xiàn)統(tǒng)一返回值。
11、統(tǒng)一異常處理
當(dāng)你沒(méi)有統(tǒng)一異常處理的時(shí)候,那么所有的接口避免不了 try catch 操作。
@GetMapping("/{id}")
public Result<T> selectPerson(@PathVariable("id") Long personId) {
try {
PersonVO vo = personService.selectById(personId);
return Result.success(vo);
} catch (Exception e) {
//打印日志
return Result.error("系統(tǒng)異常");
}
}
每個(gè)接口都得這么玩,那不得滿屏的 try catch。
所以可以基于 Spring 提供的統(tǒng)一異常處理機(jī)制來(lái)完成。
12、盡量不傳遞 null 值
這個(gè)很好理解,不傳 null 值可以避免方法不支持為 null 入?yún)r(shí)產(chǎn)生的空指針問(wèn)題。
當(dāng)然為了更好的表明該方法是不是可以傳 null 值,可以通過(guò)@NonNull 和@Nullable 注解來(lái)標(biāo)記。@NonNull 就表示不能傳 null 值,@Nullable 就是可以傳 null 值。
//示例1
public void updatePerson(@Nullable Person person) {
if (person == null) {
return;
}
personService.updateById(person);
}
//示例2
public void updatePerson(@NonNull Person person) {
personService.updateById(person);
}
13、盡量不返回 null 值
盡量不返回 null 值是為了減少調(diào)用者對(duì)返回值的為 null 判斷,如果無(wú)法避免返回 null 值,可以通過(guò)返回 Optional 來(lái)代替 null 值。
public Optional<Person> getPersonById(Long personId) {
return Optional.ofNullable(personService.selectById(personId));
}
如果不想這么寫(xiě),也可以通過(guò)@NonNull 和@Nullable 表示方法會(huì)不會(huì)返回 null 值。
14、日志打印規(guī)范
好的日志打印能幫助我們快速定位問(wèn)題
好的日志應(yīng)該遵循以下幾點(diǎn):
可搜索性,要有明確的關(guān)鍵字信息 異常日志需要打印出堆棧信息 合適的日志級(jí)別,比如異常使用 error,正常使用 info 日志內(nèi)容太大不打印,比如有時(shí)需要將圖片轉(zhuǎn)成 Base64,那么這個(gè) Base64 就可以不用打印
15、統(tǒng)一類(lèi)庫(kù)
在一個(gè)項(xiàng)目中,可能會(huì)由于引入的依賴(lài)不同導(dǎo)致引入了很多相似功能的類(lèi)庫(kù),比如常見(jiàn)的 json 類(lèi)庫(kù),又或者是一些常用的工具類(lèi),當(dāng)遇到這種情況下,應(yīng)當(dāng)規(guī)范在項(xiàng)目中到底應(yīng)該使用什么類(lèi)庫(kù),而不是一會(huì)用 Fastjson,一會(huì)使用 Gson。
16、盡量使用工具類(lèi)
比如在對(duì)集合判空的時(shí)候,可以這么寫(xiě)
public void updatePersons(List<Person> persons) {
if (persons != null && persons.size() > 0) {
}
}
但是一般不推薦這么寫(xiě),可以通過(guò)一些判斷的工具類(lèi)來(lái)寫(xiě)
public void updatePersons(List<Person> persons) {
if (!CollectionUtils.isEmpty(persons)) {
}
}
不僅集合,比如字符串的判斷等等,就使用工具類(lèi),不要手動(dòng)判斷。
17、盡量不要重復(fù)造輪子
就拿格式化日期來(lái)來(lái)說(shuō),我們一般封裝成一個(gè)工具類(lèi)來(lái)調(diào)用,比如如下代碼
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDateTime(Date date) {
return DATE_TIME_FORMAT.format(date);
}
這段代碼看似沒(méi)啥問(wèn)題,但是卻忽略了 SimpleDateFormat 是個(gè)線程不安全的類(lèi),所以這就會(huì)引起坑。
一般對(duì)于這種已經(jīng)有開(kāi)源的項(xiàng)目并且已經(jīng)做得很好的時(shí)候,比如 Hutool,就可以把輪子直接拿過(guò)來(lái)用了。
18、類(lèi)和方法單一職責(zé)
單一職責(zé)原則是設(shè)計(jì)模式的七大設(shè)計(jì)原則之一,它的核心意思就是字面的意思,一個(gè)類(lèi)或者一個(gè)方法只做單一的功能。
就拿 Nacos 來(lái)說(shuō),在 Nacos1.x 的版本中,有這么一個(gè)接口 HttpAgent

這個(gè)類(lèi)只干了一件事,那就是封裝 http 請(qǐng)求參數(shù),向 Nacos 服務(wù)端發(fā)送請(qǐng)求,接收響應(yīng),這其實(shí)就是單一職責(zé)原則的體現(xiàn)。
當(dāng)其它的地方需要向 Nacos 服務(wù)端發(fā)送請(qǐng)求時(shí),只需要通過(guò)這個(gè)接口的實(shí)現(xiàn),傳入?yún)?shù)就可以發(fā)送請(qǐng)求了,而不需要關(guān)心如何攜帶服務(wù)端鑒權(quán)參數(shù)、http 請(qǐng)求參數(shù)如何組裝等問(wèn)題。
19、盡量使用聚合/組合代替繼承
繼承的弊端:
靈活性低。java 語(yǔ)言是單繼承的,無(wú)法同時(shí)繼承很多類(lèi),并且繼承容易導(dǎo)致代碼層次太深,不易于維護(hù) 耦合性高。一旦父類(lèi)的代碼修改,可能會(huì)影響到子類(lèi)的行為
所以一般推薦使用聚合/組合代替繼承。
聚合/組合的意思就是通過(guò)成員變量的方式來(lái)使用類(lèi)。
比如說(shuō),OrderService 需要使用 UserService,可以注入一個(gè) UserService 而非通過(guò)繼承 UserService。
聚合和組合的區(qū)別就是,組合是當(dāng)對(duì)象一創(chuàng)建的時(shí)候,就直接給屬性賦值,而聚合的方式可以通過(guò) set 方式來(lái)設(shè)置。
組合:
public class OrderService {
private UserService userService = new UserService();
}
聚合:
public class OrderService {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
}
20、使用設(shè)計(jì)模式優(yōu)化代碼
在平時(shí)開(kāi)發(fā)中,使用設(shè)計(jì)模式可以增加代碼的擴(kuò)展性。
比如說(shuō),當(dāng)你需要做一個(gè)可以根據(jù)不同的平臺(tái)做不同消息推送的功能時(shí),就可以使用策略模式的方式來(lái)優(yōu)化。
設(shè)計(jì)一個(gè)接口:
public interface MessageNotifier {
/**
* 是否支持改類(lèi)型的通知的方式
*
* @param type 0:短信 1:app
* @return
*/
boolean support(int type);
/**
* 通知
*
* @param user
* @param content
*/
void notify(User user, String content);
}
短信通知實(shí)現(xiàn):
@Component
public class SMSMessageNotifier implements MessageNotifier {
@Override
public boolean support(int type) {
return type == 0;
}
@Override
public void notify(User user, String content) {
//調(diào)用短信通知的api發(fā)送短信
}
}
app 通知實(shí)現(xiàn):
public class AppMessageNotifier implements MessageNotifier {
@Override
public boolean support(int type) {
return type == 1;
}
@Override
public void notify(User user, String content) {
//調(diào)用通知app通知的api
}
}
最后提供一個(gè)方法,當(dāng)需要進(jìn)行消息通知時(shí),調(diào)用 notifyMessage,傳入相應(yīng)的參數(shù)就行。
@Resource
private List<MessageNotifier> messageNotifiers;
public void notifyMessage(User user, String content, int notifyType) {
for (MessageNotifier messageNotifier : messageNotifiers) {
if (messageNotifier.support(notifyType)) {
messageNotifier.notify(user, content);
}
}
}
假設(shè)此時(shí)需要支持通過(guò)郵件通知,只需要有對(duì)應(yīng)實(shí)現(xiàn)就行。
21、不濫用設(shè)計(jì)模式
用好設(shè)計(jì)模式可以增加代碼的擴(kuò)展性,但是濫用設(shè)計(jì)模式確是不可取的。
public void printPerson(Person person) {
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(person.getName())) {
sb.append("姓名:").append(person.getName());
}
if (StringUtils.isNotBlank(person.getIdCardNo())) {
sb.append("身份證號(hào):").append(person.getIdCardNo());
}
// 省略
System.out.println(sb.toString());
}
比如上面打印 Person 信息的代碼,用 if 判斷就能夠做到效果,你說(shuō)我要不用責(zé)任鏈或者什么設(shè)計(jì)模式來(lái)優(yōu)化一下吧,沒(méi)必要。
22、面向接口編程
在一些可替換的場(chǎng)景中,應(yīng)該引用父類(lèi)或者抽象,而非實(shí)現(xiàn)。
舉個(gè)例子,在實(shí)際項(xiàng)目中可能需要對(duì)一些圖片進(jìn)行存儲(chǔ),但是存儲(chǔ)的方式很多,比如可以選擇阿里云的 OSS,又或者是七牛云,存儲(chǔ)服務(wù)器等等。所以對(duì)于存儲(chǔ)圖片這個(gè)功能來(lái)說(shuō),這些具體的實(shí)現(xiàn)是可以相互替換的。
所以在項(xiàng)目中,我們不應(yīng)當(dāng)在代碼中耦合一個(gè)具體的實(shí)現(xiàn),而是可以提供一個(gè)存儲(chǔ)接口
public interface FileStorage {
String store(String fileName, byte[] bytes);
}
如果選擇了阿里云 OSS 作為存儲(chǔ)服務(wù)器,那么就可以基于 OSS 實(shí)現(xiàn)一個(gè) FileStorage,在項(xiàng)目中哪里需要存儲(chǔ)的時(shí)候,只要實(shí)現(xiàn)注入這個(gè)接口就可以了。
@Autowired
private FileStorage fileStorage;
假設(shè)用了一段時(shí)間之后,發(fā)現(xiàn)阿里云的 OSS 比較貴,此時(shí)想換成七牛云的,那么此時(shí)只需要基于七牛云的接口實(shí)現(xiàn) FileStorage 接口,然后注入到 IOC,那么原有代碼用到 FileStorage 根本不需要?jiǎng)?,?shí)現(xiàn)輕松的替換。
23、經(jīng)常重構(gòu)舊的代碼
隨著時(shí)間的推移,業(yè)務(wù)的增長(zhǎng),有的代碼可能不再適用,或者有了更好的設(shè)計(jì)方式,那么可以及時(shí)的重構(gòu)業(yè)務(wù)代碼。
就拿上面的消息通知為例,在業(yè)務(wù)剛開(kāi)始的時(shí)候可能只支持短信通知,于是在代碼中就直接耦合了短信通知的代碼。但是隨著業(yè)務(wù)的增長(zhǎng),逐漸需要支持 app、郵件之類(lèi)的通知,那么此時(shí)就可以重構(gòu)以前的代碼,抽出一個(gè)策略接口,進(jìn)行代碼優(yōu)化。
24、null 值判斷
空指針是代碼開(kāi)發(fā)中的一個(gè)難題,作為程序員的基本修改,應(yīng)該要防止空指針。
可能產(chǎn)生空指針的原因:
數(shù)據(jù)返回對(duì)象為 null 自動(dòng)拆箱導(dǎo)致空指針 rpc 調(diào)用返回的對(duì)象可能為空格
所以在需要這些的時(shí)候,需要強(qiáng)制判斷是否為 null。前面也提到可以使用 Optional 來(lái)優(yōu)雅地進(jìn)行 null 值判斷。
25、pojo 類(lèi)重寫(xiě) toString 方法
pojo 一般內(nèi)部都有很多屬性,重寫(xiě) toString 方法可以方便在打印或者測(cè)試的時(shí)候查看內(nèi)部的屬性。
26、魔法值用常量表示
public void sayHello(String province) {
if ("廣東省".equals(province)) {
System.out.println("靚仔~~");
} else {
System.out.println("帥哥~~");
}
}
代碼里,廣東省就是一個(gè)魔法值,那么就可以將用一個(gè)常量來(lái)保存
private static final String GUANG_DONG_PROVINCE = "廣東省";
public void sayHello(String province) {
if (GUANG_DONG_PROVINCE.equals(province)) {
System.out.println("靚仔~~");
} else {
System.out.println("帥哥~~");
}
}
27、資源釋放寫(xiě)到 finally
比如在使用一個(gè) api 類(lèi)鎖或者進(jìn)行 IO 操作的時(shí)候,需要主動(dòng)寫(xiě)代碼需釋放資源,為了能夠保證資源能夠被真正釋放,那么就需要在 finally 中寫(xiě)代碼保證資源釋放。

如圖所示,就是 CopyOnWriteArrayList 的 add 方法的實(shí)現(xiàn),最終是在 finally 中進(jìn)行鎖的釋放。
28、使用線程池代替手動(dòng)創(chuàng)建線程
使用線程池還有以下好處:
降低資源消耗。通過(guò)重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷(xiāo)毀造成的消耗。 提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。 提高線程的可管理性。線程是稀缺資源,如果無(wú)限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng) 的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
所以為了達(dá)到更好的利用資源,提高響應(yīng)速度,就可以使用線程池的方式來(lái)代替手動(dòng)創(chuàng)建線程。
如果對(duì)線程池不清楚的同學(xué),可以看一下這篇文章: 7000 字+24 張圖帶你徹底弄懂線程池
29、線程設(shè)置名稱(chēng)
在日志打印的時(shí)候,日志是可以把線程的名字給打印出來(lái)。

如上圖,日志打印出來(lái)的就是 tom 貓的線程。
所以,設(shè)置線程的名稱(chēng)可以幫助我們更好的知道代碼是通過(guò)哪個(gè)線程執(zhí)行的,更容易排查問(wèn)題。
30、涉及線程間可見(jiàn)性加 volatile
在 RocketMQ 源碼中有這么一段代碼

在消費(fèi)者在從服務(wù)端拉取消息的時(shí)候,會(huì)單獨(dú)開(kāi)一個(gè)線程,執(zhí)行 while 循環(huán),只要 stopped 狀態(tài)一直為 false,那么就會(huì)一直循環(huán)下去,線程就一直會(huì)運(yùn)行下去,拉取消息。
當(dāng)消費(fèi)者客戶端關(guān)閉的時(shí)候,就會(huì)將 stopped 狀態(tài)設(shè)置為 true,告訴拉取消息的線程需要停止了。但是由于并發(fā)編程中存在可見(jiàn)性的問(wèn)題,所以雖然客戶端關(guān)閉線程將 stopped 狀態(tài)設(shè)置為 true,但是拉取消息的線程可能看不見(jiàn),不能及時(shí)感知到數(shù)據(jù)的修改,還是認(rèn)為 stopped 狀態(tài)設(shè)置為 false,那么就還會(huì)運(yùn)行下去。
針對(duì)這種可見(jiàn)性的問(wèn)題,java 提供了一個(gè) volatile 關(guān)鍵字來(lái)保證線程間的可見(jiàn)性。

所以,源碼中就加了 volatile 關(guān)鍵字。
加了 volatile 關(guān)鍵字之后,一旦客戶端的線程將 stopped 狀態(tài)設(shè)置為 true 時(shí)候,拉取消息的線程就能立馬知道 stopped 已經(jīng)是 false 了,那么再次執(zhí)行 while 條件判斷的時(shí)候,就不成立,線程就運(yùn)行結(jié)束了,然后退出。
31、考慮線程安全問(wèn)題
在平時(shí)開(kāi)發(fā)中,有時(shí)需要考慮并發(fā)安全的問(wèn)題。
舉個(gè)例子來(lái)說(shuō),一般在調(diào)用第三方接口的時(shí)候,可能會(huì)有一個(gè)鑒權(quán)的機(jī)制,一般會(huì)攜帶一個(gè)請(qǐng)求頭 token 參數(shù)過(guò)去,而 token 也是調(diào)用第三方接口返回的,一般這種 token 都會(huì)有個(gè)過(guò)期時(shí)間,比如 24 小時(shí)。
我們一般會(huì)將 token 緩存到 Redis 中,設(shè)置一個(gè)過(guò)期時(shí)間。向第三方發(fā)送請(qǐng)求時(shí),會(huì)直接從緩存中查找,但是當(dāng)從 Redis 中獲取不到 token 的時(shí)候,我們都會(huì)重新請(qǐng)求 token 接口,獲取 token,然后再設(shè)置到緩存中。
整個(gè)過(guò)程看起來(lái)是沒(méi)什么問(wèn)題,但是實(shí)則隱藏線程安全問(wèn)題。
假設(shè)當(dāng)出現(xiàn)并發(fā)的時(shí)候,同時(shí)來(lái)兩個(gè)線程 AB 從緩存查找,發(fā)現(xiàn)沒(méi)有,那么 AB 此時(shí)就會(huì)同時(shí)調(diào)用 token 獲取接口。假設(shè) A 先獲取到 token,B 后獲取到 token,但是由于 CPU 調(diào)度問(wèn)題,線程 B 雖然后獲取到 token,但是先往 Redis 存數(shù)據(jù),而線程 A 后存,覆蓋了 B 請(qǐng)求的 token。
這下就會(huì)出現(xiàn)大問(wèn)題,最新的 token 被覆蓋了,那么之后一定時(shí)間內(nèi) token 都是無(wú)效的,接口就請(qǐng)求不通。
針對(duì)這種問(wèn)題,可以使用 double check 機(jī)制來(lái)優(yōu)化獲取 token 的問(wèn)題。
所以,在實(shí)際中,需要多考慮考慮業(yè)務(wù)是否有線程安全問(wèn)題,有集合讀寫(xiě)安全問(wèn)題,那么就用線程安全的集合,業(yè)務(wù)有安全的問(wèn)題,那么就可以通過(guò)加鎖的手段來(lái)解決。
32、慎用異步
雖然在使用多線程可以幫助我們提高接口的響應(yīng)速度,但是也會(huì)帶來(lái)很多問(wèn)題。
事務(wù)問(wèn)題
一旦使用了異步,就會(huì)導(dǎo)致兩個(gè)線程不是同一個(gè)事務(wù)的,導(dǎo)致異常之后無(wú)法正?;貪L數(shù)據(jù)。
cpu 負(fù)載過(guò)高
之前有個(gè)小伙伴遇到需要同時(shí)處理幾萬(wàn)調(diào)數(shù)據(jù)的需求,每條數(shù)據(jù)都需要調(diào)用很多次接口,為了達(dá)到老板期望的時(shí)間要求,使用了多線程跑,開(kāi)了很多線程,此時(shí)會(huì)發(fā)現(xiàn)系統(tǒng)的 cpu 會(huì)飆升
意想不到的異常
還是上面的提到的例子,在測(cè)試的時(shí)候就發(fā)現(xiàn),由于并發(fā)量激增,在請(qǐng)求第三方接口的時(shí)候,返回了很多錯(cuò)誤信息,導(dǎo)致有的數(shù)據(jù)沒(méi)有處理成功。
雖然說(shuō)慎用異步,但不代表不用,如果可以保證事務(wù)的問(wèn)題,或是 CPU 負(fù)載不會(huì)高的話,那么還是可以使用的。
33、減小鎖的范圍
減小鎖的范圍就是給需要加鎖的代碼加鎖,不需要加鎖的代碼不要加鎖。這樣就能減少加鎖的時(shí)間,從而可以較少鎖互斥的時(shí)間,提高效率。

比如 CopyOnWriteArrayList 的 addAll 方法的實(shí)現(xiàn),lock.lock(); 代碼完全可以放到代碼的第一行,但是作者并沒(méi)有,因?yàn)榍懊媾袛嗟拇a不會(huì)有線程安全的問(wèn)題,不放到加鎖代碼中可以減少鎖搶占和占有的時(shí)間。
34、有類(lèi)型區(qū)分時(shí)定義好枚舉
比如在項(xiàng)目中不同的類(lèi)型的業(yè)務(wù)可能需要上傳各種各樣的附件,此時(shí)就可以定義好不同的一個(gè)附件的枚舉,來(lái)區(qū)分不同業(yè)務(wù)的附件。
不要在代碼中直接寫(xiě)死,不定義枚舉,代碼閱讀起來(lái)非常困難,直接看到數(shù)字都是懵逼的。。
35、遠(yuǎn)程接口調(diào)用設(shè)置超時(shí)時(shí)間
比如在進(jìn)行微服務(wù)之間進(jìn)行 rpc 調(diào)用的時(shí)候,又或者在調(diào)用第三方提供的接口的時(shí)候,需要設(shè)置超時(shí)時(shí)間,防止因?yàn)楦鞣N原因,導(dǎo)致線程”卡死“在那。
我以前就遇到過(guò)線上就遇到過(guò)這種問(wèn)題。當(dāng)時(shí)的業(yè)務(wù)是訂閱 kafka 的消息,然后向第三方上傳數(shù)據(jù)。在某個(gè)周末,突然就接到電話,說(shuō)數(shù)據(jù)無(wú)法上傳了,通過(guò)排查線上的服務(wù)器才發(fā)現(xiàn)所有的線程都線程”卡死“了,最后定位到代碼才發(fā)現(xiàn)原來(lái)是沒(méi)有設(shè)置超時(shí)時(shí)間。
36、集合使用應(yīng)當(dāng)指明初始化大小
比如在寫(xiě)代碼的時(shí)候,經(jīng)常會(huì)用到 List、Map 來(lái)臨時(shí)存儲(chǔ)數(shù)據(jù),其中最常用的就是 ArrayList 和 HashMap。但是用不好可能也會(huì)導(dǎo)致性能的問(wèn)題。
比如說(shuō),在 ArrayList 中,底層是基于數(shù)組來(lái)存儲(chǔ)的,數(shù)組是一旦確定大小是無(wú)法再改變?nèi)萘康?。但不斷的?ArrayList 中存儲(chǔ)數(shù)據(jù)的時(shí)候,總有那么一刻會(huì)導(dǎo)致數(shù)組的容量滿了,無(wú)法再存儲(chǔ)其它元素,此時(shí)就需要對(duì)數(shù)組擴(kuò)容。所謂的擴(kuò)容就是新創(chuàng)建一個(gè)容量是原來(lái) 1.5 倍的數(shù)組,將原有的數(shù)據(jù)給拷貝到新的數(shù)組上,然后用新的數(shù)組替代原來(lái)的數(shù)組。
在擴(kuò)容的過(guò)程中,由于涉及到數(shù)組的拷貝,就會(huì)導(dǎo)致性能消耗;同時(shí) HashMap 也會(huì)由于擴(kuò)容的問(wèn)題,消耗性能。所以在使用這類(lèi)集合時(shí)可以在構(gòu)造的時(shí)候指定集合的容量大小。
37、盡量不要使用 BeanUtils 來(lái)拷貝屬性
在開(kāi)發(fā)中經(jīng)常需要對(duì) JavaBean 進(jìn)行轉(zhuǎn)換,但是又不想一個(gè)一個(gè)手動(dòng) set,比較麻煩,所以一般會(huì)使用屬性拷貝的一些工具,比如說(shuō) Spring 提供的 BeanUtils 來(lái)拷貝。不得不說(shuō),使用 BeanUtils 來(lái)拷貝屬性是真的舒服,使用一行代碼可以代替幾行甚至十幾行代碼,我也喜歡用。
但是喜歡歸喜歡,但是會(huì)帶來(lái)性能問(wèn)題,因?yàn)榈讓邮峭ㄟ^(guò)反射來(lái)的拷貝屬性的,所以盡量不要用 BeanUtils 來(lái)拷貝屬性。
比如你可以裝個(gè) JavaBean 轉(zhuǎn)換的插件,幫你自動(dòng)生成轉(zhuǎn)換代碼;又或者可以使用性能更高的 MapStruct 來(lái)進(jìn)行 JavaBean 轉(zhuǎn)換,MapStruct 底層是通過(guò)調(diào)用(settter/getter)來(lái)實(shí)現(xiàn)的,而不是反射來(lái)快速執(zhí)行。
38、使用 StringBuilder 進(jìn)行字符串拼接
如下代碼:
String str1 = "123";
String str2 = "456";
String str3 = "789";
String str4 = str1 + str2 + str3;
使用 + 拼接字符串的時(shí)候,會(huì)創(chuàng)建一個(gè) StringBuilder,然后將要拼接的字符串追加到 StringBuilder,再 toString,這樣如果多次拼接就會(huì)執(zhí)行很多次的創(chuàng)建 StringBuilder,z 執(zhí)行 toString 的操作。
所以可以手動(dòng)通過(guò) StringBuilder 拼接,這樣只會(huì)創(chuàng)建一次 StringBuilder,效率更高。
StringBuilder sb = new StringBuilder();
String str = sb.append("123").append("456").append("789").toString();
39、@Transactional 應(yīng)指定回滾的異常類(lèi)型
平時(shí)在寫(xiě)代碼的時(shí)候需要通過(guò) rollbackFor 顯示指定需要對(duì)什么異常回滾,原因在這:

默認(rèn)是只能回滾 RuntimeException 和 Error 異常,所以需要手動(dòng)指定,比如指定成 Expection 等。
40、謹(jǐn)慎方法內(nèi)部調(diào)用動(dòng)態(tài)代理的方法
如下事務(wù)代碼
@Service
public class PersonService {
public void update(Person person) {
// 處理
updatePerson(person);
}
@Transactional(rollbackFor = Exception.class)
public void updatePerson(Person person) {
// 處理
}
}
update 調(diào)用了加了@Transactional 注解的 updatePerson 方法,那么此時(shí) updatePerson 的事務(wù)就是失效。
其實(shí)失效的原因不是事務(wù)的鍋,是由 AOP 機(jī)制決定的,因?yàn)槭聞?wù)是基于 AOP 實(shí)現(xiàn)的。AOP 是基于對(duì)象的代理,當(dāng)內(nèi)部方法調(diào)用時(shí),走的不是動(dòng)態(tài)代理對(duì)象的方法,而是原有對(duì)象的方法調(diào)用,如此就走不到動(dòng)態(tài)代理的代碼,就會(huì)失效了。
如果實(shí)在需要讓動(dòng)態(tài)代理生效,可以注入自己的代理對(duì)象
@Service
public class PersonService {
@Autowired
private PersonService personService;
public void update(Person person) {
// 處理
personService.updatePerson(person);
}
@Transactional(rollbackFor = Exception.class)
public void updatePerson(Person person) {
// 處理
}
}
41、需要什么字段 select 什么字段
查詢?nèi)侄斡幸韵聨c(diǎn)壞處:
增加不必要的字段的網(wǎng)絡(luò)傳輸
比如有些文本的字段,存儲(chǔ)的數(shù)據(jù)非常長(zhǎng),但是本次業(yè)務(wù)使用不到,但是如果查了就會(huì)把這個(gè)數(shù)據(jù)返回給客戶端,增加了網(wǎng)絡(luò)傳輸?shù)呢?fù)擔(dān)
會(huì)導(dǎo)致無(wú)法使用到覆蓋索引
比如說(shuō),現(xiàn)在有身份證號(hào)和姓名做了聯(lián)合索引,現(xiàn)在只需要根據(jù)身份證號(hào)查詢姓名,如果直接 select name 的話,那么在遍歷索引的時(shí)候,發(fā)現(xiàn)要查詢的字段在索引中已經(jīng)存在,那么此時(shí)就會(huì)直接從索引中將 name 字段的數(shù)據(jù)查出來(lái),返回,而不會(huì)繼續(xù)去查找聚簇索引,減少回表的操作。
所以建議是需要使用什么字段查詢什么字段。比如 mp 也支持在構(gòu)建查詢條件的時(shí)候,查詢某個(gè)具體的字段。
Wrappers.query().select("name");
42、不循環(huán)調(diào)用數(shù)據(jù)庫(kù)
不要在循環(huán)中訪問(wèn)數(shù)據(jù)庫(kù),這樣會(huì)嚴(yán)重影響數(shù)據(jù)庫(kù)性能。
比如需要查詢一批人員的信息,人員的信息存在基本信息表和擴(kuò)展表中,錯(cuò)誤的代碼如下:
public List<PersonVO> selectPersons(List<Long> personIds) {
List<PersonVO> persons = new ArrayList<>(personIds.size());
List<Person> personList = personMapper.selectByIds(personIds);
for (Person person : personList) {
PersonVO vo = new PersonVO();
PersonExt personExt = personExtMapper.selectById(person.getId());
// 組裝數(shù)據(jù)
persons.add(vo);
}
return persons;
}
遍歷每個(gè)人員的基本信息,去數(shù)據(jù)庫(kù)查找。
正確的方法應(yīng)該先批量查出來(lái),然后轉(zhuǎn)成 map:
public List<PersonVO> selectPersons(List<Long> personIds) {
List<PersonVO> persons = new ArrayList<>(personIds.size());
List<Person> personList = personMapper.selectByIds(personIds);
//批量查詢,轉(zhuǎn)換成Map
List<PersonExt> personExtList = personExtMapper.selectByIds(person.getId());
Map<String, PersonExt> personExtMap = personExtList.stream().collect(Collectors.toMap(PersonExt::getPersonId, Function.identity()));
for (Person person : personList) {
PersonVO vo = new PersonVO();
//直接從Map中查找
PersonExt personExt = personExtMap.get(person.getId());
// 組裝數(shù)據(jù)
persons.add(vo);
}
return persons;
}
43、用業(yè)務(wù)代碼代替多表 join
如上面代碼所示,原本也可以將兩張表根據(jù)人員的 id 進(jìn)行關(guān)聯(lián)查詢。但是不推薦這么,阿里也禁止多表 join 的操作

而之所以會(huì)禁用,是因?yàn)?join 的效率比較低。
MySQL 是使用了嵌套循環(huán)的方式來(lái)實(shí)現(xiàn)關(guān)聯(lián)查詢的,也就是 for 循環(huán)會(huì)套 for 循環(huán)的意思。用第一張表做外循環(huán),第二張表做內(nèi)循環(huán),外循環(huán)的每一條記錄跟內(nèi)循環(huán)中的記錄作比較,符合條件的就輸出,這種效率肯定低。
44、裝上阿里代碼檢查插件
我們平時(shí)寫(xiě)代碼由于各種因?yàn)椋热缡裁搭I(lǐng)導(dǎo)啊,項(xiàng)目經(jīng)理啊,會(huì)一直催進(jìn)度,導(dǎo)致寫(xiě)代碼都來(lái)不及思考,怎么快怎么來(lái),cv 大法上線,雖然有心想寫(xiě)好代碼,但是手確不聽(tīng)使喚。所以我建議裝一個(gè)阿里的代碼規(guī)范插件,如果有代碼不規(guī)范,會(huì)有提醒,這樣就可以知道哪些是可以優(yōu)化的了。

如果你有強(qiáng)迫癥,相信我,裝了這款插件,你的代碼會(huì)寫(xiě)的很漂亮。
45、及時(shí)跟同事溝通
寫(xiě)代碼的時(shí)候不能閉門(mén)造車(chē),及時(shí)跟同事溝通,比如剛進(jìn)入一個(gè)新的項(xiàng)目的,對(duì)項(xiàng)目工程不熟悉,一些技術(shù)方案不了解,如果上來(lái)就直接寫(xiě)代碼,很有可能就會(huì)踩坑。
·········· END ··············
?? 歡迎準(zhǔn)備 Java 面試以及學(xué)習(xí) Java 的同學(xué)加入我的知識(shí)星球,干貨很多!收費(fèi)雖然是白菜價(jià),但星球里的內(nèi)容或許比你參加上萬(wàn)的培訓(xùn)班質(zhì)量還要高。
?? 《Java 面試指北》持續(xù)更新完善中!這是一份教你如何更高效地準(zhǔn)備面試的小冊(cè),涵蓋常見(jiàn)八股文(系統(tǒng)設(shè)計(jì)、常見(jiàn)框架、分布式、高并發(fā) ......)、優(yōu)質(zhì)面經(jīng)等內(nèi)容。
推薦閱讀 :
曾經(jīng)真是網(wǎng)癮少年 害,畢業(yè)三年了! 一個(gè)普通程序員的周末 做公眾號(hào)這一年的經(jīng)歷和一件“大事” 簡(jiǎn)單聊聊我的 Java 后端開(kāi)發(fā)求職之路
?? 如果本文對(duì)你有幫助的話,歡迎 點(diǎn)贊&在看&分享 ,這對(duì)我繼續(xù)分享&創(chuàng)作優(yōu)質(zhì)文章非常重要。非常感謝!
