前面介紹了lambda運算式,但是我們可以看到,lambda運算式其實也就是簡化了一部分代碼的編寫,說起來也不算是非常有用的語言特性。但是如果lambda運算式配合這篇文章介紹的流類庫,就會發揮出巨大的作用。 初識流類庫
老樣子,先來看一個例子。有一個整數列表,我現在希望找到其中所有大於5的數,所以我可能會這麼寫。雖然這是中規中矩的代碼,但是就算是實現這麼一個簡單的功能,也需要這麼一大坨代碼,實在是讓人不爽。
List<Integer> integers = new ArrayList<>();integers.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));//擷取大於5的 所有元素List<Integer> integersGreaterThan5 = new ArrayList<>();for (int i : integers) { if (i > 5) { integersGreaterThan5.add(i); }}System.out.println(integersGreaterThan5);
那麼如果配合流類庫呢。代碼會極大地縮減,而且可讀性也大大提高。
integersGreaterThan5.clear();//使用流類庫integersGreaterThan5 = integers.stream() .filter(i -> i > 5) .collect(Collectors.toList());System.out.println(integersGreaterThan5);
流類庫是Java 8新增的一組類庫,讓我們可以對集合類庫進行複雜的操作,這些類庫代碼位於java.util.stream包下,注意不要和Java IO流搞混了。從上面的代碼可以看到,使用流類庫基本上可以分為以下幾步:把集合轉換為流、對流進行操作、將流轉換為相應的資料結構。 擷取流
在支援查看原始碼的IDE中追蹤上面代碼的stream()方法,可以發現這個方法在java.util.Collection介面中,大部分集合類都實現了這個介面,這也意味著大多數集合類都有這個方法,利用這個方法,我們就可以將集合轉換為一個流,流類庫幾乎所有方法都需要在流上才能操作。
當然如果細究一下,這個方法長的是這個樣子。這也是Java 8的新特性,由於流類庫是在介面中添加的新方法,Java 8以前的代碼是沒有實現這些新方法的。為了老版本的代碼也可以正常運行,Java 8引入了介面預設方法,讓介面也可以實現方法,如果在實作類別中沒有實現,就會使用介面中的預設實現。這樣一來,即使老版本的代碼沒有實現這些新介面,程式也仍然可以正常工作。
default Stream<E> stream() { return StreamSupport.stream(spliterator(), false);}
流操作
過濾
最常見的流操作就是過濾了,過濾相當於SQL中的where語句,就是按某種條件把流中的元素篩選出來,組成一個新流。大部分流操作的結果仍然是一個流,所以我們可以鏈式調用。下面是一個找出大於3的偶數的例子。
List<Integer> integers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .filter(i -> i > 3) .filter(i -> i % 2 == 0) .collect(Collectors.toList());System.out.println(integers);
映射
另一種常見的流操作是映射,類似於SQL中的select,可以將一組元素轉換成另一種元素。下面的例子將一組整數轉換為平方。這是一個簡單的例子,實際場合中常常需要將一組物件流程轉換為另一組對象。
List<Integer> integers = Stream.of(1, 2, 3, 4, 5) .map(i -> i * i) .collect(Collectors.toList());System.out.println(integers);
平整映射
有時候需要將多個流的結果合并為一個流,這時候需要使用平整映射。
List<Integer> integers = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4)) .flatMap(n -> n.stream()) .collect(Collectors.toList());System.out.println(integers);
最大值和最小值
這兩個功能不必說了。需要注意的是,min和max方法接受的是比較子形式的lambda運算式,當然也可以用上篇文章介紹的比較子方法來簡化比較代碼的編寫。
int min = Stream.of(1, 2, 3, 4) .min(Comparator.comparingInt(i -> i)).get();int max = Stream.of(1, 2, 3, 4) .max(Comparator.comparingInt(i -> i)).get();System.out.println(String.format("max:%d,min:%d", max, min));
通用迭代
有時候需要進行一種比較複雜的操作:從一個流中取前兩個元素執行某個操作,然後用結果和第三個元素繼續操作,直到處理完所有元素。這種操作叫做reduce。下面的例子很簡單,求和以及求積。
reduce有兩種形式,第一種是取前兩個元素操作,然後將結果和第三個元素操作,然後以此類推。第二種是用給定的初始值和第一個元素操作,然後結果和第二個元素操作。需要注意第一種形式的傳回值是一個Optional對象,為了得到最終的值我們需要調用get()方法。
int sum = Stream.of(1, 2, 3, 4, 5) .reduce((acc, e) -> acc + e) .get();System.out.println(String.format("sum:%d", sum));int product = Stream.of(1, 2, 3, 4, 5) .reduce(1, (acc, e) -> acc * e);System.out.println(String.format("product:%d", product));
謂詞操作
還有一些流操作和SQL中的謂詞操作類似,可以實現一些判斷功能。allMatch當所有元素滿足條件時返回true;anyMatch只要有一個元素滿足就會返回真;noneMatch當沒有元素滿足條件時返回真;distinct會去除流中的重複元素。
boolean allGreaterThan5 = Stream.of(1, 2, 3, 4, 5) .allMatch(i -> i > 5);System.out.println("allGreaterThan5:" + allGreaterThan5);boolean anyEqualsTo2 = Stream.of(1, 2, 3, 4, 5) .anyMatch(i -> i == 2);System.out.println("anyEqualsTo2:" + anyEqualsTo2);boolean noneLessThan0 = Stream.of(1, 2, 3, 4) .noneMatch(i -> i < 0);System.out.println("noneLessThan0:" + noneLessThan0);List<Integer> distinct = Stream.of(1, 1, 2, 3, 2, 4, 5) .distinct() .collect(Collectors.toList());
還有一些操作可以截取流的一部分,limit會保留流的前幾個元素,而skip會跳過前幾個元素而擷取之後的所有元素。
String list1 = Stream.of(1, 2, 3, 4, 5) .limit(3) .map(String::valueOf) .collect(Collectors.joining(", ", "[", "]"));System.out.println(list1);String list2 = Stream.of(1, 2, 3, 4, 5) .skip(3) .map(String::valueOf) .collect(Collectors.joining(", ", "[", "]"));System.out.println(list2);
基本類型流
流類庫是一個通用的架構,所以顯而易見地用到了Java泛型的技術。但是我們知道由於Java存在一個基本類型裝箱拆箱的過程,所以會有效能開銷。為了避免這些開銷,流類庫針對常見的基本類型int、long、double做了特殊處理,為它們單獨準備了一些類和方法。
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);LongStream longStream = LongStream.of(1, 2, 3, 4);DoubleStream doubleStream = DoubleStream.of(1.0, 2.0);
對於一些方法也有基本類型的版本,可以將一個物件流程轉換為對應的基本類型,這些方法的命名規則是方法名+To+基本類型。如果需要處理大量資料的基本類型流,可以考慮使用這些方法。
int sum = Stream.of(1, 2, 3, 4) .mapToInt(i -> i) .reduce(0, (acc, e) -> acc + e);System.out.println(String.format("sum:%d", sum));
收集器
使用流類庫的最後一步就是將流轉換為我們需要的集合了,這就需要用到收集器。收集資料的最後一步需要調用collect方法,它的參數是java.util.stream.Collector類的靜態方法。聽起來是不是有點奇怪,實際上,接受的是這些方法的傳回值。例如toList()方法實際上是這樣的,返回的是Collector對象,然後由collect方法處理,擷取最後的集合。
public static <T>Collector<T, ?, List<T>> toList() { return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }, CH_ID);}
獲得集合
如果需要把流轉換為集合,可以使用toList()和toSet()等方法。它們會返回List和Set集合。如果需要使用特定的集合,可以使用toCollection方法,它的參數是一個比較特殊的函數介面,用於建立集合。所以這裡可以直接使用構造方法引用來建立集合。
List<Integer> integers = Stream.of(1, 2, 3, 4) .collect(Collectors.toList());Set<Integer> set = Stream.of(1, 2, 3) .collect(Collectors.toSet());//使用自己希望的集合ArrayList<Integer> integers2 = Stream.of(1, 2, 3) .collect(Collectors.toCollection(ArrayList::new));
獲得值
收集器不僅可以獲得集合,還可以由流擷取一個值,這可以通過調用maxBy、minBy、averageXXX和summingXXX方法來實現。下面是一組例子。
int max = Stream.of(1, 2, 3, 4) .collect(Collectors.maxBy(Comparator.comparing(i -> i))) .get();int min = Stream.of(1, 2, 3, 4) .collect(Collectors.minBy(Comparator.comparing(i -> i))) .get();double average = Stream.of(1, 2, 3, 4) .collect(Collectors.averagingDouble(Integer::doubleValue));int sum = Stream.of(1, 2, 3, 4) .collect(Collectors.summingInt(i -> i));System.out.println( String.format("max:%d,min:%d,average:%f,sum:%d", max, min, average, sum));
有時候需要將流的資料群組合為一個字串,這需要joining收集器,它的三個參數分別是分隔字元、首碼和尾碼。當然由於它需要字元序列,所以這裡還需要用map方法將整數流轉換為字串流。
String string = Stream.of(1, 2, 3, 4, 5) .map(String::valueOf) .collect(Collectors.joining(", ", "[", "]"));System.out.println(string);// 結果: [1, 2, 3, 4, 5]
還有一個簡單的收集器,作用就是計數,需要注意的是計數返回的結果是long類型就行了。這個收集器的主要作用是和其他收集器一起完成複雜的功能。
long count = Stream.of(1, 2, 3) .collect(Collectors.counting());System.out.println("count:" + count);// 結果:3
資料分塊
資料分塊允許你給定一個條件,然後收集器會按照這個條件將流分為滿足條件和不滿足條件的兩個部分,這個收集器的返回結果是一個Map<Boolean, List<T>>。下面的例子將流分為了奇數和偶數兩個部分。
Map<Boolean, List<Integer>> map = Stream.of(1, 2, 3, 4, 5) .collect(Collectors.partitioningBy(i -> i % 2 == 0));System.out.println(map);// 結果// {false=[1, 3, 5], true=[2, 4]}
資料分組
資料分塊只能分為真假兩種情況,如果需要更細分的話,需要使用資料分組。這個大概類似於SQL中的group by語句。下面的例子將流按照數組個位元分為好幾組。
Map<Integer, List<Integer>> map = Stream.of(21, 32, 43, 54, 11, 33, 22) .collect(Collectors.groupingBy(i -> i % 10));System.out.println(map);// 結果// {1=[21, 11], 2=[32, 22], 3=[43, 33], 4=[54]}
組合收集器
如果問題比較複雜,還可以將多個收集器組合起來使用,一些收集器有重載的版本,支援第二個收集器,可以用來實現這個功能。就拿前面那個資料分組的例子來說,這次我不僅要分組,而且只需要每組的十位元字,那麼就可以這樣寫。groupingBy的第二個參數可以使用mapping收集器,mapping這裡的作用和流操作的map類似,將流再次進行映射,然後收集結果作為最後Map的索引值對。
//按個位元字分組,然後只擷取十位元字Map<Integer, List<Integer>> map = Stream.of(21, 32, 43, 54, 11, 33, 22) .collect(Collectors.groupingBy(i -> i % 10, Collectors.mapping(i -> i / 10, Collectors.toList())));System.out.println(map);// {1=[2, 1], 2=[3, 2], 3=[4, 3], 4=[5]}
再舉一個例子,現在我希望對每組的結果進行求值。就可以使用另外一個收集器summingXXX。還有一些收集器可以完成求平均數等的操作,這裡就不一一列舉了。
//按個位元字分組,然後求各組的和Map<Integer, Integer> map2 = Stream.of(21, 32, 43, 54, 11, 33, 22) .collect(Collectors.groupingBy(i -> i % 10, Collectors.summingInt(i -> i)));System.out.println(map2);// {1=32, 2=54, 3=76, 4=54}
另外一點內容
還有一點知識點不知道該怎麼說,乾脆都放在這裡好了。 惰性求值
流類庫設計的非常精巧,也對效能做了很多最佳化。所有的流操作都是惰性的,也就是說直到最後調用收集器的時候,整個流操作才開始進行。在這之前,你的流操作只不過類似於SQL的執行計畫,這時候還沒有真正執行程式。所以下面的代碼什麼都不會輸出。
Stream.of(1, 2, 3, 4, 5) .filter(i -> i > 1) .filter(i -> { System.out.print(i); return i <= 3; });
單次迴圈
如果進行了很多流操作,流類庫會不會對流進行多次迭代,導致程式速度很慢呢。這個擔心也是多餘的,流類庫經過最佳化,會保證迭代以最少的次數進行。所以下面的代碼輸出結果是122334455,這說明流只迭代了一次。
List<Integer> integers = Stream.of(1, 2, 3, 4, 5) .filter(i -> { System.out.print(i); return i > 1; }) .filter(i -> { System.out.print(i); return i <= 3; }) .collect(Collectors.toList());System.out.println();
新的for迴圈
原來,我們如果需要進行一定次數的迴圈,需要使用for來做到。
//傳統for迴圈for (int i = 0; i < 3; ++i) { System.out.print(i);}System.out.println();
現在利用流類庫的range方法,我們可以簡化這一點。
IntStream.range(0, 3) .forEach(i -> System.out.print(i));System.out.println();
資料並行化
由於流類庫的特性,所以讓流;並行化非常容易,只需要調用parrallel方法即可。資料的分割、結果的合并都會由類庫自動完成。
List<Integer> integers = IntStream.range(1, 101) .parallel() .filter(i -> i % 2 == 0) .boxed() .collect(Collectors.toList());
需要注意並不是說並行化之後,速度就一定會比序列化快,這需要根據當前系統、機器、執行的資料流大小來進行綜合評估。ArrayList這類容器就比較容易並行化,而HashMap並行化就比較困難。
總之,並行化這個主題比較複雜,這裡就不詳細討論了。
最後推薦一本關於Java函數式編程的書籍,這本書對於Java 8的函數式編程做了很多介紹,我覺得很不錯。