JAVA8 之 Stream 流(四)

若是說前面幾章是函數式編程的方法論,那麼 Stream 流就應該是 JAVA8 爲咱們提供的最佳實踐。java

Stream 流的定義編程

Stream 是支持串行和並行操做的一系列元素。流操做會被組合到流管道中(Pipeline)中,一個流管道必須包含一個源(Source),這個源能夠是一個數組(Array),集合(Collection)或者 I/O Channel,會有一個或者多箇中間操做,中間操做的意思就是流與流的操做,流還會包含一個停止操做,這個停止操做會生成一個結果。設計模式

Stream 流的做用數組

以函數式編程的方式更好的操做集合。徹底依賴於函數式接口。在 java.util.stream 包中。app

流的建立方式dom

  • 使用數組的方式ide

    //第一種方式,使用 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
相關文章
相關標籤/搜索