Java8新特性-----Stream流

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循環的循環體纔是“做什麼”

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

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

  1. 將集合A根據條件一過濾爲子集B;
  2. 然後再根據條件二過濾爲子集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. 首先篩選所有姓張的人;
  2. 然後篩選名字有三個字的人;
  3. 最後進行對結果進行打印輸出。

每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的麼?不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。

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循環)依次進行以
下若干操作步驟:

  1. 第一個隊伍只要名字爲3個字的成員姓名;存儲到一個新集合中。
  2. 第一個隊伍篩選之後只要前3個人;存儲到一個新集合中。
  3. 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中。
  4. 第二個隊伍篩選之後不要前2個人;存儲到一個新集合中。
  5. 將兩個隊伍合併爲一個隊伍;存儲到一個新集合中。
  6. 根據姓名創建 Person 對象;存儲到一個新集合中。
  7. 打印整個隊伍的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=‘張二狗’}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章