1. 程式人生 > >JAVA8 之 Stream 流(四)

JAVA8 之 Stream 流(四)

如果說前面幾章是函數語言程式設計的方法論,那麼 Stream 流就應該是 JAVA8 為我們提供的最佳實踐。

Stream 流的定義

Stream 是支援序列和並行操作的一系列元素。流操作會被組合到流管道中(Pipeline)中,一個流管道必須包含一個源(Source),這個源可以是一個數組(Array),集合(Collection)或者 I/O Channel,會有一個或者多箇中間操作,中間操作的意思就是流與流的操作,流還會包含一箇中止操作,這個中止操作會生成一個結果。

Stream 流的作用

以函數語言程式設計的方式更好的操作集合。完全依賴於函式式介面。在 java.util.stream 包中。

流的建立方式

  • 使用陣列的方式

    //第一種方式,使用 Stream.of 方法
    Stream stream1 = Stream.of("hello","world","hello world");
    
    String[] myArray = new String[]{"hello","world","hello world"};
    Stream stream2 = Stream.of(myArray);
    
    //第二種方式,使用 Arrays.stream()
    Stream stream3 = Arrays.stream(myArray);
  • 使用集合的方式

    Stream 的作用是以函數語言程式設計的方式操作集合,所以對於集合類,一定有更好更方便的方法去建立 Stream 流。

    List<String> list = Arrays.asList(myArray);
    Stream stream4 = list.stream();

    對於集合類,直接呼叫 stream 方法就可以獲得這個集合對應的 Stream 流。通過檢視原始碼我們發現這個方法直接定義在 Collection 介面中,並且是一個預設方法。所以所有 Collection 的子類都可以直接呼叫這個方法。這也是最為常用的方法。

    default Stream<E> stream() {
      return StreamSupport.stream(spliterator(), false);
    }
  • 使用檔案流(基本不會使用,簡單瞭解即可)

    下面是一個直接讀取檔案中的內容並且轉化為 Stream 流,最後輸出的過程。

    //檔案流
    private static Stream<String> fileStream(){
      Path path = Paths.get("C:\\Users\\abs\\a.txt");
      try(Stream<String> lines = Files.lines(path)){
        lines.forEach(System.out::println);
        return lines;
      }catch(IOException e){
        throw new RuntimeException(e);
      }
    }
  • 其他方式

    最後的方式是用於建立無限流,無限流的意思是如果你不加任何限制,流中的資料是無限。用於建立無限流的方法有 iterator 和 generate。

    generate 方法需要傳入一個 Supplier 型別的函式式介面,這個函式式介面用於產生無限流中所需要的資料。

    //全是數字 1 的無限流
    Stream.generate(()->1);
    //隨機數字的無限流
    Stream.generate(Math::random);

    iterator 方法需要傳入兩個引數,第一個給定一個初始值,第二個引數是一個函式式介面 UnaryOperator,這個函式式介面就是輸入和輸出相同的 Function 介面。

    Stream.iterate(0,n->n+1).limit(10).forEach(System.out::println);

    輸出結果:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9

    上面的 limit 是避免無限流一直產生,到達指定個數就停止。

Steam 流的優勢

下面我們通過一個簡單的例子來了解一下使用 Stream 流到底有哪些好處。等我們學完 Stream API 後會給大家提供更多的例子,讓大家真正瞭解它。

比如我們給定一個 List 集合,裡面放了很多數字,我們想要得到數字的平方然後求和。

以前的寫法:

List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);
int res = 0;
for(int i=0;i<l.size();i++){
  res += i*i;
}

使用 Stream 後的寫法只需要一行程式碼:

int r = l.stream().map(i->i+i).reduce(0,Integer::sum);

大家現在可能不明白 map 或者 reduce 的作用,我們稍後會詳細講解這一部分,這裡只是想讓大家看看區別,以及認識到 Stream 對於函數語言程式設計的使用和好處。

Stream 流的特性和原理

流不儲存值,通過管道的方式獲取值。對流的操作會生成一個結果,不過並不會修改底層的資料來源。集合可以作為流的底層資料來源,也可以通過 generate/iterator 方法來生成資料來源。

得到流之後,我們可以對流中的資料進行很多操作,比如過濾,對映,排序等等,處理完之後的資料可以再次被收集起來轉化為我們需要的資料型別。

從上面的圖我們可以看出,一個完成的流操作過程是包含兩種型別的,一個是中間操作,一個是終止操作。中間操作指的是過濾,排序和對映等中間處理過程的方法,終止操作指的是我們將流處理完畢後返回結果的操作,比如 collect,reduce 和 count 等等。

中間操作:一個流後面可以跟隨零個或者多箇中間操作。其目的只要是開啟流,做出某種程度的資料對映/過濾,然後返回一個新的流,交給下一個操作使用,這些操作都是延遲的,就是說僅僅呼叫到這些類的方法,並沒有真正開始流的遍歷。

終止操作:一個流只能有一個終止操作,當這個操作執行後,流就被使用光了,無法再被操作。所以這必定是流的最後一個操作。終止操作的執行,才會是真正開始流的遍歷,並且會生成一個結果。

還有一個重要的概念流是惰性的,在資料來源上的計算只有資料在被終止的時候才會被執行。也就是說所有的中間操作都是惰性求值,不遇到終止操作,中間操作的程式碼是不會執行的。

舉個例子:

我們對於一個數字結合進行一個 map 中間操作,將元素乘以 2,同時我們有一個 System.out 語句用於檢視程式碼是否執行了。

List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);        
l.stream().map(i->{
  i = i*2;
  System.out.println(i);
  return i;
});

最終的執行結果是什麼也沒列印。

那如果我們給他加一個終止操作那,結果如下:

中間操作:2
終止操作:2
中間操作:4
終止操作:4
中間操作:6
終止操作:6
中間操作:8
終止操作:8
中間操作:10
終止操作:10
中間操作:12
終止操作:12
中間操作:14
終止操作:14

這時中間操作和終止操作都執行了,這證明中間操作是惰性的。

還有一個需要注意的點就是,Stream 其實與 IO Stream 的概念是一致的,是不能重複使用的,關閉(執行終止操作後就關閉了)後也是不能使用的。

//用集合生成一個流並進行過濾,過濾後返回一個 stream s1。
List<Integer> l = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Stream s1 = l.stream().filter(item -> item > 2);

//因為 filter 是中間操作,流並沒有被關閉,所以還可以執行其他操作,distnict 是一個終止操作,執行完畢後流就關閉了
s1.distinct();

//流已經關閉了,再執行操作就會丟擲異常 
s1.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at com.paul.framework.chapter7.StreamCreate.main(StreamCreate.java:48)

Stream 流的 API 採用了建造者設計模式,這就意味著我們可以在一句程式碼中連續呼叫 Stream 的 API。

中間操作 API

  • filter

    顧名思義,filter 就是過濾的意思。引數需要我們傳入一個 Predicate 型別的函式式介面。不符合 Predicate 函式式介面的條件的流將被過濾出去。

    Stream<T> filter(Predicate<? super T> predicate);

    我們需要篩選出分數大於 60 分的學生:

    public static void main(String[] args) {
      List<Student> lists = new ArrayList<>();
      lists.add(new Student("wang",80,"Female"));
      lists.add(new Student("li",95,"Male"));
      lists.add(new Student("zhao",100,"FeMale"));
      lists.add(new Student("qian",54,"Male"));
    
      // filter 是一箇中間操作,返回過濾後的 Stream 流。forEach 是一個終止操作,對 filter 過濾之後的流進行處理。
      lists.stream().filter(s->s.getMark()>60).forEach((s)-> System.out.println(s.getName()));
    }
    
    //上一個例子我們對過濾後的流進行了列印操作,我們其實也可以把過濾後的流整理成一個集合
    List<Student> l = lists.stream().filter(s->s.getMark()>60).collect(Collectors.toList());
    l.forEach(s-> System.out.println(s.getName()));

    兩次列印的結果是相同的:

    //第一次列印的結果
    wang
    li
    zhao
    
    //第二次列印的結果
    wang
    li
    zhao

    filter 函式為我們提供了最為簡單的方法去過濾集合,避免了重複程式碼,邏輯也更易懂。

  • map

    通過流的方式對集合中的元素進行匹配操作。引數需要我們傳入一個 Function 型別的函式式介面。Function 型別的函式式介面需要一個輸入和一個輸出,對應對映之前需要的元素和對映之後得到的元素。

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

    比如集合中的元素是學生類,我們最終想要得到的結果是學生的分數,就可以使用 map 方法。

    List<Student> lists = new ArrayList<>();
    lists.add(new Student("wang",80,"Female"));
    lists.add(new Student("li",95,"Male"));
    lists.add(new Student("zhao",100,"FeMale"));
    lists.add(new Student("qian",54,"Male"));
    lists.stream().map(s->s.getMark()).collect(Collectors.toList()).forEach(System.out::println);

    將 list 轉換為 stream 後,通過 map 方法將學生類轉換成學生成績的 int 型別,然後通過 collect 方法將流轉換為集合,最後通過 forEach 方法將學生成績打印出來。

    列印的結果:

    80
    95
    100
    54

    比如我們想將集合中的字串轉換成大寫字母。

    List<String> list = Arrays.asList("hello","world","helloworld","test");
    list.stream().map(String::toUpperCase).collect(Collectors.toList()).forEach(System.out::println);

    map 方法裡我們通過方法引用(將字串轉為大寫的方法 String 類已經定義好了,所以我們直接使用方法引用,而不是寫一個匿名函式的 Lambda 表示式)將集合中的字串轉換成大寫,然後通過 collect 將流轉換為集合,最終使用 forEach 方法列印轉換後的字串。

    列印結果:

    HELLO
    WORLD
    HELLOWORLD
    TEST
  • mapTo*

    如果我們的 map 方法返回值是 int,long 或者 double 的話,我們可以直接使用 Stream API 為我們提供了 mapToInt,mapToLong,mapToDouble 方法。這幾個方法返回的是 IntStream,LongStream 和 DoubleStream。

    mapToInt, mapToLong 和 mapToDouble 是為了避免自動拆裝箱帶來的效能損耗。大家應該知道,像 int,long,double 這種基本資料型別是不能使用面向物件相關操作的,為此 Java 引入了自動拆裝箱的功能,能夠在需要使用面向物件的特性時幫我們將基本資料型別 int,long 和 double 轉換為 Integer,Long 和 Double 等包裝型別。在需要使用基本資料型別時(比如計算),又可以將包裝型別 Integer,Long 和 Double 轉換為基本資料型別 int,long 和 double。

    如果我們使用的不對,就會有一些自動拆裝箱的效能損耗。

    如果我們需要得到基本資料型別的結果,就可以使用 mapToInt, mapToLong 和 mapToDouble,這樣的到的是基本資料型別的流,可以方便我們進行計算等等操作。

    int sum = lists.stream().mapToInt(s->s.getMark()).sum();
    System.out.println(sum);
  • flatMap

    flat 的意思是扁平化,這個函式式的作用是將我們 map 之後的集合或者陣列等等元素打散成我們想要的元素。

    flatMap 方法需要返回一個 Stream 資料型別。T 是輸入的集合型別元素,R 是打散之後的元素型別,是 Stream 型別。

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    來看一個例子:

    比如我們的流中以前有三個 ArrayList,map 之後依然後會有三個 ArrayList,flatMap 會將三個 ArrayList 合併到一個 ArrayList 中。

    Stream<List<Integer>> stream = Stream.of(Arrays.asList(1),ArrayList.asList(2,3), ArrayList.asList(4,5,6));
    
    // 將 stream 裡面的每一個 list 再次轉化為 stream<Integer>,然後在進行 map 操作。
    stream.flatMap(theList->theList.stream()).map(item->item*item).forEach(System.out::println);

    這個例子中,List 代表打散之前的元素,Integer 代表我們打散之後的元素型別。

    在看另外一個例子,字串去重複。

    List<String> list = Arrays.asList("hello welcome","world hello","hello world hello","hello welcome");
    
    //錯誤的寫法, 這是對 String[] 的 distinct
    List<String[]> result = list.stream().map(item->item.split(" ")).distinct().collect(Collectors.toList());

    split 方法輸入的是字串,返回的是一個字串陣列,所以最後返回的是 String 陣列流 Stream<String[]>。

    我們使用 flatMap 將 String 陣列打散成 String。

    //正確的寫法,要用 flatmap 將 String[] 打散成 String
    List<String> result = list.stream().map(item->item.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());

  • flatMapTo*

    flatMapTo* 也有許多具體的實現實現,和 mapTo* 用法類似,這裡就不再贅述了。

  • limit

    limit 方法可以對流中需要返回的元素加以限制,因為流中元素的方法執行是嚴格按照順序進行的,limit 方法就相當於取前幾個元素。

    我們通過下面這個例子來了解 limit 和無限流。

    IntStream.iterate(0, i->(i+1)%2).distinct().limit(6).forEach(System.out::println);

    IntStream.iterate(0, i->(i+1)%2) 不斷產生 0,1,0,1,0,1..... 這樣的無限流,distinct 方法去除重複,limit 方法雖然限制流中只有 6 個元素,但是 distinct 方法先執行它會對無限流一致執行去復操作,所以方法永遠不會結束。這個 limit 在這裡也失去了作用。

    執行結果雖然只顯示了 0,1。但是方法一直不會結束。

    正確的寫法:

    IntStream.iterate(0, i->(i+1)%2).limit(6).distinct().forEach(System.out::println);

    先呼叫 limit 方法,限制流中只有 6 個元素,然後去重,結果列印 0,1。程式結束。

  • skip

    skip 方法和 limit 方法的用法類似,可以跳過流中的前幾個元素。

    IntStream.iterate(0, i->i+1).limit(10).skip(3).forEach(System.out::println);

    首先通過 iterate 和 limit 產生 10 Integer 個元素的流,通過 skip 跳過前三個。最終的結果如下:

    3
    4
    5
    6
    7
    8
    9
  • sort

    sort 方法有兩個實現,一個是不需要傳入引數的,另一個是需要我們傳入 Comparator。

    //根據自然順序排序
    Stream<T> sorted();
    //根據 Comparator 的規則進行排序
    Stream<T> sorted(Comparator<? super T> comparator);

    我們以前對集合排序時通常會使用 JDK 中 Collection 介面的 sort 方法:

    List<String> names = Arrays.asList("java8","lambda","method","class");
    
    //以前的寫法
    Collections.sort(names, new Comparator<String>() {
      @Override
      public int compare(String o1, String o2) {
        return o2.compareTo(o1);
      }
    });

    使用 Lambda 表示式對上面的寫法進行一下改進:

    Collections.sort(names,(o1,o2)-> o2.compareTo(o1));

    現在我們還可以使用 Stream API 中的 sorted 方法:

    names.stream().sorted().forEach(System.out::println);
    names.stream().sorted((o1,o2)-> o2.compareTo(o1)).forEach(System.out::println);

    結果:

    class
    java8
    lambda
    method