Java 之 Stream 流


Stream流

  Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端

一、傳統遍歷

  1、傳統集合的多步遍歷代碼

     幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。

     例如:

 1 import java.util.ArrayList;  2 import java.util.List;  3 
 4 public class DemoForEach {  5         public static void main(String[] args) {  6             List<String> list = new ArrayList<>();  7             list.add("張無忌");  8             list.add("周芷若");  9             list.add("趙敏"); 10             list.add("張強"); 11             list.add("張三豐"); 12             for (String name : list) { 13  System.out.println(name); 14  } 15  } 16 }

 

  2、循環遍歷的弊端

     Java 8Lambda讓我們可以更加專注於做什么What),而不是怎么做How),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發現

      •  for循環的語法就是“怎么做”;
      •    for循環的循環體才是“做什么”

      為什么使用循環?因為要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最后一個順次處理的循環。前者是目的,后者是方式。

      試想一下,如果希望對集合中的元素進行篩選過濾:

      ① 將集合A根據條件一過濾為子集B;

      ② 然后再根據條件二過濾為子集C;

     Java8 之前的做法:

 1 import java.util.ArrayList;  2 import java.util.List;  3     public class DemoNormalFilter {  4         public static void main(String[] args) {  5             List<String> list = new ArrayList<>();  6             list.add("張無忌");  7             list.add("周芷若");  8             list.add("趙敏");  9             list.add("張強"); 10             list.add("張三豐"); 11             List<String> zhangList = new ArrayList<>(); 12             for (String name : list) { 13                 if (name.startsWith("張")) { 14  zhangList.add(name); 15  } 16  } 17             List<String> shortList = new ArrayList<>(); 18             for (String name : zhangList) { 19                 if (name.length() == 3) { 20  shortList.add(name); 21  } 22  } 23             for (String name : shortList) { 24  System.out.println(name); 25  } 26  } 27     }

    這段代碼中含有三個循環,每一個作用不同: 

       ① 首先篩選出所有姓張的人;

       ② 然后篩選名字有三個字的人;

       ③ 最后進行對結果進行打印輸出。

     每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的么?

    不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。

  3、Stream 的更優寫法

      使用Java8的Stream API,代碼實現:

 1  import java.util.ArrayList;  2 import java.util.List;  3 
 4     public class DemoStreamFilter {  5         public static void main(String[] args) {  6             List<String> list = new ArrayList<>();  7             list.add("張無忌");  8             list.add("周芷若");  9             list.add("趙敏"); 10             list.add("張強"); 11             list.add("張三豐"); 12  list.stream() 13                     .filter(s -> s.startsWith("張")) 14                     .filter(s -> s.length() == 3) 15  .forEach(System.out::println); 16  } 17     } 

  直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度為3、逐一打印

  代碼中並沒有體現使用線性循環或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。

二、流式思想概述

  整體來看,流式思想類似於工廠車間的生產流水線”。

  當需要對多個元素進行操作(特別是多步操作)的時候,考慮到性能及便利性,應該首先拼好一個模型步驟方案,然后再按照方案去執行它。
  

 

   這張圖中展示了過濾、映射、跳過、計數等多步操作,這是一種集合元素的處理方案,而方案就是一種函數模型

    圖中的每一個方框都是一個,調用指定的方法,可以從一個流模型轉換為另一個流模型。而最右側的數字3是最終結果。

   這里的 filter map skip 都是在對函數模型進行操作,集合元素並沒有真正被處理。只有當終結方法 count執行的時候,整個模型才會按照指定策略執行操作。而這得益於Lambda的延遲執行特性 。

  注意“Stream其實是一個集合元素的函數模型,它並不是集合,也不是數據結構,其本身並不存儲任何元素(或其地址值)。 

  Stream(流)是一個來自數據源的元素隊列:

    •   元素是特定類型的對象,形成一個隊列。 Java中的Stream並不會存儲元素,而是按需計算。
    •   數據源 流的來源。 可以是集合,數組 等。

  和以前的Collection操作不同, Stream操作還有兩個基礎的特征:

    •  Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(fluentstyle)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)
    •    內部迭代: 以前對集合遍歷都是通過Iterator或者增強for的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式,流可以直接調用遍歷方法。

   當使用一個流的時候,通常包括三個基本步驟:

    獲取一個數據源(source數據轉換執行操作獲取想要的結果,

  每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。

三、獲取流

  java.util.stream.Stream<T> Java 8新加入的最常用的流接口。(這並不是一個函數式接口。)

  獲取一個流非常簡單,有以下幾種常用的方式:

    •  所有的 Collection 集合都可以通過 stream 默認方法獲取流;
    •    Stream 接口的靜態方法 of 可以獲取數組對應的流

  1、根據 Collection 獲取流

      java.util.Collection 接口中加入了default方法 stream 用來獲取流,所以其所有實現類均可獲取流。

      Demo:

 1 import java.util .*;  2 import java.util.stream.Stream;  3 
 4     public class DemoGetStream {  5         public static void main(String[] args) {  6             List<String> list = new ArrayList<>();  7             // ...
 8             Stream<String> stream1 = list.stream();  9             Set<String> set = new HashSet<>(); 10             // ...
11             Stream<String> stream2 = set.stream(); 12             Vector<String> vector = new Vector<>(); 13             // ...
14             Stream<String> stream3 = vector.stream(); 15  } 16     }

 

  2、根據 Map 獲取流

     java.util.Map 接口不是 Collection 的子接口,且其K-V數據結構不符合流元素的單一特征,所以獲取對應的流需要分keyvalueentry等情況:

     Demo:

 1 import java.util.HashMap;  2 import java.util.Map;  3 import java.util.stream.Stream;  4 
 5     public class DemoGetStream {  6         public static void main(String[] args) {  7             Map<String, String> map = new HashMap<>();  8             // ...
 9             Stream<String> keyStream = map.keySet().stream(); 10             Stream<String> valueStream = map.values().stream(); 11             Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); 12  } 13     }

 

  3、根據數組獲取流

      如果使用的不是集合或映射而是數組,由於數組對象不可能添加默認方法,所以 Stream 接口中提供了靜態方法of ,使用很簡單:

      Demo:

1  import java.util.stream.Stream; 2 
3     public class DemoGetStream { 4         public static void main(String[] args) { 5             String[] array = {"張無忌", "張翠山", "張三豐", "張一元"}; 6             Stream<String> stream = Stream.of(array); 7  } 8     }

   注意of 方法的參數其實是一個可變參數,所以支持數組。 

 

四、常用方法

  流模型的操作很豐富,這里介紹一些常用的API。這些方法可以被分成兩種:

    •  延遲方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其余方法均為延遲方法。)
    •    終結方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈式調用。本小節中,終結方法包括 count forEach 方法。

  1、逐一處理:forEach

    雖然方法名字叫 forEach ,但是與for循環中的“for-each”昵稱不同。

void forEach(Consumer<? super T> action);

    該方法接收一個 Consumer 接口函數,會將每一個流元素交給該函數進行處理。

   Consumer 接口

java.util.function.Consumer<T>接口是一個消費型接口。
Consumer接口中包含抽象方法void accept(T t),意為消費一個指定泛型的數據。

      基本使用:

1 import java.util.stream.Stream; 2   public class DemoStreamForEach { 3       public static void main(String[] args) { 4           Stream<String> stream = Stream.of("張無忌", "張三豐", "周芷若"); 5           stream.forEach(name‐> System.out.println(name)); 6     } 7   }

 

  2、過濾:filter

    可以通過 filter 方法將一個流轉換成另一個子集流。方法簽名:

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

    該接口接收一個 Predicate 函數式接口參數(可以是一個Lambda或方法引用)作為篩選條件。

    Predicate 接口:

boolean test(T t);

    該方法將會產生一個boolean值結果,代表指定的條件是否滿足。如果結果為true,那么Stream流的 filter 方法將會留用元素;如果結果為false,那么 filter 方法將會舍棄元素。

    基本使用:

1 import java.util.stream.Stream; 2     public class DemoStreamFilter { 3         public static void main(String[] args) { 4             Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5             Stream<String> result = original.filter(s ‐> s.startsWith("張")); 6  } 7     }

    在這里通過Lambda表達式來指定了篩選的條件:必須姓張

  3、映射:map

    如果需要將流中的元素映射到另一個流中,可以使用 map 方法。方法簽名:

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

    該接口需要一個 Function 函數式接口參數,可以將當前流中的T類型數據轉換為另一種R類型的流。

    Function 接口:

R apply(T t);

        這可以將一種T類型轉換成為R類型,而這種轉換的動作,就稱為映射

    基本使用:

1 import java.util.stream.Stream; 2     public class DemoStreamMap { 3     public static void main(String[] args) { 4       Stream<String> original = Stream.of("10", "12", "18"); 5       Stream<Integer> result = original.map(str‐>Integer.parseInt(str)); 6     } 7   }

 

    這段代碼中, map 方法的參數通過方法引用,將字符串類型轉換成為了int類型(並自動裝箱為 Integer 類對象)。

  4、統計個數:count

    正如舊集合 Collection 當中的 size 方法一樣,流提供 count 方法來數一數其中的元素個數:

long count();

    該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)。

    基本使用: 

1 import java.util.stream.Stream; 2   public class DemoStreamCount { 3     public static void main(String[] args) { 4       Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5       Stream<String> result = original.filter(s ‐> s.startsWith("張")); 6       System.out.println(result.count()); // 2
7     } 8   }

 

  5、取用前幾個:limit

    limit 方法可以對流進行截取,只取用前n個。方法簽名

Stream<T> limit(long maxSize);

    參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。

    基本使用:

1 import java.util.stream.Stream; 2     public class DemoStreamLimit { 3         public static void main(String[] args) { 4             Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5             Stream<String> result = original.limit(2); 6             System.out.println(result.count()); // 2
7     } 8   }

 

  6、跳過前幾個:skip

    如果希望跳過前幾個元素,可以使用 skip 方法獲取一個截取之后的新流:

Stream<T> skip(long n);

    如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度為0的空流。

    基本使用:

1 import java.util.stream.Stream; 2   public class DemoStreamSkip { 3     public static void main(String[] args) { 4       Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5       Stream<String> result = original.skip(2); 6       System.out.println(result.count()); // 1
7     } 8   }

 

  7、組合:concat

    如果有兩個流,希望合並成為一個流,那么可以使用 Stream 接口的靜態方法 concat : 

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

    注意:這是一個靜態方法,與 java.lang.String 當中的 concat 方法是不同的

    基本使用:

1 import java.util.stream.Stream; 2     public class DemoStreamConcat { 3       public static void main(String[] args) { 4         Stream<String> streamA = Stream.of("張無忌"); 5         Stream<String> streamB = Stream.of("張翠山"); 6         Stream<String> result = Stream.concat(streamA, streamB); 7       } 8     }

 

五、案例

  題目現在有兩個 ArrayList 集合存儲隊伍當中的多個成員姓名, 依次進行以下若干操作步驟:

1. 第一個隊伍只要名字為3個字的成員姓名;存儲到一個新集合中。
2. 第一個隊伍篩選之后只要前3個人;存儲到一個新集合中。
3. 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中。
4. 第二個隊伍篩選之后不要前2個人;存儲到一個新集合中。
5. 將兩個隊伍合並為一個隊伍;存儲到一個新集合中。
6. 根據姓名創建 Person 對象;存儲到一個新集合中。
7. 打印整個隊伍的Person對象信息。

  兩個隊伍(集合)的代碼如下:

 1 import java.util.ArrayList;  2 import java.util.List;  3     public class DemoArrayListNames {  4         public static void main(String[] args) {  5             //第一支隊伍
 6             ArrayList<String> one = new ArrayList<>();  7             one.add("迪麗熱巴");  8             one.add("宋遠橋");  9             one.add("蘇星河"); 10             one.add("石破天"); 11             one.add("石中玉"); 12             one.add("老子"); 13             one.add("庄子"); 14             one.add("洪七公"); 15             //第二支隊伍
16             ArrayList<String> two = new ArrayList<>(); 17             two.add("古力娜扎"); 18             two.add("張無忌"); 19             two.add("趙麗穎"); 20             two.add("張三豐"); 21             two.add("尼古拉斯趙四"); 22             two.add("張天愛"); 23             two.add("張二狗"); 24             // ....
25  } 26     }

  Person 類的代碼為:

 1 public class Person {  2     private String name;  3 
 4     public Person() {  5  }  6 
 7     public Person(String name) {  8         this.name = name;  9  } 10 
11  @Override 12     public String toString() { 13         return "Person{name='" + name + "'}"; 14  } 15 
16     public String getName() { 17         return name; 18  } 19 
20     public void setName(String name) { 21         this.name = name; 22  } 23 }

  方式一:使用傳統的for循環(或增強for循環)

  代碼實現:

 1 public class DemoArrayListNames {  2         public static void main(String[] args) {  3             List<String> one = new ArrayList<>();  4             // ...
 5             List<String> two = new ArrayList<>();  6             // ...  7             
 8             // 第一個隊伍只要名字為3個字的成員姓名;
 9             List<String> oneA = new ArrayList<>(); 10             for (String name : one) { 11                 if (name.length() == 3) { 12  oneA.add(name); 13  } 14  } 15             
16             // 第一個隊伍篩選之后只要前3個人;
17             List<String> oneB = new ArrayList<>(); 18             for (int i = 0; i < 3; i++) { 19  oneB.add(oneA.get(i)); 20  } 21             
22             // 第二個隊伍只要姓張的成員姓名;
23             List<String> twoA = new ArrayList<>(); 24             for (String name : two) { 25                 if (name.startsWith("張")) { 26  twoA.add(name); 27  } 28  } 29             
30             // 第二個隊伍篩選之后不要前2個人;
31             List<String> twoB = new ArrayList<>(); 32             for (int i = 2; i < twoA.size(); i++) { 33  twoB.add(twoA.get(i)); 34  } 35             
36             // 將兩個隊伍合並為一個隊伍;
37             List<String> totalNames = new ArrayList<>(); 38  totalNames.addAll(oneB); 39  totalNames.addAll(twoB); 40             // 根據姓名創建Person對象;
41             List<Person> totalPersonList = new ArrayList<>(); 42             for (String name : totalNames) { 43                 totalPersonList.add(new Person(name)); 44  } 45             // 打印整個隊伍的Person對象信息。
46             for (Person person : totalPersonList) { 47  System.out.println(person); 48  } 49  } 50     }

  方式二:使用 Stream 流式處理方式。

  代碼實現:

 1 import java.util.ArrayList;  2 import java.util.List;  3 import java.util.stream.Stream;  4     public class DemoStreamNames {  5         public static void main(String[] args) {  6             List<String> one = new ArrayList<>();  7             // ...
 8             List<String> two = new ArrayList<>();  9             // ... 10             // 第一個隊伍只要名字為3個字的成員姓名; 11             // 第一個隊伍篩選之后只要前3個人;
12             Stream<String> streamOne = one.stream().filter(s->s.length() == 3).limit(3); 13             
14             // 第二個隊伍只要姓張的成員姓名; 15             // 第二個隊伍篩選之后不要前2個人;
16             Stream<String> streamTwo = two.stream().filter(s->s.startsWith("張")).skip(2); 17             
18             // 將兩個隊伍合並為一個隊伍; 19             // 根據姓名創建Person對象; 20             // 打印整個隊伍的Person對象信息。
21             Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println); 22  } 23     }

 





 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM