一個操作一行
可讀性對於業務程式碼來說十分重要,有些時候甚至可以犧牲一點效能。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表示式更高。
重用StreamStream是不可重用的,如果一個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及其能力,最後總結了一些日常開發過程的技巧實踐。希望可以幫到正在閱讀的你,如有問題,也請斧正。