您當前的位置:首頁 > 書法

用了Stream後,程式碼反而越寫越醜?

作者:由 小姐姐味道 發表于 書法時間:2021-09-07

用了Stream後,程式碼反而越寫越醜?

原創:小姐姐味道(微信公眾號ID:xjjdog),歡迎分享,轉載請保留出處。

Java8的stream流,加上lambda表示式,可以讓程式碼變短變美,已經得到了廣泛的應用。我們在寫一些複雜程式碼的時候,也有了更多的選擇。

程式碼首先是給人看的,其次才是給機器執行的。程式碼寫的是否簡潔明瞭,是否寫的漂亮,對後續的bug修復和功能擴充套件,意義重大。很多時候,是否能寫出優秀的程式碼,是和工具沒有關係的。程式碼是工程師能力和修養的體現,有的人,即使用了stream,用了lambda,程式碼也依然寫的像屎一樣。

不信,我們來參觀一下一段

美妙

的程式碼。好傢伙,filter裡面竟然帶著瀟灑的邏輯。

public List getFeeds(Query query,Page page){

List orgiList = new ArrayList<>();

List collect = page。getRecords()。stream()

。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 orgiList,FeedItemVo vo){

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 getOrderByUser(String userId){

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 getOrderByUser(String userId){

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 getOrderByUser(String userId){

return orderRepo。findOrderByUser()。stream()

。map(OrderDto::new);

}

除了map和flatMap的函式可以做語義化,更多的filter可以使用Predicate去代替。比如:

Predicate registarIsCorrect = reg ->

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 getUserName() {

return Optional。ofNullable(userName);

}

另外,我們要儘量的少使用Optional的get方法,它同樣會讓程式碼變醜。比如:

Optional userName = “xjjdog”;

String defaultEmail = userName。get() == null ? “”:userName。get() + “@xjjdog。cn”;

而應該修改成這樣的方式:

Optional userName = “xjjdog”;

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 getAuthUsers(){

。。。

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結合起來,來增強函式程式設計的體驗。

io。vavr

vavr

0。10。3

但無論提供瞭如何強大的api和程式設計方式,都扛不住小夥伴的濫用。這些程式碼,在邏輯上完全是說的通的,但就是看起來彆扭,維護起來費勁。

寫一堆垃圾lambda程式碼,是虐待同事最好的方式,也是埋坑的不二選擇。

寫程式碼嘛,就如同說話、聊天一樣。大家幹著同樣的工作,有的人說話好聽顏值又高,大家都喜歡和他聊天;有的人不好好說話,哪裡痛戳哪裡,雖然他存在著但大家都討厭。

程式碼,除了工作的意義,不過是我們在世界上表達自己想法的另一種方式罷了。如何寫好程式碼,不僅僅是個技術問題,更是一個意識問題。

作者簡介:小姐姐味道 (xjjdog),一個不允許程式設計師走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎新增好友,進一步交流。

推薦閱讀:

1。 玩轉Linux

2。 什麼味道專輯

3。 藍芽如夢

4。 殺機!

5。 失聯的架構師,只留下一段指令碼

6。 架構師寫的BUG,非比尋常

標簽: 程式碼  null  Stream  VO  Order