JAVA基礎之程式碼簡潔之道
背景
軟體質量,不但依賴於架構及專案管理,更與程式碼質量緊密相關。簡潔高效的程式碼不但易於閱讀,更能避免潛在BUG與風險,提高程式碼質量。近期,一位Oracle程式設計師在Hacker News上吐槽自己的工作,引起了熱議。
這個工程師的核心痛點是,Oracle經歷長期的產品線迭代,程式碼異常龐大、邏輯複雜,整個程式碼中充斥著神祕的巨集命令。每新增一個特性或者修復BUG,該工程師都需要大量的調研,小心謹慎的進行著日常的工作。而Oracle每次的版本釋出都經歷數百萬次的測試,腦補一下,如噩夢一般。那麼我們應該如何編寫簡潔高效的程式碼呢?其實業內有過很多相關書籍,比如經典的書籍有《程式碼整潔之道》、《編寫可讀程式碼的藝術》、《重構:改善既有程式碼的設計》,可用於修煉內功。以及我們有嚴格的程式碼規範以及方便的靜態程式碼掃描工具,可用於加強研發程式碼質量能力。
簡潔之術
其實程式碼規範和靜態程式碼掃描工具能夠幫助我們完成很多程式碼簡潔的工作。諸如:註釋、命名、方法、異常、單元測試等多個方面。但卻無法總結了一些程式碼簡潔最佳實踐,其實Java是物件導向語音,而物件導向的特徵是封裝、繼承、多型,巧妙的運用這三大特性、理解Java的一些關鍵字特性、語音特性、閱讀JDK原始碼,就可以寫出相對簡潔的程式碼了。
簡化邏輯
// 修改前``
if(list.size()>0) {
return true;
} else {
return false;
}
複製程式碼
// 修改後
return list.size()>0;
複製程式碼
(1) if/else 語法:if語句包含一個布林表示式,當if語句的布林表示式值為false時,else語句塊會被執行;
(2) return 關鍵字:返回一個任意型別的值;
(3) list.size()>0 表示式:list.size()方法本身是一個返回int型別數值的函式,而與>0組成了一個布林表示式;
省略無意義賦值
(1)區域性變數list的資料型別與該方法的返回值型別一致,而多餘的變數也將會增加JVM垃圾回收的消耗;
(2)區域性變數list只是負責接收了mapper.queryList(params)的返回值,而並沒有其他邏輯處理;
(3)此程式碼存在於service層和mapper層之間,可以在框架層面進一步抽象,利用註解、java8 default方法等進一步改進;
最小化判斷
程式碼中if else的存在只是因為sendMessage函式的第二個引數會有兩種情況(成功/失敗),儘量讓判斷最小化;
set方法治理
(1)大坨的set方法很影響程式碼可讀性,可封裝成特定方法或者使用lombok工具簡化程式碼;
(2)區域性變數就近宣告,增加可讀性,區域性變數宣告和使用地方距離遙遠,會導致的讀者頻繁滑動;
(3)可不宣告變數儘量不要宣告多餘的變數,冗餘程式碼;(如date、time兩段程式碼);
巧用JAVA8特性-函數語言程式設計簡化程式碼
JAVA8特性“函數語言程式設計”,使用Lambdas我們能做到什麼?
(1)遍歷集合(List、Map等)、Sum、Max、Min、Avg、Sort、Distinct等等 (2)函式介面 (3)謂詞(Predicate)使用 (4)實現Map和Reduce (5)實現事件處理/簡化多執行緒
內、外部迴圈
上述程式碼是傳統方式的遍歷一個List的寫法,簡單來說主要有3個不足:
(1)只能順序處理list中的資料(process one by one)
(2)不能充分利用多核cpu
(3)不利於編譯器優化(jit)
而使用函數語言程式設計能規避上面的三個問題:
(1)不一定需要順序處理List中的元素,順序可以不確定
(2)可以並行處理,充分利用多核CPU的優勢
(3)有利於JIT編譯器對程式碼進行優化
(4)程式碼看起來更簡潔,完全交給編譯器內部迴圈
default方法
在Java8中,介面中的方法可以被實現,用關鍵字 default 作為修飾符來標識,介面中被實現的方法叫做 default 方法。使用default方法,當介面發生改變的時候,實現類不需要做改動,所有的子類都會繼承 default 方法。
當一個介面擴充套件另外一個包含預設方法的介面的時候,有以下3種處理方式。
(1)完全無視預設方法(直接繼承上級介面的預設方法)
(2)重新申明預設方法為抽象方法(無實現,具體子類必需再次實現該方法)
(3)重新實現預設方法(重寫了預設方法的實現,依然是一個預設方法)
日期處理
Java8中新增了LocalDate和LocalTime介面,為什麼要搞一套全新的處理日期和時間的API?因為舊的java.util.Date實在是太難用了。
(1)java.util.Date月份從0開始,一月是0,十二月是11,變態吧!java.time.LocalDate月份和星期都改成了enum,就不可能再用錯了。
(2)java.util.Date和SimpleDateFormatter都不是執行緒安全的,而LocalDate和LocalTime和最基本的String一樣,是不變型別,不但執行緒安全,而且不能修改。
(3)java.util.Date是一個“萬能介面”,它包含日期、時間,還有毫秒數,如果你只想用java.util.Date儲存日期,或者只儲存時間,那麼,只有你知道哪些部分的資料是有用的,哪些部分的資料是不能用的。在新的Java8中,日期和時間被明確劃分為LocalDate和LocalTime,LocalDate無法包含時間,LocalTime無法包含日期。
當然,LocalDateTime才能同時包含日期和時間。
新介面更好用的原因是考慮到了日期時間的操作,經常發生往前推或往後推幾天的情況。用java.util.Date配合Calendar要寫好多程式碼,而且一般的開發人員還不一定能寫對。
1、Clock時鐘。Clock類提供了訪問當前日期和時間的方法,Clock是時區敏感的,可以用來取代System.currentTimeMillis(),來獲取當前的微秒數。某一個特定的時間點也可以使用Instant類(為Final類)來表示,Instant類也可以用來建立老的java.util.Date物件。
2、LocalDate和LocalTime、LocalDateTime(均為Final類,不帶時區)的一系列計算。LocalDateTime和Instant兩者很像都是不帶時區的日期和時間,Instant中是不帶時區的即時時間點。比如:兩個人都是2018年4月14日出生的,一個出生在北京,一個出生在紐約;看上去他們是一起出生的(LocalDateTime的語義),其實他們是有時間差的(Instant的語義)
Streams與集合
Stream是對集合的包裝,通常和lambda一起使用。使用lambdas可以支援許多操作。如 map,filter,limit,sorted,count,min,max,sum,collect等等。 同樣,Stream使用懶運算,他們並不會真正地讀取所有資料。遇到像getFirst()這樣的方法就會結束鏈式語法,通過下面一系列例子介紹:比如我有個Person類,就是一個簡單的pojo, 針對這個物件,我們可能有這樣一系列的運算需求。
傳遞行為,而不僅僅是傳值
//sumAll演算法很簡單,完成的是將List中所有元素相加。
public static int sumAll(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
複製程式碼
sumAll演算法很簡單,完成的是將List中所有元素相加。某一天如果我們需要增加一個對List中所有偶數求和的方法sumAllEven,那麼就產生了sumAll2,如下:
public static int sumAll2(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}
複製程式碼
又有一天,我們需要增加第三個方法:對List中所有大於3的元素求和,那是不是繼續加下面的方法呢?sumAll3
public static int sumAll3(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number > 3) {
total += number;
}
}
return total;
}
複製程式碼
觀察這三個方法我們發現,有很多重複內容,唯一不同的是方法中的if條件不一樣(第一個可以看成if(true)),如果讓我們優化,可能想到的第一種重構就是策略模式吧,程式碼如下:
這無疑使用設計模式的方式優化了冗餘程式碼,但是可能要額外增加幾個類,以後擴充套件也要新增,下面看看使用lambda如何實現,宣告方法:第一個引數還是我們之前傳遞的List陣列,第二個看起來可能有點陌生,通過檢視jdk可以知道,這個類是一個謂詞(布林值的函式)
public static int sumAllByPredicate(List<Integer> numbers, Predicate<Integer> p) {
int total = 0;
for (int number : numbers) {
if (p.test(number)) {
total += number;
}
}
return total;
}
//呼叫:
sumAllByPredicate(numbers, n -> true);
sumAllByPredicate(numbers, n -> n % 2 == 0);
sumAllByPredicate(numbers, n -> n > 3);
複製程式碼
程式碼是不是比上面簡潔了很多?語義也很明確,重要的是不管以後怎麼變,都可以一行程式碼就修改了。。。萬金油啊。
其他
JAVA8 還推出了很多特性,來簡化程式碼。比如String.join函式、Objects類、Base64編碼類。
字串拼接
Objects類
Base64編碼
總結
好的程式碼需要不停的打磨,作為一個優秀的工程師,我們應該嚴格遵守,每次提交的程式碼要比遷出的時候更好。經常有人說,作為工程師一定要有團隊精神,但這種精神並不是說說而已的,需要實際的行動來體現的。設計模式、JDK的新特性都是我們可以藉助的經驗,編碼完成後思考一下,還可不可以在簡化、優化,不要成為一個“作惡”的工程師。
作者簡介
馬鐵利,隨行付架構部負責人 & TGO鯤鵬會北京分會會員,10年全棧工程師,擅長微服務分散式架構設計。主要負責隨行付架構部日常管理;參與構建微服務平臺周邊基礎設施及中介軟體;負責隨行付對外開源等事宜。
更多內容請關注微信公眾號:黑少微服務
複製程式碼