用了Stream後,程式碼反而越寫越醜?
原創:小姐姐味道(微信公眾號ID:xjjdog),歡迎分享,轉載請保留出處。
Java8的stream流,加上lambda表示式,可以讓程式碼變短變美,已經得到了廣泛的應用。我們在寫一些複雜程式碼的時候,也有了更多的選擇。
程式碼首先是給人看的,其次才是給機器執行的。程式碼寫的是否簡潔明瞭,是否寫的漂亮,對後續的bug修復和功能擴充套件,意義重大。很多時候,是否能寫出優秀的程式碼,是和工具沒有關係的。程式碼是工程師能力和修養的體現,有的人,即使用了stream,用了lambda,程式碼也依然寫的像屎一樣。
不信,我們來參觀一下一段
美妙
的程式碼。好傢伙,filter裡面竟然帶著瀟灑的邏輯。
public List
List
List
。filter(this::addDetail)
。map(FeedItemVo::convertVo)
。filter(vo -> this。addOrgNames(query。getIsSlow(),orgiList,vo))
。collect(Collectors。toList());
//。。。其他邏輯
return collect;
}
private boolean addDetail(FeedItem feed){
vo。setItemCardConf(service。getById(feed。getId()));
return true;
}
private boolean addOrgNames(boolean isSlow,List
if(isShow && vo。getOrgIds() != null){
orgiList。add(vo。getOrgiName());
}
return true;
}
如果覺得不過癮的話,我們再貼上一小段。
if (!CollectionUtils。isEmpty(roleNameStrList) && roleNameStrList。contains(REGULATORY_ROLE)) {
vos = vos。stream()。filter(
vo -> !CollectionUtils。isEmpty(vo。getSpecialTaskItemVoList())
&& vo。getTaskName() != null)
。collect(Collectors。toList());
} else {
vos = vos。stream()。filter(vo -> vo。getIsSelect()
&& vo。getTaskName() != null)
。collect(Collectors。toList());
vos = vos。stream()。filter(
vo -> !CollectionUtils。isEmpty(vo。getSpecialTaskItemVoList())
&& vo。getTaskName() != null)
。collect(Collectors。toList());
}
result。addAll(vos。stream()。collect(Collectors。toList()));
程式碼能跑,但多畫蛇添足。該縮排的不縮排,該換行的不換行,說什麼也算不上好程式碼。
如何改善?除了技術問題,還是一個意識問題。時刻記得,優秀的程式碼,首先是可讀的,然後才是功能完善。
1。 合理的換行
在Java中,同樣的功能,程式碼行數寫的少了,並不見得你的程式碼就好。由於Java使用
;
作為程式碼行的分割,如果你喜歡的話,甚至可以將整個Java檔案搞成一行,就像是混淆後的JavaScript一樣。
當然,我們知道這麼做是不對的。在lambda的書寫上,有一些套路可以讓程式碼更加規整。
Stream。of(“i”, “am”, “xjjdog”)。map(toUpperCase())。map(toBase64())。collect(joining(“ ”));
上面這種程式碼的寫法,就非常的不推薦。除了在閱讀上容易造成障礙,在程式碼發生問題的時候,比如丟擲異常,在異常堆疊中找問題也會變的困難。所以,我們要將它優雅的換行。
Stream。of(“i”, “am”, “xjjdog”)
。map(toUpperCase())
。map(toBase64())
。collect(joining(“ ”));
不要認為這種改造很沒有意義,或者認為這樣的換行是理所當然的。在我平常的程式碼review中,這種糅雜在一塊的程式碼,真的是數不勝數,你完全搞不懂寫程式碼的人的意圖。
合理的換行是程式碼青春永駐的配方。
2。 捨得拆分函式
為什麼函式能夠越寫越長?是因為技術水平高,能夠駕馭這種變化麼?答案是因為懶!由於開發工期或者意識的問題,遇到有新的需求,直接往老的程式碼上新增ifelse,即使遇到相似的功能,也直接選擇將原來的程式碼複製過去。久而久之,碼將不碼。
首先聊一點效能方面的。在JVM中,JIT編譯器會對呼叫量大,邏輯簡單的程式碼進行方法內聯,以減少棧幀的開銷,並能進行更多的最佳化。所以,短小精悍的函式,其實是對JVM友好的。
在可讀性方面,將一大坨程式碼,拆分成有意義的函式,是非常有必要的,也是重構的精髓所在。在lambda表示式中,這種拆分更是有必要。
我將拿一個經常在程式碼中出現的實體轉換示例來說明一下。下面的轉換,建立了一個匿名的函式
order->{}
,它在語義表達上,是非常弱的。
public Stream
return orderRepo。findOrderByUser()。stream()
。map(order-> {
OrderDto dto = new OrderDto();
dto。setOrderId(order。getOrderId());
dto。setTitle(order。getTitle()。split(“#”)[0]);
dto。setCreateDate(order。getCreateDate()。getTime());
return dto;
});
}
在實際的業務程式碼中,這樣的賦值複製還有轉換邏輯通常非常的長,我們可以嘗試把dto的建立過程給獨立開來。因為轉換動作不是主要的業務邏輯,我們通常不會關心其中到底發生了啥。
public Stream
return orderRepo。findOrderByUser()。stream()
。map(this::toOrderDto);
}
public OrderDto toOrderDto(Order order){
OrderDto dto = new OrderDto();
dto。setOrderId(order。getOrderId());
dto。setTitle(order。getTitle()。split(“#”)[0]);
dto。setCreateDate(order。getCreateDate()。getTime());
return dto;
}
這樣的轉換程式碼還是有點醜。但如果OrderDto的建構函式,引數就是Order的話
public OrderDto(Order order)
,那我們就可以把真個轉換邏輯從主邏輯中移除出去,整個程式碼就可以非常的清爽。
public Stream
return orderRepo。findOrderByUser()。stream()
。map(OrderDto::new);
}
除了map和flatMap的函式可以做語義化,更多的filter可以使用Predicate去代替。比如:
Predicate
reg。getRegulationId() != null
&& reg。getRegulationId() != 0
&& reg。getType() == 0;
registarIsCorrect,就可以當作
filter
的引數。
3。 合理的使用Optional
在Java程式碼裡,由於NullPointerException不屬於強制捕捉的異常,它會隱藏在程式碼裡,造成很多不可預料的bug。所以,我們會在拿到一個引數的時候,都會驗證它的合法性,看一下它到底是不是null,程式碼中到處充滿了這樣的程式碼。
if(null == obj)
if(null == user。getName() || “”。equals(user。getName()))
if (order != null) {
Logistics logistics = order。getLogistics();
if(logistics != null){
Address address = logistics。getAddress();
if (address != null) {
Country country = address。getCountry();
if (country != null) {
Isocode isocode = country。getIsocode();
if (isocode != null) {
return isocode。getNumber();
}
}
}
}
}
Java8引入了Optional類,用於解決臭名昭著的空指標問題。實際上,它是一個包裹類,提供了幾個方法可以去判斷自身的空值問題。
上面比較複雜的程式碼示例,就可以替換成下面的程式碼。
String result = Optional。ofNullable(order)
。flatMap(order->order。getLogistics())
。flatMap(logistics -> logistics。getAddress())
。flatMap(address -> address。getCountry())
。map(country -> country。getIsocode())
。orElse(Isocode。CHINA。getNumber());
當你不確定你提供的東西,是不是為空的時候,一個好的習慣是不要返回null,否則呼叫者的程式碼將充滿了null的判斷。我們要把null消滅在萌芽中。
public Optional
return Optional。ofNullable(userName);
}
另外,我們要儘量的少使用Optional的get方法,它同樣會讓程式碼變醜。比如:
Optional
String defaultEmail = userName。get() == null ? “”:userName。get() + “@xjjdog。cn”;
而應該修改成這樣的方式:
Optional
String defaultEmail = userName
。map(e -> e + “@xjjdog。cn”)
。orElse(“”);
那為什麼我們的程式碼中,依然充滿了各式各樣的空值判斷?即使在非常專業和流行的程式碼中?一個非常重要的原因,就是Optional的使用需要保持一致。當其中的一環出現了斷層,大多數編碼者都會以模仿的方式去寫一些程式碼,以便保持與原始碼風格的一致。
如果想要普及Optional在專案中的使用,腳手架設計者或者review人,需要多下一點功夫。
4。 返回Stream還是返回List?
很多人在設計介面的時候,會陷入兩難的境地。我返回的資料,是直接返回Stream,還是返回List?
如果你返回的是一個List,比如ArrayList,那麼你去修改這個List,會直接影響裡面的值,除非你使用不可變的方式對其進行包裹。同樣的,陣列也有這樣的問題。
但對於一個Stream來說,是不可變的,它不會影響原始的集合。對於這種場景,我們推薦直接返回Stream流,而不是返回集合。這種方式還有一個好處,能夠強烈的暗示API使用者,多多使用Stream相關的函式,以便能夠統一程式碼風格。
public Stream
。。。
return Stream。of(users);
}
不可變集合是一個強需求,它能防止外部的函式對這些集合進行不可預料的修改。在guava中,就有大量的
Immutable
類支援這種包裹。再舉一個例子,Java的列舉,它的
values()
方法,為了防止外面的api對列舉進行修改,就只能複製一份資料。
但是,如果你的api,面向的是最終的使用者,不需要再做修改,那麼直接返回List就是比較好的,比如函式在Controller中。
5。 少用或者不用並行流
Java的並行流有很多問題,這些問題對併發程式設計不熟悉的人高頻率踩坑。不是說並行流不好,但如果你發現你的團隊,老在這上面栽跟頭,那你也會毫不猶豫的降低推薦的頻率。
並行流一個老生常談的問題,就是執行緒安全問題。在迭代的過程中,如果使用了執行緒不安全的類,那麼就容易出現問題。比如下面這段程式碼,大多數情況下執行都是錯誤的。
List transform(List source){
List dst = new ArrayList<>();
if(CollectionUtils。isEmpty()){
return dst;
}
source。stream。
。parallel()
。map(。。)
。filter(。。)
。foreach(dst::add);
return dst;
}
你可能會說,我把foreach改成collect就行了。但是注意,很多開發人員是沒有這樣的意識的。既然api提供了這樣的函式,它在邏輯上又講得通,那你是阻擋不住別人這麼用的。
並行流還有一個濫用問題,就是在迭代中執行了耗時非常長的IO任務。在用並行流之前,你有沒有一個疑問?既然是並行,那它的執行緒池是怎麼配置的?
很不幸,所有的並行流,共用了一個ForkJoinPool。它的大小,預設是
CPU個數-1
,大多數情況下,是不夠用的。
如果有人在並行流上跑了耗時的IO業務,那麼你即使執行一個簡單的數學運算,也需要排隊。關鍵是,你是沒辦法阻止專案內的其他同學使用並行流的,也無法知曉他幹了什麼事情。
那怎麼辦?我的做法是一刀切,直接禁止。雖然殘忍了一些,但它避免了問題。
總結
Java8加入的Stream功能非常棒,我們不需要再羨慕其他語言,寫起程式碼來也更加行雲流水。雖然看著很厲害的樣子,但它也只不過是一個語法糖而已,不要寄希望於用了它就獲得了超能力。
隨著Stream的流行,我們的程式碼裡這樣的程式碼也越來越多。但現在很多程式碼,使用了Stream和Lambda以後,程式碼反而越寫越糟,又臭又長以至於不能閱讀。沒其他原因,濫用了!
總體來說,使用Stream和Lambda,要保證主流程的簡單清晰,風格要統一,合理的換行,捨得加函式,正確的使用Optional等特性,而且不要在filter這樣的函數里加程式碼邏輯。在寫程式碼的時候,要有意識的遵循這些小tips,簡潔優雅就是生產力。
如果覺得Java提供的特性還是不夠,那我們還有一個開源的類庫
vavr
,提供了更多的可能性,能夠和Stream以及Lambda結合起來,來增強函式程式設計的體驗。
但無論提供瞭如何強大的api和程式設計方式,都扛不住小夥伴的濫用。這些程式碼,在邏輯上完全是說的通的,但就是看起來彆扭,維護起來費勁。
寫一堆垃圾lambda程式碼,是虐待同事最好的方式,也是埋坑的不二選擇。
寫程式碼嘛,就如同說話、聊天一樣。大家幹著同樣的工作,有的人說話好聽顏值又高,大家都喜歡和他聊天;有的人不好好說話,哪裡痛戳哪裡,雖然他存在著但大家都討厭。
程式碼,除了工作的意義,不過是我們在世界上表達自己想法的另一種方式罷了。如何寫好程式碼,不僅僅是個技術問題,更是一個意識問題。
作者簡介:小姐姐味道 (xjjdog),一個不允許程式設計師走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎新增好友,進一步交流。
推薦閱讀:
1。 玩轉Linux
2。 什麼味道專輯
3。 藍芽如夢
4。 殺機!
5。 失聯的架構師,只留下一段指令碼
6。 架構師寫的BUG,非比尋常