java8實戰讀書筆記:初識Stream、流的基本操做(流計算)

本文是博主在學習《java8實戰》的一些學習筆記。java

從本節開始,將進入到java8 Stream(流)的學習中來。數據庫

本文中的部分示例基於以下場景:餐廳點菜,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    // 省略set get方法
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中,咱們能夠這樣寫:app

1menu.streams() .filter( Dish::isVegetarian).map( Dish::getName) .forEach( a -> System.out.println(a) );

其運行輸出的結果:框架

java8實戰讀書筆記:初識Stream、流的基本操做(流計算)

怎麼樣,神奇吧!!!

在解釋上面的代碼以前,咱們先對流作一個理論上的介紹。運維

流是什麼?ide

流,就是數據流,是元素序列,在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同樣,只能遍歷一次,若是要屢次遍歷,請建立多個流。

接下來咱們將重點先介紹流的經常使用操做方法。

流的經常使用操做

filter

filter函數的方法聲明以下:

1java.util.stream.Stream#filter
2Stream<T> filter(Predicate<? super T> predicate);

該方法接收一個謂詞,返回一個流,即filter方法接收的lambda表達式須要知足 ( T -> Boolean )。

示例:從菜單中選出全部是素食的菜品:

1List<Dish> vegetarianDishs = menu.stream().filter(  Dish::isVegetarian )    // 使用filter過濾流中的菜品。
2                                          .collect(toList());              // 將流轉換成List,該方法將在後面介紹。

舒適提示:流的操做能夠分紅中間件操做和終端操做。中間操做一般的返回結果仍是流,而且在調用終端操做以前,並不會當即調用,等終端方法調用後,中間操做纔會真正觸發執行,該示例中的collect方法爲終端方法。

咱們類比一下數據庫查詢操做,除了基本的篩選動做外,還有去重,分頁等功能,那java8的流API能支持這些操做嗎?
答案固然是確定。

distinct

distinct,相似於數據庫中的排重函數,就是對結果集去重。
例若有一個數值numArr = [1,5,8,6,5,2,6],如今要輸出該數值中的全部奇數而且不能重複輸出,那該如何實現呢?

1Arrays.stream(numArr).filter(  a -> a % 2 == 0 ).distinict().forEach(System.out::println);

limit

截斷流,返回一個i不超過指定元素個數的流。
仍是以上例舉例,若是要輸出的元素是偶數,不能重複輸出,而且只輸出1個元素,那又該如何實現呢?

1Arrays.stream(numArr).filter(  a -> a % 2 == 0 ).distinict().limit(1).forEach(System.out::println);

skip

跳過指定元素,返回剩餘元素的流,與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}

輸出結果:

java8實戰讀書筆記:初識Stream、流的基本操做(流計算)

爲何會返回兩個String[]元素呢?由於map(s -> s.split()) 此時返回的流爲Stream,那咱們是否是能夠繼續對該Steam[String[]],把String[]轉換爲字符流,其代碼以下:

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}

其返回結果:

java8實戰讀書筆記:初識Stream、流的基本操做(流計算)

仍是不符合預期,其實緣由也很好理解,再次通過map(Arrays:stream)後,返回的結果爲Stream,即包含兩個元素,每個元素爲一個字符流,能夠經過以下代碼驗證:

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}

其輸出結果:

java8實戰讀書筆記:初識Stream、流的基本操做(流計算)

符合預期。一言以蔽之,flatMap能夠把兩個流合併成一個流進行操做。

查找和匹配

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 );  // 將
返回false。

anyMatch

該方法的函數聲明以下:

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 );  // 將返回true。

noneMatch

該方法的函數聲明以下:

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。

findFirst

查找流中的一個元素,其函數聲明以下:

1Optional<T> findFirst();
返回流中的一個元素。其返回值爲Optional,這是jdk8中引入的一個類,俗稱值容器類,其主要左右是用來避免值空指針,一種更加優雅的方式來處理null。該類的具體使用將在下一篇詳細介紹。

1public static void test_find_first(List<Dish> menu) {
2    Optional<Dish> dish = menu.stream().findFirst();
3    // 這個方法表示,Optional中包含Dish對象,則執行裏面的代碼,不然什麼事不幹,是否是比判斷是否爲null更友好
4    dish.ifPresent(a -> System.out.println(a.getName()));  
5}

findAny

返回流中任意一個元素,其函數聲明以下:

1Optional<T> findAny();

reduce

reduce歸約,看過大數據的人用過會很是敏感,目前的java8的流操做是否是有點map-reduce的味道,歸約,就是對流中全部的元素進行統計分析,歸約成一個數值。
首先咱們看一下reduce的函數說明:

1T reduce(T identity, BinaryOperator<T> accumulator)
  • T identity:累積器的初始值。
  • BinaryOperator< T> accumulator:累積函數。BinaryOperator< T> extend BiFunction。BinaryOperator的函數式表示,接受兩個T類型的入參,返回T類型的返回值。
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;//  sum = sum + 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    // int sum = goodsNumber.stream().reduce(0, Integer::sum);
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    // 對流進行歸併,求和,這裏使用了流的並行執行版本 parallelStream,內部使用Fork-Join框架多線程並行執行,
12    // 關於流的內部高級特性,後續再進行深刻,目前先以掌握其用法爲主。
13    int sum2 = nums.parallelStream().reduce(0,Integer::sum, Integer::sum);
14    System.out.println("和爲:" + sum2);
15
16    // 下面給出上述版本的debug版本。
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次合併。

流的基本操做就介紹到這裏,在此總結一下,目前接觸到的流操做:

一、filter

  • 函數功能:過濾
  • 操做類型:中間操做
  • 返回類型:Stream
  • 函數式接口:Predicate
  • 函數描述符:T -> boolean
    二、distinct

  • 函數功能:去重
  • 操做類型:中間操做
  • 返回類型:Stream
    三、skip

  • 函數功能:跳過n個元素
  • 操做類型:中間操做
  • 返回類型:Stream
  • 接受參數:long
    四、limit

  • 函數功能:截斷流,值返回前n個元素的流
  • 操做類型:中間操做
  • 返回類型:Stream
  • 接受參數:long
    五、map

  • 函數功能:映射
  • 操做類型:中間操做
  • 返回類型:Stream
  • 函數式接口:Function
  • 函數描述符:T -> R

六、flatMap

  • 函數功能:扁平化流,將多個流合併成一個流
  • 操做類型:中間操做
  • 返回類型:Stream
  • 函數式接口:Function>
  • 函數描述符:T -> Stream
    七、sorted
  • 函數功能:排序
  • 操做類型:中間操做
  • 返回類型:Stream
  • 函數式接口:Comparator
  • 函數描述符:(T,T) -> int
    八、anyMatch
  • 函數功能:流中任意一個匹配則返回true
  • 操做類型:終端操做
  • 返回類型:boolean
  • 函數式接口:Predicate
  • 函數描述符:T -> boolean
    九、allMatch
  • 函數功能:流中所有元素匹配則返回true
  • 操做類型:終端操做
  • 返回類型:boolean
  • 函數式接口:Predicate
  • 函數描述符:T -> boolean
    十、 noneMatch
  • 函數功能:流中全部元素都不匹配則返回true
  • 操做類型:終端操做
  • 返回類型:boolean
  • 函數式接口:Predicate
  • 函數描述符:T -> boolean
    十一、findAny
  • 函數功能:從流中任意返回一個元素
  • 操做類型:終端操做
  • 返回類型:Optional
    十二、findFirst
  • 函數功能:返回流中第一個元素
  • 操做類型:終端操做
  • 返回類型:Optional
    1三、forEach
  • 函數功能:遍歷流
  • 操做類型:終端操做
  • 返回類型:void
  • 函數式接口:Consumer
  • 函數描述符:T -> void
    1四、collect
  • 函數功能:將流進行轉換
  • 操做類型:終端操做
  • 返回類型:R
  • 函數式接口:Collector
    1五、reduce

  • 函數功能:規約流
  • 操做類型:終端操做
  • 返回類型:Optional
  • 函數式接口:BinaryOperator
  • 函數描述符:(T,T) -> T
    1六、count
  • 函數功能:返回流中總元素個數
  • 操做類型:終端操做
  • 返回類型:long
    因爲篇幅的緣由,流的基本計算就介紹到這裏了,下文還將重點介紹流的建立,數值流與Optional類的使用。

更多文章請關注公衆號:

java8實戰讀書筆記:初識Stream、流的基本操做(流計算)
一波廣告來襲,做者新書《RocketMQ技術內幕》已出版上市:
java8實戰讀書筆記:初識Stream、流的基本操做(流計算)

《RocketMQ技術內幕》已出版上市,目前可在主流購物平臺(京東、天貓等)購買,本書從源碼角度深度分析了RocketMQ NameServer、消息發送、消息存儲、消息消費、消息過濾、主從同步HA、事務消息;在實戰篇重點介紹了RocketMQ運維管理界面與當前支持的39個運維命令;並在附錄部分羅列了RocketMQ幾乎全部的配置參數。本書獲得了RocketMQ創始人、阿里巴巴Messaging開源技術負責人、Linux OpenMessaging 主席的高度承認並做序推薦。目前是國內第一本成體系剖析RocketMQ的書籍。

相關文章
相關標籤/搜索