本文中的部分示例基於如下場景:餐廳點菜,Dish為餐廳中可提供的菜品,Dish的定義如下:
1public class Dish {
2
3 private final String name;
4
5 private final boolean vegetarian;
6
7 private final int calories;
8
9 private final Type type;
10
11 public Dish(String name, boolean vegetarian, int calories, Type type) {
12 this.name = name;
13 this.vegetarian = vegetarian;
14 this.calories = calories;
15 this.type = type;
16 }
17
18 public enum Type { MEAT, FISH, OTHER }
19
20
21}
菜單的數據如下:
1List<Dish> menu = Arrays.asList(
2new Dish("pork", false, 800, Dish.Type.MEAT),
3new Dish("beef", false, 700, Dish.Type.MEAT),
4new Dish("chicken", false, 400, Dish.Type.MEAT),
5new Dish("french fries", true, 530, Dish.Type.OTHER),
6new Dish("rice", true, 350, Dish.Type.OTHER),
7new Dish("season fruit", true, 120, Dish.Type.OTHER),
8new Dish("pizza", true, 550, Dish.Type.OTHER),
9new Dish("prawns", false, 300, Dish.Type.FISH),
10new Dish("salmon", false, 450, Dish.Type.FISH) );
我們以一個簡單的示例來引入流:從菜單列表中,查找出是素食的菜品,並列印其菜品的名稱。
在Java8之前,我們通常是這樣實現該需求的:
1List<String> dishNames = new ArrayList<>();
2for(Dish d menu) {
3 if(d.isVegetarian()) {
4 dishNames.add(d.getName());
5 }
6}
7
8for(String n : dishNames) {
9 System.out.println(n);
10}
那在java8中,我們可以這樣寫:
1menu.streams() .filter( Dish::isVegetarian).map( Dish::getName) .forEach( a -> System.out.println(a) );
其運行輸出的結果:
在解釋上面的代碼之前,我們先對流做一個理論上的介紹。
流是什麼?流,就是數據流,是元素序列,在Java8中,流的接口定義在 java.util.stream.Stream包中,並且在Collection(集合)接口中新增一個方法:
1default Stream<E> stream() {
2 return StreamSupport.stream(spliterator(), false);
3}
流的簡短定義:從支持數據處理操作的源生成的元素序列。例如集合、數組都是支持數據操作的數據結構(容器),都可以做為流的創建源,該定義的核心要素如下:
源
流是從一個源創建來而來,而且這個源是支持數據處理的,例如集合、數組等。
元素序列
流代表一個元素序列(流水線),因為是從根據一個數據處理源而創建得來的。
數據處理操作
流的側重點並不在數據存儲,而在於數據處理,例如示例中的filter、map、forEach等。
迭代方式
流的迭代方式為內部迭代,而集合的迭代方式為外部迭代。例如我們遍歷Collection接口需要用戶去做迭代,例如for-each,然後在循環體中寫對應的處理代碼,這叫外部迭代。相反,Stream庫使用內部迭代,我們只需要對流傳入對應的函數即可,表示要做什麼就行。
注意:流和迭代器Iterator一樣,只能遍歷一次,如果要多次遍歷,請創建多個流。
接下來我們將重點先介紹流的常用操作方法。
流的常用操作filterfilter函數的方法聲明如下:
1java.util.stream.Stream#filter
2Stream<T> filter(Predicate<? super T> predicate);
該方法接收一個謂詞,返回一個流,即filter方法接收的lambda表達式需要滿足 ( T -> Boolean )。
示例:從菜單中選出所有是素食的菜品:
1List<Dish> vegetarianDishs = menu.stream().filter( Dish::isVegetarian )
2 .collect(toList());
溫馨提示:流的操作可以分成中間件操作和終端操作。中間操作通常的返回結果還是流,並且在調用終端操作之前,並不會立即調用,等終端方法調用後,中間操作才會真正觸發執行,該示例中的collect方法為終端方法。
我們類比一下資料庫查詢操作,除了基本的篩選動作外,還有去重,分頁等功能,那java8的流API能支持這些操作嗎?
答案當然是肯定。
distinct,類似於資料庫中的排重函數,就是對結果集去重。
例如有一個數值numArr = [1,5,8,6,5,2,6],現在要輸出該數值中的所有奇數並且不能重複輸出,那該如何實現呢?
1Arrays.stream(numArr).filter( a -> a % 2 == 0 ).distinict().forEach(System.out::println);
截斷流,返回一個i不超過指定元素個數的流。
還是以上例舉例,如果要輸出的元素是偶數,不能重複輸出,並且只輸出1個元素,那又該如何實現呢?
1Arrays.stream(numArr).filter( a -> a % 2 == 0 ).distinict().limit(1).forEach(System.out::println);
跳過指定元素,返回剩餘元素的流,與limit互補。
Map還是類比資料庫操作,我們通常可以只選擇一個表中的某一列,java8流操作也提供了類似的方法。
例如,我們需要從菜單中提取所有菜品的名稱,在java8中我們可以使用如下代碼實現:
1版本1:List<String> dishNames = menu.stream().map( (Dish d) -> d.getName() ).collect(Collectors.toList());
2版本2:List<String> dishNames = menu.stream().map( d -> d.getName() ).collect(Collectors.toList());
3版本3:List<String> dishNames = menu.stream().map(Dish::getName).collect(Collectors.toList());
文章的後續部分儘量使用最簡潔的lambda表達式。
我們來看一下Stream關於map方法的聲明:
1<R> Stream<R> map(Function<? super T, ? extends R> mapper)
2
接受一個函數Function,其函數聲明為:T -> R,接收一個T類型的對象,返回一個R類型的對象。
當然,java為了高效的處理基礎數據類型(避免裝箱、拆箱帶來性能損耗)也定義了如下方法:
1IntStream mapToInt(ToIntFunction<? super T> mapper)
2LongStream mapToLong(ToLongFunction<? super T> mapper)
3DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
思考題:對於字符數值["Hello","World"] ,輸出字符序列,並且去重。
第一次嘗試:
1public static void test_flat_map() {
2 String[] strArr = new String[] {"hello", "world"};
3 List<String> strList = Arrays.asList(strArr);
4 strList.stream().map( s -> s.split(""))
5 .distinct().forEach(System.out::println);
6}
輸出結果:
1public static void test_flat_map() {
2 String[] strArr = new String[] {"hello", "world"};
3 List<String> strList = Arrays.asList(strArr);
4 strList.stream().map( s -> s.split(""))
5 .map(Arrays::stream)
6 .distinct().forEach(System.out::println);
7}
其返回結果:
1public static void test_flat_map() {
2 String[] strArr = new String[] {"hello", "world"};
3 List<String> strList = Arrays.asList(strArr);
4 strList.stream().map( s -> s.split(""))
5 .map(Arrays::stream)
6 .forEach( (Stream<String> s) -> {
7 System.out.println("\n --start---");
8 s.forEach(a -> System.out.print(a + " "));
9 System.out.println("\n --end---");
10 } );
11}
綜合上述分析,之所以不符合預期,主要是原數組中的兩個字符,經過map後返回的是兩個獨立的流,那有什麼方法將這兩個流合併成一個流,然後再進行disinic去重呢?
答案當然是可以的,flatMap方法閃亮登場:先看代碼和顯示結果:
1public static void test_flat_map() {
2 String[] strArr = new String[] {"hello", "world"};
3 List<String> strList = Arrays.asList(strArr);
4 strList.stream().map( s -> s.split(""))
5 .flatMap(Arrays::stream)
6 .distinct().forEach( a -> System.out.print(a +" "));
7}
其輸出結果:
Stream API提供了allMatch、anyMatch、noneMatch、findFirst和findAny方法來實現對流中數據的匹配與查找。
allMatch我們先看一下該方法的聲明:
1boolean allMatch(Predicate<? super T> predicate);
接收一個謂詞函數(T->boolean),返回一個boolean值,是一個終端操作,用於判斷流中的所有元素是否與Predicate相匹配,只要其中一個元素不複合,該表達式將返回false。
示例如下:例如存在這樣一個List a,其中元素為 1,2,4,6,8。判斷流中的元素是否都是偶數。
1boolean result = a.stream().allMatch( a -> a % 2 == 0 );
該方法的函數聲明如下:
1boolean anyMatch(Predicate<? super T> predicate)
2
同樣接收一個謂詞Predicate( T -> boolean ),表示只要流中的元素至少一個匹配謂詞,即返回真。
示例如下:例如存在這樣一個List a,其中元素為 1,2,4,6,8。判斷流中的元素是否包含偶數。
1boolean result = a.stream().anyMatch( a -> a % 2 == 0 );
該方法的函數聲明如下:
1boolean noneMatch(Predicate<? super T> predicate);
同樣接收一個謂詞Predicate( T -> boolean ),表示只要流中的元素全部不匹配謂詞表達式,則返回true。
示例如下:例如存在這樣一個List a,其中元素為 2,4,6,8。判斷流中的所有元素都不式奇數。
1boolean result = a.stream().noneMatch( a -> a % 2 == 1 ); // 將返回true。
查找流中的一個元素,其函數聲明如下:
1Optional<T> findFirst();
返回流中的一個元素。其返回值為Optional,這是jdk8中引入的一個類,俗稱值容器類,其主要左右是用來避免值空指針,一種更加優雅的方式來處理null。該類的具體使用將在下一篇詳細介紹。
1public static void test_find_first(List<Dish> menu) {
2 Optional<Dish> dish = menu.stream().findFirst();
3
4 dish.ifPresent(a -> System.out.println(a.getName()));
5}
返回流中任意一個元素,其函數聲明如下:
1Optional<T> findAny();
reduce歸約,看過大數據的人用過會非常敏感,目前的java8的流操作是不是有點map-reduce的味道,歸約,就是對流中所有的元素進行統計分析,歸約成一個數值。
首先我們看一下reduce的函數說明:
1T reduce(T identity, BinaryOperator<T> accumulator)
1Optional<T> reduce(BinaryOperator<T> accumulator);
可以理解為沒有初始值的歸約,如果流為空,則會返回空,故其返回值使用了Optional類來優雅處理null值。
1<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
首先,最後的返回值類型為U。
U identity:累積函數的初始值。
BiFunction accumulator:累積器函數,對流中的元素使用該累積器進行歸約,在具體執行時accumulator.apply( identity, 第二個參數的類型不做限制 ),只要最終返回U即可。
BinaryOperator< U> combiner:組合器。對累積器的結果進行組合,因為歸約reduce,java流計算內部使用了fork-join框架,會對流的中的元素使用並行累積,每個線程處理流中一部分數據,最後對結果進行組合,得出最終的值。
溫馨提示:對流API的學習,一個最最重點的就是要掌握這些函數式編程接口,然後掌握如何使用Lambda表達式進行行為參數化(lambda表達當成參數傳入到函數中)。
接下來我們舉例來展示如何使用reduce。
示例1:對集合中的元素求和
1List<Integer> goodsNumber = Arrays.asList( 3, 5, 8, 4, 2, 13 );
2java7之前的示例:
3int sum = 0;
4for(Integer i : goodsNumber) {
5sum += i;
6}
7System.out.println("sum:" + sum);
求和運算符: c = a + b,也就是接受2個參數,返回一個值,並且這三個值的類型一致。
故我們可以使用T reduce(T identity, BinaryOperator< T> accumulator)來實現我們的需求:
1public static void test_reduce() {
2 List<Integer> goodsNumber = Arrays.asList( 3, 5, 8, 4, 2, 13 );
3 int sum = goodsNumber.stream().reduce(0, (a,b) -> a + b);
4
5
6 System.out.println(sum);
7}
不知大家是否只讀(a,b)這兩個參數的來源,其實第一個參數為初始值T identity,第二個參數為流中的元素。
那三個參數的reduce函數主要用在什麼場景下呢?接下來還是用求和的例子來展示其使用場景。在java多線程編程模型中,引入了fork-join框架,就是對一個大的任務進行先拆解,用多線程分別並行執行,最終再兩兩進行合併,得出最終的結果。reduce函數的第三個函數,就是組合這個動作,下面給出並行執行的流式處理示例代碼如下:
1 public static void test_reduce_combiner() {
2
3
4 List<Integer> nums = new ArrayList<>();
5 int s = 0;
6 for(int i = 0; i < 200; i ++) {
7 nums.add(i);
8 s = s + i;
9 }
10
11
12
13 int sum2 = nums.parallelStream().reduce(0,Integer::sum, Integer::sum);
14 System.out.println("和為:" + sum2);
15
16
17
18
19 AtomicInteger accumulatorCount = new AtomicInteger(0);
20
21
22 AtomicInteger combinerCount = new AtomicInteger(0);
23
24 int sum = nums.parallelStream().reduce(0,(a,b) -> {
25 accumulatorCount.incrementAndGet();
26 return a + b;
27 }, (c,d) -> {
28 combinerCount.incrementAndGet();
29 return c+d;
30 });
31
32 System.out.println("accumulatorCount:" + accumulatorCount.get());
33 System.out.println("combinerCountCount:" + combinerCount.get());
34}
從結果上可以看出,執行了100次累積動作,但只進行了15次合併。
流的基本操作就介紹到這裡,在此總結一下,目前接觸到的流操作:
1、filter
函數功能:過濾
操作類型:中間操作
返回類型:Stream
函數式接口:Predicate
函數描述符:T -> boolean
2、distinct
函數功能:去重
操作類型:中間操作
返回類型:Stream
3、skip
函數功能:跳過n個元素
操作類型:中間操作
返回類型:Stream
接受參數:long
4、limit
函數功能:截斷流,值返回前n個元素的流
操作類型:中間操作
返回類型:Stream
接受參數:long
5、map
函數功能:映射
操作類型:中間操作
返回類型:Stream
函數式接口:Function
函數描述符:T -> R
6、flatMap
函數功能:扁平化流,將多個流合併成一個流
操作類型:中間操作
返回類型:Stream
函數式接口:Function>
函數描述符:T -> Stream
7、sorted
函數功能:排序
操作類型:中間操作
返回類型:Stream
函數式接口:Comparator
函數描述符:(T,T) -> int
8、anyMatch
函數功能:流中任意一個匹配則返回true
操作類型:終端操作
返回類型:boolean
函數式接口:Predicate
函數描述符:T -> boolean
9、allMatch
函數功能:流中全部元素匹配則返回true
操作類型:終端操作
返回類型:boolean
函數式接口:Predicate
函數描述符:T -> boolean
10、 noneMatch
函數功能:流中所有元素都不匹配則返回true
操作類型:終端操作
返回類型:boolean
函數式接口:Predicate
函數描述符:T -> boolean
11、findAny
函數功能:從流中任意返回一個元素
操作類型:終端操作
返回類型:Optional
12、findFirst
函數功能:返回流中第一個元素
操作類型:終端操作
返回類型:Optional
13、forEach
函數功能:遍歷流
操作類型:終端操作
返回類型:void
函數式接口:Consumer
函數描述符:T -> void
14、collect
函數功能:將流進行轉換
操作類型:終端操作
返回類型:R
函數式接口:Collector
15、reduce
函數功能:規約流
操作類型:終端操作
返回類型:Optional
函數式接口:BinaryOperator
函數描述符:(T,T) -> T
16、count
函數功能:返回流中總元素個數
操作類型:終端操作
返回類型:long
由於篇幅的原因,流的基本計算就介紹到這裡了。