首頁>技術>

一個操作一行

可讀性對於業務程式碼來說十分重要,有些時候甚至可以犧牲一點效能。stream大部分時候都是鏈式操作,如果不注意分隔,加上有些函式可能還比較長,運算子比較多,會給閱讀者帶來很多麻煩。所以提倡在stream之後一個運算子一行,清晰明瞭。

//BadString s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1").parallel()        .filter(x -> x.startsWith("a")).collect(joining());//GoodString s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")        .parallel()        .filter(x -> x.startsWith("a"))        .collect(joining());
import static

這條還是和程式碼可讀性相關的。stream相關的方法可以靜態引入,從而減少一些冗長的類名,減少視覺干擾。

//Badstrings.stream()	.sorted(Comparator.reverseOrder())	.limit(10)	.collect(Collectors.toMap(Function.identity(), String::length));//Goodstrings.stream()	.sorted(reverseOrder())	.limit(10)	.collect(toMap(identity(), String::length));
方法引用優於lambda

方法引用說白了就是基於型別推斷的lambda表示式,寫法上有所區別。當一個函式的輸入和輸出都一致時(即方法簽名一致),可以簡化成方法引用的寫法,以String的length方法為例:

x -> x.length//等價於String::length

為什麼上一個例子中filter(x -> x.startsWith("a"))就用不了String::startsWith呢,因為這裡輸入的引數是x,而不是"a",無法達到判斷的效果。如果真想這麼寫也可以:

public static boolean startWithA(String x){    return x.startsWith("a");  }//這個類叫Test,這樣就可以使用方法引用了String s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1").parallel()        .filter(Test::startWithA).collect(joining());

回正題,為什麼推薦儘量使用方法引用呢?因為lambda表示式在編譯時會被翻譯成一個靜態方法:

private static Integer lambda$main$0(String s) {	return s.length();}

但方法引用只會對應一個invokedynamic的位元組碼命令,不會有額外的方法,處理效率比lambda表示式更高。

重用Stream

Stream是不可重用的,如果一個stream執行一個終端操作之後,再次執行的話就會報異常:stream has already been operated upon or closed。之前我們說過,stream不是一個數據結構,不儲存資料,一次性的。但如果你出於不想重複寫程式碼的考慮,真的想重用也不是沒有辦法。

Supplier<Stream<String>> streamSupplier =    () -> Stream.of("d2", "a2", "b1", "b3", "c")            .filter(s -> s.startsWith("a"));streamSupplier.get().anyMatch(s -> true);   // okstreamSupplier.get().noneMatch(s -> true);  // ok

放到一個Supplier中,每次呼叫get都會構建一個新的stream出來,雖然效率上沒有提升,但達到了Do not Repeat Youself的目的,程式碼更乾淨。

注意使用順序,先filter後處理

這是效率的問題,也是一個邏輯問題。肯定要先做過濾再做別的操作,不然所有的操作都會走一遍最後再過濾肯定相對較慢。如果有filter的需求,大部分就放在第一個(除非你需要過濾中間操作可能產生的null)。就不舉例子了。

注意使用Null Check

這一步可以有效地減少stream操作過程中的NPE,當你無法控制stream裡面都有什麼東西時,可以新增一步非空過濾,如下:

Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5, null)        .filter(Objects::nonNull)        .filter(x -> x > 1)        .reduce(Integer::sum);

這一步校驗的小技巧可以避免很多可能的異常,比較實用。

用不同的stream處理對應的原生型別

Stream支援了原生型別的各種stream,比如:IntStream LongStream DoubleStream。如果處理的流是原生型別的資料,優先選擇這些stream,這樣就免去拆箱的過程,效率更高。

//GoodOptionalInt reduce = IntStream.of(1, 2, 3, 4, 5)        .filter(x -> x > 1)        .reduce(Integer::sum);//Bad:注意這裡返回的 Optional<Integer>Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5)        .filter(x -> x > 1)        .reduce(Integer::sum);
異常處理

有時候在某個中間過程執行方法時,這個方法會向外拋一個受檢異常,這個異常你必須要處理,作為stream是可以支援在中間處理的,但程式碼可能就會變成這樣:

List<Class> classes =    Stream.of("java.lang.Object",              "java.lang.Integer",              "java.lang.String")          .map(className -> {            try {                return Class.forName(className);            }            catch (ClassNotFoundException e) {                // Ignore error                return null;            }        })        //這裡要過濾轉換失敗帶來的null,這裡的filter可以在寫後面        .filter(Objects::nonNull)        .collect(Collectors.toList());

用一個map來處理異常,程式碼的視覺汙染會十分嚴重,讓人看了就沒有去維護的慾望。但受檢異常必須要處理,怎麼辦呢:

Class toClass(String className) {    try {        return Class.forName(className);    }    catch (ClassNotFoundException e) {        return null;    }}List<Class> classes =    Stream.of("java.lang.Object",              "java.lang.Integer",              "java.lang.String")          .map(this::toClass)          .filter(Objects::nonNull)          .collect(Collectors.toList());

定義一個方法在外部,將受檢異常"吃掉"。既處理了異常,也讓程式碼變得可讀可維護。

debug技巧

stream的debug比較困難,它不像正常程式碼那樣執行一行是一行,像IDEA這種的工具可以提供debug工具,但也僅限於本地且按照了IDEA的情況下。有一個API卻可以感知執行的步驟,而且屬於中間操作,不像foreach那樣是一個終端操作。依然使用上面的例子:

Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5, null)        .filter(Objects::nonNull)        .filter(x -> x > 1)        .peek(System.out::println)        .reduce(Integer::sum);System.out.println(reduce.get());//output234514

peek接受一個Consumer作為引數,Consumer的方法簽名是接收一個值且返回void,什麼都不改變,什麼也不返回。雖然把用作debug有點不厚道,但真的很好用。

總結

stream最佳實踐系列已經全部更新完成,從stream的基礎開始,講了stream的常用API及其能力,最後總結了一些日常開發過程的技巧實踐。希望可以幫到正在閱讀的你,如有問題,也請斧正。

11
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 深入理解計算機系統-從幾個編碼細節看計算機資訊的表示和處理