有了這45個小技巧,再也不怕女朋友程式碼寫得爛了!!

2022-10-24 15:00:12

大家好,我是三友~~

不知道大家有沒有經歷過維護一個已經離職的人的程式碼的痛苦,一個方法寫老長,還有很多的if else ,根本無法閱讀,更不知道程式碼背後的含義,最重要的是沒有人可以問,此時只能心裡默默地問候這個留坑的兄弟。。

其實造成這些原因的很大一部分原因是由於程式碼規範的問題,如果寫的規範,註釋好,其實很多問題也就解決了。所以本文我就從程式碼的編寫規範,格式的優化,設計原則和一些常見的程式碼優化的技巧等方面總結了了45個小技巧分享給大家,如果不足,歡迎指正。

1、規範命名

命名是寫程式碼中最頻繁的操作,比如類、屬性、方法、引數等。好的名字應當能遵循以下幾點:

見名知意

比如需要定義一個變數需要來計數

int i = 0;

名稱 i 沒有任何的實際意義,沒有體現出數量的意思,所以我們應當指明數量的名稱

int count = 0;
能夠讀的出來

如下程式碼:

private String sfzh;
private String dhhm;

這些變數的名稱,根本讀不出來,更別說實際意義了。

所以我們可以使用正確的可以讀出來的英文來命名

private String idCardNo;
private String phone;

2、規範程式碼格式

好的程式碼格式能夠讓人感覺看起來程式碼更加舒適。

好的程式碼格式應當遵守以下幾點:

  • 合適的空格
  • 程式碼對齊,比如大括號要對齊
  • 及時換行,一行不要寫太多程式碼

好在現在開發工具支援一鍵格式化,可以幫助美化程式碼格式。

3、寫好程式碼註釋

在《程式碼簡潔之道》這本書中作者提到了一個觀點,註釋的恰當用法是用來彌補我們在用程式碼錶達意圖時的失敗。換句話說,當無法通過讀程式碼來了解程式碼所表達的意思的時候,就需要用註釋來說明。

作者之所以這麼說,是因為作者覺得隨著時間的推移,程式碼可能會變動,如果不及時更新註釋,那麼註釋就容易產生誤導,偏離程式碼的實際意義。而不及時更新註釋的原因是,程式設計師不喜歡寫註釋。(作者很懂啊)

但是這不意味著可以不寫註釋,當通過程式碼如果無法表達意思的時候,就需要註釋,比如如下程式碼

for (Integer id : ids) {
    if (id == 0) {
        continue;
    }
    //做其他事
}

為什麼 id == 0 需要跳過,程式碼是無法看出來了,就需要註釋了。

好的註釋應當滿足一下幾點:

  • 解釋程式碼的意圖,說明為什麼這麼寫,用來做什麼
  • 對引數和返回值註釋,入參代表什麼,出參代表什麼
  • 有警示作用,比如說入參不能為空,或者程式碼是不是有坑
  • 當程式碼還未完成時可以使用 todo 註釋來註釋

4、try catch 內部程式碼抽成一個方法

try catch程式碼有時會干擾我們閱讀核心的程式碼邏輯,這時就可以把try catch內部主邏輯抽離成一個單獨的方法

如下圖是Eureka伺服器端原始碼中服務下線的實現中的一段程式碼

整個方法非常長,try中程式碼是真正的服務下線的程式碼實現,finally可以保證讀鎖最終一定可以釋放。

所以這段程式碼其實就可以對核心的邏輯進行抽取。

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、方法別太長

方法別太長就是字面的意思。一旦程式碼太長,給人的第一眼感覺就很複雜,讓人不想讀下去;同時方法太長的程式碼可能讀起來容易讓人摸不著頭腦,不知道哪一些程式碼是同一個業務的功能。

我曾經就遇到過一個方法寫了2000+行,各種if else判斷,我光理清程式碼思路就用了很久,最終理清之後,就用策略模式給重構了。

所以一旦方法過長,可以嘗試將相同業務功能的程式碼單獨抽取一個方法,最後在主方法中呼叫即可。

6、抽取重複程式碼

當一份程式碼重複出現在程式的多處地方,就會造成程式又臭又長,當這份程式碼的結構要修改時,每一處出現這份程式碼的地方都得修改,導致程式的擴充套件性很差。

所以一般遇到這種情況,可以抽取成一個工具類,還可以抽成一個公共的父類別。

7、多用return

在有時我們平時寫程式碼的情況可能會出現if條件套if的情況,當if條件過多的時候可能會出現如下情況:

if (條件1) {
    if (條件2) {
        if (條件3) {
            if (條件4) {
                if (條件5) {
                    System.out.println("三友的java日記");
                }
            }
        }
    }
}

面對這種情況,可以換種思路,使用return來優化

if (!條件1) {
    return;
}
if (!條件2) {
    return;
}
if (!條件3) {
    return;
}
if (!條件4) {
    return;
}
if (!條件5) {
    return;
}

System.out.println("三友的java日記");

這樣優化就感覺看起來更加直觀

8、if條件表示式不要太複雜

比如在如下程式碼:

if (((StringUtils.isBlank(person.getName())
        || "三友的java日記".equals(person.getName()))
        && (person.getAge() != null && person.getAge() > 10))
        && "漢".equals(person.getNational())) {
    // 處理邏輯
}

這段邏輯,這種條件表示式乍一看不知道是什麼,仔細一看還是不知道是什麼,這時就可以這麼優化

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) {
    // 處理邏輯
}

此時就很容易看懂if的邏輯了

9、優雅地引數校驗

當前端傳遞給後端引數的時候,通常需要對引數進場檢驗,一般可能會這麼寫

@PostMapping
public void addPerson(@RequestBody AddPersonRequest addPersonRequest) {
    if (StringUtils.isBlank(addPersonRequest.getName())) {
        throw new BizException("人員姓名不能為空");
    }

    if (StringUtils.isBlank(addPersonRequest.getIdCardNo())) {
        throw new BizException("身份證號不能為空");
    }

    // 處理新增邏輯
}

這種寫雖然可以,但是當欄位的多的時候,光校驗就佔據了很長的程式碼,不夠優雅。

針對引數校驗這個問題,有第三方庫已經封裝好了,比如hibernate-validator框架,只需要拿來用即可。

所以就在實體類上加@NotBlank、@NotNull註解來進行校驗

@Data
@ToString
private class AddPersonRequest {

    @NotBlank(message = "人員姓名不能為空")
    private String name;
    @NotBlank(message = "身份證號不能為空")
    private String idCardNo;
        
    //忽略
}

此時Controller介面就需要方法上就需要加上@Valid註解

@PostMapping
public void addPerson(@RequestBody @Valid AddPersonRequest addPersonRequest) {
    // 處理新增邏輯
}

10、統一返回值

後端在設計介面的時候,需要統一返回值

{  
    "code":0,
    "message":"成功",
    "data":"返回資料"
}

不僅是給前端引數,也包括提供給第三方的介面等,這樣介面呼叫方法可以按照固定的格式解析程式碼,不用進行判斷。如果不一樣,相信我,前端半夜都一定會來找你。

Spring中很多方法可以做到統一返回值,而不用每個方法都返回,比如基於AOP,或者可以自定義HandlerMethodReturnValueHandler來實現統一返回值。

11、統一例外處理

當你沒有統一例外處理的時候,那麼所有的介面避免不了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("系統異常");
    }
}

每個介面都得這麼玩,那不得滿屏的try catch。

所以可以基於Spring提供的統一例外處理機制來完成。

12、儘量不傳遞null值

這個很好理解,不傳null值可以避免方法不支援為null入參時產生的空指標問題。

當然為了更好的表明該方法是不是可以傳null值,可以通過@NonNull和@Nullable註解來標記。@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值是為了減少呼叫者對返回值的為null判斷,如果無法避免返回null值,可以通過返回Optional來代替null值。

public Optional<Person> getPersonById(Long personId) {
    return Optional.ofNullable(personService.selectById(personId));
}

如果不想這麼寫,也可以通過@NonNull和@Nullable表示方法會不會返回null值。

14、紀錄檔列印規範

好的紀錄檔列印能幫助我們快速定位問題

好的紀錄檔應該遵循以下幾點:

  • 可搜尋性,要有明確的關鍵字資訊
  • 異常紀錄檔需要列印出堆疊資訊
  • 合適的紀錄檔級別,比如異常使用error,正常使用info
  • 紀錄檔內容太大不列印,比如有時需要將圖片轉成Base64,那麼這個Base64就可以不用列印

15、統一類庫

在一個專案中,可能會由於引入的依賴不同導致引入了很多相似功能的類庫,比如常見的json類庫,又或者是一些常用的工具類,當遇到這種情況下,應當規範在專案中到底應該使用什麼類庫,而不是一會用Fastjson,一會使用Gson。

16、儘量使用工具類

比如在對集合判空的時候,可以這麼寫

public void updatePersons(List<Person> persons) {
    if (persons != null && persons.size() > 0) {
           
    }
}

但是一般不推薦這麼寫,可以通過一些判斷的工具類來寫

public void updatePersons(List<Person> persons) {
    if (!CollectionUtils.isEmpty(persons)) {

    }
}

不僅集合,比如字串的判斷等等,就使用工具類,不要手動判斷。

17、儘量不要重複造輪子

就拿格式化日期來來說,我們一般封裝成一個工具類來呼叫,比如如下程式碼

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);
}

這段程式碼看似沒啥問題,但是卻忽略了SimpleDateFormat是個執行緒不安全的類,所以這就會引起坑。

一般對於這種已經有開源的專案並且已經做得很好的時候,比如Hutool,就可以把輪子直接拿過來用了。

18、類和方法單一職責

單一職責原則是設計模式的七大設計原則之一,它的核心意思就是字面的意思,一個類或者一個方法只做單一的功能。

就拿Nacos來說,在Nacos1.x的版本中,有這麼一個介面HttpAgent

這個類只幹了一件事,那就是封裝http請求引數,向Nacos伺服器端傳送請求,接收響應,這其實就是單一職責原則的體現。

當其它的地方需要向Nacos伺服器端傳送請求時,只需要通過這個介面的實現,傳入引數就可以傳送請求了,而不需要關心如何攜帶伺服器端鑑權引數、http請求引數如何組裝等問題。

19、儘量使用聚合/組合代替繼承

繼承的弊端:

  • 靈活性低。java語言是單繼承的,無法同時繼承很多類,並且繼承容易導致程式碼層次太深,不易於維護
  • 耦合性高。一旦父類別的程式碼修改,可能會影響到子類的行為

所以一般推薦使用聚合/組合代替繼承。

聚合/組合的意思就是通過成員變數的方式來使用類。

比如說,OrderService需要使用UserService,可以注入一個UserService而非通過繼承UserService。

聚合和組合的區別就是,組合是當物件一建立的時候,就直接給屬性賦值,而聚合的方式可以通過set方式來設定。

組合:

public class OrderService {

    private UserService userService = new UserService();

}

聚合:

public class OrderService {
    
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

20、使用設計模式優化程式碼

在平時開發中,使用設計模式可以增加程式碼的擴充套件性。

比如說,當你需要做一個可以根據不同的平臺做不同訊息推播的功能時,就可以使用策略模式的方式來優化。

設計一個介面:

public interface MessageNotifier {

    /**
     * 是否支援改型別的通知的方式
     *
     * @param type 0:簡訊 1:app
     * @return
     */

    boolean support(int type);

    /**
     * 通知
     *
     * @param user
     * @param content
     */

    void notify(User user, String content);

}

簡訊通知實現:

@Component
public class SMSMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 0;
    }

    @Override
    public void notify(User user, String content) {
        //呼叫簡訊通知的api傳送簡訊
    }
}

app通知實現:

public class AppMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 1;
    }

    @Override
    public void notify(User user, String content) {
       //呼叫通知app通知的api
    }
}

最後提供一個方法,當需要進行訊息通知時,呼叫notifyMessage,傳入相應的引數就行。

@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);
        }
    }
}

假設此時需要支援通過郵件通知,只需要有對應實現就行。

21、不濫用設計模式

用好設計模式可以增加程式碼的擴充套件性,但是濫用設計模式確是不可取的。

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("身份證號:").append(person.getIdCardNo());
    }

    // 省略
    System.out.println(sb.toString());
}

比如上面列印Person資訊的程式碼,用if判斷就能夠做到效果,你說我要不用責任鏈或者什麼設計模式來優化一下吧,沒必要。

22、面向介面程式設計

在一些可替換的場景中,應該參照父類別或者抽象,而非實現。

舉個例子,在實際專案中可能需要對一些圖片進行儲存,但是儲存的方式很多,比如可以選擇阿里雲的OSS,又或者是七牛雲,儲存伺服器等等。所以對於儲存圖片這個功能來說,這些具體的實現是可以相互替換的。

所以在專案中,我們不應當在程式碼中耦合一個具體的實現,而是可以提供一個儲存介面

public interface FileStorage {
    
    String store(String fileName, byte[] bytes);

}

如果選擇了阿里雲OSS作為儲存伺服器,那麼就可以基於OSS實現一個FileStorage,在專案中哪裡需要儲存的時候,只要實現注入這個介面就可以了。

@Autowired
private FileStorage fileStorage;

假設用了一段時間之後,發現阿里雲的OSS比較貴,此時想換成七牛雲的,那麼此時只需要基於七牛雲的介面實現FileStorage介面,然後注入到IOC,那麼原有程式碼用到FileStorage根本不需要動,實現輕鬆的替換。

23、經常重構舊的程式碼

隨著時間的推移,業務的增長,有的程式碼可能不再適用,或者有了更好的設計方式,那麼可以及時的重構業務程式碼。

就拿上面的訊息通知為例,在業務剛開始的時候可能只支援簡訊通知,於是在程式碼中就直接耦合了簡訊通知的程式碼。但是隨著業務的增長,逐漸需要支援app、郵件之類的通知,那麼此時就可以重構以前的程式碼,抽出一個策略介面,進行程式碼優化。

24、null值判斷

空指標是程式碼開發中的一個難題,作為程式設計師的基本修改,應該要防止空指標。

可能產生空指標的原因:

  • 資料返回物件為null
  • 自動拆箱導致空指標
  • rpc呼叫返回的物件可能為空格

所以在需要這些的時候,需要強制判斷是否為null。前面也提到可以使用Optional來優雅地進行null值判斷。

25、pojo類重寫toString方法

pojo一般內部都有很多屬性,重寫toString方法可以方便在列印或者測試的時候檢視內部的屬性。

26、魔法值用常數表示

public void sayHello(String province) {
    if ("廣東省".equals(province)) {
        System.out.println("靚仔~~");
    } else {
        System.out.println("帥哥~~");
    }
}

程式碼裡,廣東省就是一個魔法值,那麼就可以將用一個常數來儲存

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、資源釋放寫到finally

比如在使用一個api類鎖或者進行IO操作的時候,需要主動寫程式碼需釋放資源,為了能夠保證資源能夠被真正釋放,那麼就需要在finally中寫程式碼保證資源釋放。

如圖所示,就是CopyOnWriteArrayList的add方法的實現,最終是在finally中進行鎖的釋放。

28、使用執行緒池代替手動建立執行緒

使用執行緒池還有以下好處:

  • 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統 的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

所以為了達到更好的利用資源,提高響應速度,就可以使用執行緒池的方式來代替手動建立執行緒。

如果對執行緒池不清楚的同學,可以看一下這篇文章: 7000字+24張圖帶你徹底弄懂執行緒池

29、執行緒設定名稱

在紀錄檔列印的時候,紀錄檔是可以把執行緒的名字給列印出來。

如上圖,紀錄檔列印出來的就是tom貓的執行緒。

所以,設定執行緒的名稱可以幫助我們更好的知道程式碼是通過哪個執行緒執行的,更容易排查問題。

30、涉及執行緒間可見性加volatile

在RocketMQ原始碼中有這麼一段程式碼

在消費者在從伺服器端拉取訊息的時候,會單獨開一個執行緒,執行while迴圈,只要stopped狀態一直為false,那麼就會一直迴圈下去,執行緒就一直會執行下去,拉取訊息。

當消費者使用者端關閉的時候,就會將stopped狀態設定為true,告訴拉取訊息的執行緒需要停止了。但是由於並行程式設計中存在可見性的問題,所以雖然使用者端關閉執行緒將stopped狀態設定為true,但是拉取訊息的執行緒可能看不見,不能及時感知到資料的修改,還是認為stopped狀態設定為false,那麼就還會執行下去。

針對這種可見性的問題,java提供了一個volatile關鍵字來保證執行緒間的可見性。

所以,原始碼中就加了volatile關鍵字。

加了volatile關鍵字之後,一旦使用者端的執行緒將stopped狀態設定為true時候,拉取訊息的執行緒就能立馬知道stopped已經是false了,那麼再次執行while條件判斷的時候,就不成立,執行緒就執行結束了,然後退出。

31、考慮執行緒安全問題

在平時開發中,有時需要考慮並行安全的問題。

舉個例子來說,一般在呼叫第三方介面的時候,可能會有一個鑑權的機制,一般會攜帶一個請求頭token引數過去,而token也是呼叫第三方介面返回的,一般這種token都會有個過期時間,比如24小時。

我們一般會將token快取到Redis中,設定一個過期時間。向第三方傳送請求時,會直接從快取中查詢,但是當從Redis中獲取不到token的時候,我們都會重新請求token介面,獲取token,然後再設定到快取中。

整個過程看起來是沒什麼問題,但是實則隱藏執行緒安全問題。

假設當出現並行的時候,同時來兩個執行緒AB從快取查詢,發現沒有,那麼AB此時就會同時呼叫token獲取介面。假設A先獲取到token,B後獲取到token,但是由於CPU排程問題,執行緒B雖然後獲取到token,但是先往Redis存資料,而執行緒A後存,覆蓋了B請求的token。

這下就會出現大問題,最新的token被覆蓋了,那麼之後一定時間內token都是無效的,介面就請求不通。

針對這種問題,可以使用double check機制來優化獲取token的問題。

所以,在實際中,需要多考慮考慮業務是否有執行緒安全問題,有集合讀寫安全問題,那麼就用執行緒安全的集合,業務有安全的問題,那麼就可以通過加鎖的手段來解決。

32、慎用非同步

雖然在使用多執行緒可以幫助我們提高介面的響應速度,但是也會帶來很多問題。

事務問題

一旦使用了非同步,就會導致兩個執行緒不是同一個事務的,導致異常之後無法正常回滾資料。

cpu負載過高

之前有個小夥伴遇到需要同時處理幾萬調資料的需求,每條資料都需要呼叫很多次介面,為了達到老闆期望的時間要求,使用了多執行緒跑,開了很多執行緒,此時會發現系統的cpu會飆升

意想不到的異常

還是上面的提到的例子,在測試的時候就發現,由於並行量激增,在請求第三方介面的時候,返回了很多錯誤資訊,導致有的資料沒有處理成功。

雖然說慎用非同步,但不代表不用,如果可以保證事務的問題,或是CPU負載不會高的話,那麼還是可以使用的。

33、減小鎖的範圍

減小鎖的範圍就是給需要加鎖的程式碼加鎖,不需要加鎖的程式碼不要加鎖。這樣就能減少加鎖的時間,從而可以較少鎖互斥的時間,提高效率。

比如CopyOnWriteArrayList的addAll方法的實現,lock.lock(); 程式碼完全可以放到程式碼的第一行,但是作者並沒有,因為前面判斷的程式碼不會有執行緒安全的問題,不放到加鎖程式碼中可以減少鎖搶佔和佔有的時間。

34、有型別區分時定義好列舉

比如在專案中不同的型別的業務可能需要上傳各種各樣的附件,此時就可以定義好不同的一個附件的列舉,來區分不同業務的附件。

不要在程式碼中直接寫死,不定義列舉,程式碼閱讀起來非常困難,直接看到數位都是懵逼的。。

35、遠端介面呼叫設定超時時間

比如在進行微服務之間進行rpc呼叫的時候,又或者在呼叫第三方提供的介面的時候,需要設定超時時間,防止因為各種原因,導致執行緒」卡死「在那。

我以前就遇到過線上就遇到過這種問題。當時的業務是訂閱kafka的訊息,然後向第三方上傳資料。在某個週末,突然就接到電話,說資料無法上傳了,通過排查線上的伺服器才發現所有的執行緒都執行緒」卡死「了,最後定位到程式碼才發現原來是沒有設定超時時間。

36、集合使用應當指明初始化大小

比如在寫程式碼的時候,經常會用到List、Map來臨時儲存資料,其中最常用的就是ArrayList和HashMap。但是用不好可能也會導致效能的問題。

比如說,在ArrayList中,底層是基於陣列來儲存的,陣列是一旦確定大小是無法再改變容量的。但不斷的往ArrayList中儲存資料的時候,總有那麼一刻會導致陣列的容量滿了,無法再儲存其它元素,此時就需要對陣列擴容。所謂的擴容就是新建立一個容量是原來1.5倍的陣列,將原有的資料給拷貝到新的陣列上,然後用新的陣列替代原來的陣列。

在擴容的過程中,由於涉及到陣列的拷貝,就會導致效能消耗;同時HashMap也會由於擴容的問題,消耗效能。所以在使用這類集合時可以在構造的時候指定集合的容量大小。

37、儘量不要使用BeanUtils來拷貝屬性

在開發中經常需要對JavaBean進行轉換,但是又不想一個一個手動set,比較麻煩,所以一般會使用屬性拷貝的一些工具,比如說Spring提供的BeanUtils來拷貝。不得不說,使用BeanUtils來拷貝屬性是真的舒服,使用一行程式碼可以代替幾行甚至十幾行程式碼,我也喜歡用。

但是喜歡歸喜歡,但是會帶來效能問題,因為底層是通過反射來的拷貝屬性的,所以儘量不要用BeanUtils來拷貝屬性。

比如你可以裝個JavaBean轉換的外掛,幫你自動生成轉換程式碼;又或者可以使用效能更高的MapStruct來進行JavaBean轉換,MapStruct底層是通過呼叫(settter/getter)來實現的,而不是反射來快速執行。

38、使用StringBuilder進行字串拼接

如下程式碼:

String str1 = "123";
String str2 = "456";
String str3 = "789";
String str4 = str1 + str2 + str3;

使用 + 拼接字串的時候,會建立一個StringBuilder,然後將要拼接的字串追加到StringBuilder,再toString,這樣如果多次拼接就會執行很多次的建立StringBuilder,z執行toString的操作。

所以可以手動通過StringBuilder拼接,這樣只會建立一次StringBuilder,效率更高。

StringBuilder sb = new StringBuilder();
String str = sb.append("123").append("456").append("789").toString();

39、@Transactional應指定回滾的異常型別

平時在寫程式碼的時候需要通過rollbackFor顯示指定需要對什麼異常回滾,原因在這:

預設是隻能回滾RuntimeException和Error異常,所以需要手動指定,比如指定成Expection等。

40、謹慎方法內部呼叫動態代理的方法

如下事務程式碼

@Service
public class PersonService {
    
    public void update(Person person) {
        // 處理
        updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person
{
        // 處理
    }

}

update呼叫了加了@Transactional註解的updatePerson方法,那麼此時updatePerson的事務就是失效。

其實失效的原因不是事務的鍋,是由AOP機制決定的,因為事務是基於AOP實現的。AOP是基於物件的代理,當內部方法呼叫時,走的不是動態代理物件的方法,而是原有物件的方法呼叫,如此就走不到動態代理的程式碼,就會失效了。

如果實在需要讓動態代理生效,可以注入自己的代理物件

@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什麼欄位

查詢全欄位有以下幾點壞處:

增加不必要的欄位的網路傳輸

比如有些文字的欄位,儲存的資料非常長,但是本次業務使用不到,但是如果查了就會把這個資料返回給使用者端,增加了網路傳輸的負擔

會導致無法使用到覆蓋索引

比如說,現在有身份證號和姓名做了聯合索引,現在只需要根據身份證號查詢姓名,如果直接select name 的話,那麼在遍歷索引的時候,發現要查詢的欄位在索引中已經存在,那麼此時就會直接從索引中將name欄位的資料查出來,返回,而不會繼續去查詢聚簇索引,減少回表的操作。

所以建議是需要使用什麼欄位查詢什麼欄位。比如mp也支援在構建查詢條件的時候,查詢某個具體的欄位。

 Wrappers.query().select("name");

42、不迴圈呼叫資料庫

不要在迴圈中存取資料庫,這樣會嚴重影響資料庫效能。

比如需要查詢一批人員的資訊,人員的資訊存在基本資訊表和擴充套件表中,錯誤的程式碼如下:

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());
        // 組裝資料
        persons.add(vo);
    }
    return persons;
}

遍歷每個人員的基本資訊,去資料庫查詢。

正確的方法應該先批次查出來,然後轉成map:

public List<PersonVO> selectPersons(List<Long> personIds) {
    List<PersonVO> persons = new ArrayList<>(personIds.size());
    List<Person> personList = personMapper.selectByIds(personIds);
        //批次查詢,轉換成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());
        // 組裝資料
        persons.add(vo);
    }
    return persons;
}

43、用業務程式碼代替多表join

如上面程式碼所示,原本也可以將兩張表根據人員的id進行關聯查詢。但是不推薦這麼,阿里也禁止多表join的操作

而之所以會禁用,是因為join的效率比較低。

MySQL是使用了巢狀迴圈的方式來實現關聯查詢的,也就是for迴圈會套for迴圈的意思。用第一張表做外迴圈,第二張表做內迴圈,外迴圈的每一條記錄跟內迴圈中的記錄作比較,符合條件的就輸出,這種效率肯定低。

44、裝上阿里程式碼檢查外掛

我們平時寫程式碼由於各種因為,比如什麼領導啊,專案經理啊,會一直催進度,導致寫程式碼都來不及思考,怎麼快怎麼來,cv大法上線,雖然有心想寫好程式碼,但是手確不聽使喚。所以我建議裝一個阿里的程式碼規範外掛,如果有程式碼不規範,會有提醒,這樣就可以知道哪些是可以優化的了。

如果你有強迫症,相信我,裝了這款外掛,你的程式碼會寫的很漂亮。

45、及時跟同事溝通

寫程式碼的時候不能閉門造車,及時跟同事溝通,比如剛進入一個新的專案的,對專案工程不熟悉,一些技術方案不瞭解,如果上來就直接寫程式碼,很有可能就會踩坑。

參考資料:

《程式碼簡潔之道》

《阿里巴巴Java開發手冊》

如何寫出讓人抓狂的程式碼?

往期熱門文章推薦

擼了一個簡易的設定中心,順帶還給整合到了SpringCloud

RocketMQ保姆級教學

三萬字盤點Spring/Boot的那些常用擴充套件點

RocketMQ的push消費方式實現的太聰明瞭

一網打盡非同步神器CompletableFuture

@Async註解的坑,小心

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。