1. 引言
1.1 傳統集合的多步遍歷代碼
幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
for (String name : list) {
System.out.println(name);
}
}
這是一段非常簡單的集合遍歷操作:對集合中的每一個字符串都進行打印輸出操作。
1.2 循環遍歷的弊端
Java 8的Lambda讓我們可以更加專注於做什麼(What),而不是怎麼做(How),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發現:
- for循環的語法就是“怎麼做”
- for循環的循環體纔是“做什麼”
爲什麼使用循環?因爲要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最後一個順次處理的循環。前者是目的,後者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
- 將集合A根據條件一過濾爲子集B;
- 然後再根據條件二過濾爲子集C。
java8之前的做法:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("張")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
這段代碼中含有三個循環,每一個作用不同:
- 首先篩選所有姓張的人;
- 然後篩選名字有三個字的人;
- 最後進行對結果進行打印輸出。
每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的麼?不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。
1.3 Stream的更優寫法
下面來看一下藉助Java 8的Stream API,什麼才叫優雅:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
list.stream()
.filter(s ‐> s.startsWith("張"))
.filter(s ‐> s.length() == 3)
.forEach(System.out::println);
}
直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度爲3、逐一打印。代碼中並沒有體現使用線性循環或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。
2. 流式思想概述
請暫時忘記對傳統IO流的固有印象!
Stream(流)是一個來自數據源的元素隊列
- 元素是特定類型的對象,形成一個隊列。 Java中的Stream並不會存儲元素,而是按需計算。
- 數據源 流的來源。 可以是集合,數組 等。
和以前的Collection操作不同, Stream操作還有兩個基礎的特徵:
- Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(fluentstyle)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)。
- 內部迭代: 以前對集合遍歷都是通過Iterator或者增強for的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式,流可以直接調用遍歷方法。
當使用一個流的時候,通常包括三個基本步驟:獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結果,每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。
①Stream 自己不會存儲元素。
②Stream 不會改變源對象。相反,他們會返回一個持有結果的新Stream。
③Stream 操作是延遲執行的。這意味着他們會等到需要結果的時候才執行。
3. StreamAPI
Stream API ( java.util.stream) 把真正的函數式編程風格引入到Java中。這是目前爲止對Java類庫最好的補充,因爲Stream API可以極大提供Java程序員的生產力,讓程序員寫出高效率、乾淨、簡潔的代碼。
Stream 的操作三個步驟:
- 1- 創建 Stream
一個數據源(如:集合、數組),獲取一個流 - 2- 中間操作
一箇中間操作鏈,對數據源的數據進行處理 - 3- 終止操作(終端操作)
一旦執行終止操作,就執行中間操作鏈,併產生結果。之後,不會再被使用
3. 獲取 Stream流
獲取一個流非常簡單,有以下幾種常用的方式:
- 所有的 Collection 集合都可以通過 stream 默認方法獲取流;
- Stream 接口的靜態方法 of 可以獲取數組對應的流。
3.1 根據Collection獲取流
首先, java.util.Collection 接口中加入了default方法 stream 用來獲取流,所以其所有實現類均可獲取流。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();
// ...
Stream<String> stream3 = vector.stream();
}
3.2 根據Map獲取流
java.util.Map 接口不是 Collection 的子接口,且其K-V數據結構不符合流元素的單一特徵,所以獲取對應的流需要分key、value或entry等情況
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// ...
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
3.3 根據數組獲取流
如果使用的不是集合或映射而是數組,由於數組對象不可能添加默認方法,所以 Stream 接口中提供了靜態方法 of ,使用很簡單:
public static void main(String[] args) {
String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
Stream<String> stream = Stream.of(array);
}
of 方法的參數其實是一個可變參數,所以支持數組
4. Steam流中間操作
延遲方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈式調用。
4.1 過濾:filte
可以通過 filter 方法將一個流轉換成另一個子集流。
Stream<T> filter(Predicate<? super T> predicate);
- 1
該接口接收一個 Predicate 函數式接口參數(可以是一個Lambda或方法引用)作爲篩選條件。將會產生一個boolean值結果,代表指定的條件是否滿足。如果結果爲true,那麼Stream流的 filter 方法將會留用元素;如果結果爲false,那麼 filter 方法將會捨棄元素。
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.filter(s ‐> s.startsWith("張"));
}
在這裏通過Lambda表達式來指定了篩選的條件:必須姓張
4.2 映射:map
如果需要將流中的元素映射到另一個流中,可以使用 map 方法。
<R> Stream<R> map(Function<? super T, ? extends R> mapper
該接口需要一個 Function 函數式接口參數,可以將當前流中的T類型數據轉換爲另一種R類型的流。這可以將一種T類型轉換成爲R類型,而這種轉換的動作,就稱爲“映射”。
public static void main(String[] args) {
Stream<String> original = Stream.of("10", "12", "18");
Stream<Integer> result = original.map(str‐>Integer.parseInt(str));
}
4.3 取用前幾個:limit
limit 方法可以對流進行截取,只取用前n個。
Stream<T> limit(long maxSize);
參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
}
4.4 跳過前幾個:skip
如果希望跳過前幾個元素,可以使用 skip 方法獲取一個截取之後的新流:
Stream<T> skip(long n);
- 1
如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度爲0的空流。
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
}
4.5 組合:concat
如果有兩個流,希望合併成爲一個流,那麼可以使用 Stream 接口的靜態方法 concat
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
- 1
這是一個靜態方法,與 java.lang.String 當中的 concat 方法是不同的。
public static void main(String[] args) {
Stream<String> streamA = Stream.of("張無忌");
Stream<String> streamB = Stream.of("張翠山");
Stream<String> result = Stream.concat(streamA, streamB);
}
5. Steam流終止操作
終結方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈式調用。
5.1 逐一處理:forEach
void forEach(Consumer<? super T> action);
- 1
該方法接收一個 Consumer 接口函數,會將每一個流元素交給該函數進行處理。
public static void main(String[] args) {
Stream<String> stream = Stream.of("張無忌", "張三丰", "周芷若");
stream.forEach(name‐> System.out.println(name));
}
雖然方法名字叫 forEach ,但是與for循環中的“for-each”暱稱不同
5.2 統計個數:count
正如舊集合 Collection 當中的 size 方法一樣,流提供 count 方法來數一數其中的元素個數
long count();
- 1
該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.filter(s ‐> s.startsWith("張"));
System.out.println(result.count()); // 2
}
- 1
- 2
- 3
- 4
- 5
5. 集合元素處理
現在有兩個 ArrayList 集合存儲隊伍當中的多個成員姓名,要求使用傳統的for循環(或增強for循環)依次進行以
下若干操作步驟:
- 第一個隊伍只要名字爲3個字的成員姓名;存儲到一個新集合中。
- 第一個隊伍篩選之後只要前3個人;存儲到一個新集合中。
- 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中。
- 第二個隊伍篩選之後不要前2個人;存儲到一個新集合中。
- 將兩個隊伍合併爲一個隊伍;存儲到一個新集合中。
- 根據姓名創建 Person 對象;存儲到一個新集合中。
- 打印整個隊伍的Person對象信息。
兩個隊伍(集合)信息:
public static void main(String[] args) {
//第一支隊伍
ArrayList<String> one = new ArrayList<>();
one.add("迪麗熱巴");
one.add("宋遠橋");
one.add("蘇星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("莊子");
one.add("洪七公");
//第二支隊伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("趙麗穎");
two.add("張三丰");
two.add("尼古拉斯趙四");
two.add("張天愛");
two.add("張二狗");
// ....
}
Person 類:
public class Person {
private String name;
public Person() {}
public Person(String name) {
this.name = name;
}
// toString...
// getter、setter...
}
5.1 使用傳統的for循環寫法:
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一個隊伍只要名字爲3個字的成員姓名;
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}
// 第一個隊伍篩選之後只要前3個人;
List<String> oneB = new ArrayList<>();
for (int i = 0; i < 3; i++) {
oneB.add(oneA.get(i));
}
// 第二個隊伍只要姓張的成員姓名;
List<String> twoA = new ArrayList<>();
for (String name : two) {
if (name.startsWith("張")) {
twoA.add(name);
}
}
// 第二個隊伍篩選之後不要前2個人;
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {
twoB.add(twoA.get(i));
}
// 將兩個隊伍合併爲一個隊伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);
// 根據姓名創建Person對象;
List<Person> totalPersonList = new ArrayList<>();
for (String name : totalNames) {
totalPersonList.add(new Person(name));
}
// 打印整個隊伍的Person對象信息。
for (Person person : totalPersonList) {
System.out.println(person);
}
}
運行後結果:
Person{name=‘宋遠橋’}
Person{name=‘蘇星河’}
Person{name=‘石破天’}
Person{name=‘張天愛’}
Person{name=‘張二狗’}
5.2 集合元素處理(Stream方式)
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一個隊伍只要名字爲3個字的成員姓名;
// 第一個隊伍篩選之後只要前3個人;
Stream<String> streamOne = one.stream().filter(s ‐> s.length() == 3).limit(3);
// 第二個隊伍只要姓張的成員姓名;
// 第二個隊伍篩選之後不要前2個人;
Stream<String> streamTwo = two.stream().filter(s ‐> s.startsWith("張")).skip(2);
// 將兩個隊伍合併爲一個隊伍;
// 根據姓名創建Person對象;
// 打印整個隊伍的Person對象信息。
Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println);
}
}
運行效果完全一樣
Person{name=‘宋遠橋’}
Person{name=‘蘇星河’}
Person{name=‘石破天’}
Person{name=‘張天愛’}
Person{name=‘張二狗’}