天天看點

java8 — Stream篇1. Stream産生的背景2. 傳統方式的不足3. 什麼是Stream4. Stream流的特點5. Stream流的操作種類6. Stream流的操作過程7. Stream API 詳解與使用8. 總結

1. Stream産生的背景

Stream 作為 Java 8 的一功能強大的新特性,它與 java I/O裡的 InputStream 和 OutputStream 是完全不同的概念。Java 8 中的 Stream 是對集合(Collection)對象功能的增強,它專注于對集合對象進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量資料操作 (bulk data operation)。Stream API 借助于同樣新出現與java8中的 Lambda 表達式,極大的提高程式設計效率和程式可讀性。同時它提供串行和并行兩種模式進行彙聚操作,并發模式能夠充分利用多核處理器的優勢,使用 fork/join 并行方式來拆分任務和加速處理過程。通常編寫并行代碼很難而且容易出錯, 但使用 Stream API 無需編寫一行多線程的代碼,就可以很友善地寫出高性能的并發程式。是以說,Java 8 中首次出現的 java.util.stream 是一個函數式語言+多核時代綜合影響的産物。

2. 傳統方式的不足

在java8以前,java對于某些常用的功能或需求的處理方式要麼很繁瑣、不高效,要麼要依賴資料庫的操作(如某些聚合操作),如以下場景需求:

在一個批量資料中:

  • 求出每月、每周、每日平均值等
  • 求出最大值
  • 取出n個樣本
  • 排除無效或不關心的某些資料

等等操作,對于以上操作,如果使用java代碼處理,是極其繁瑣的,笨拙的,要麼就得依賴借助與資料庫的聚合操作以快速得到結果。但在當今這個資料大爆炸的時代,在資料來源多樣化、資料海量化的今天,很多時候不得不脫離 RDBMS,或者以底層傳回的資料為基礎進行更上層的資料統計。

試舉例:假設有一個商品資料集合,要對手機類型的商品進行一個統計分析,計算出銷售量最高的手機品牌。

商品實體類:Goods

public class Goods {
	
	private String name;
	
	//假設1代表手機類别
	private Integer type;
	
	private Integer brand;
	
	private Integer sellCount;
	
	//constructor、getter、sertter省略
}
           

java8以前的處理方式

//原始商品資料集合,此處僅模拟代碼邏輯,不填充資料
List<Goods> goods = new ArrayList<>();
//周遊商品集合資料,篩選出手機類型資料
List<Goods> phones = new ArrayList<>();
for (Goods g : goods) {
	if (g.getType() == 1) {
		phones.add(g);
	}
}
//對手機商品集合進行排序,選出銷售量最大的那個
phones.sort(new Comparator<Goods>() {

	@Override
	public int compare(Goods g1, Goods g2) {
		return g1.getSellCount() - g2.getSellCount();
	}
			
});
		
//然後從排好序中的資料取出最大值即可
           

java8使用Stream的處理方式

Optional<Goods> maxSellGood = goods.stream()
			.filter((g) -> {return g.getType() == 1;})
			.max((g1, g2) -> {
				return g1.getSellCount() - g2.getSellCount();
			});
		//直接得出銷售量最大的
		Goods phone = maxSellGood.get();
           

會明顯發現Stream所帶來的高效與簡潔,并且性能極好,接下來就來認識一下什麼是Stream!

3. 什麼是Stream

Stream的英文翻譯是“流”,是的,正如字面意思一樣,Stream就是一種流操作的概念。它不是集合元素,也不是資料結構并且不儲存資料,它是有關算法和計算的,它更像一個進階版本的 Iterator。原始版本的 Iterator,使用者隻能顯式地一個一個周遊元素并對其執行某些操作;而進階版本的 Stream,使用者隻要給出對元素集合的操作指令,比如 “過濾掉長度大于 10 的字元串”、“擷取每個字元串的首字母”等,Stream 會隐式地在内部進行周遊,做出相應的資料轉換。

Stream 就如同一個疊代器(Iterator),單向,不可往複,資料隻能周遊一次,周遊過一次後即用盡了,就好比流水從面前流過,一去不複返。

Stream可以通過下圖來簡易了解:

java8 — Stream篇1. Stream産生的背景2. 傳統方式的不足3. 什麼是Stream4. Stream流的特點5. Stream流的操作種類6. Stream流的操作過程7. Stream API 詳解與使用8. 總結

而和疊代器又不同的是,Stream 可以并行化操作,疊代器隻能指令式地、串行化操作。顧名思義,當使用串行方式去周遊時,每個 item 讀完後再讀下一個 item。而使用并行去周遊時,資料會被分成多個段,其中每一個都在不同的線程中處理,然後将結果一起輸出。Stream 的并行操作依賴于 Java7 中引入的 Fork/Join 架構(JSR166y)來拆分任務和加速處理過程。

4. Stream流的特點

1. 單向,不可往複,資料隻能周遊一次;

2. 采用内部疊代的方式(即處理過程有流自行完成);

3. 不修改也不影響原始資料(這一點其實很重要,Stream是将原始資料拷貝并轉換為流,并不是直接對原始資料進行操作,這就保證了原始資料的安全性與完整性);

5. Stream流的操作種類

流的操作分為兩種,分别為中間操作 和 終止操作。

1. 中間操作

當資料源中的資料上了流水線後,這個過程對資料進行的所有操作都稱為“中間操作”。 

中間操作仍然會傳回一個流對象,是以多個中間操作可以串連起來形成一個流水線。

2. 終止操作

當所有的中間操作完成後,若要将資料從流水線上拿下來,則需要執行終止操作。 

終止操作将傳回一個執行結果,這就是你想要的資料( 終止操作時一次性全部處理,稱為“惰性求值”)。

6. Stream流的操作過程

使用Stream需要三步:

1. 準備資料源(集合或數組),轉為Stream流對象;

2. 執行中間操作

中間操作可以有多個,多個中間操作串起來就形成了一葛流水線操作;

3. 執行終止操作

終止操作後,本次流處理結束,你将獲得一個執行結果。

7. Stream API 詳解與使用

7.1 常用的幾種建立Stream流的方式

1. 使用集合接口Collection 接口提供的stream建立串行流(這裡不講并行流)

List<String> list = Arrays.asList("Jim", "Tom", "Sam", "Kaven");
Stream<String> stream = list.stream();
           

2. 使用Arrays提供的stream方法,以數組的形式建立

String[] strings = {"Jim", "Tom", "Sam", "Kaven"};
Stream<String> stream2 = Arrays.stream(strings);
           

3. 可以使用靜态方法 Stream.of(), 通過顯示值建立一個流。它可以接收任意數量的參數。

Stream<String> stream3 = Stream.of("Jim", "Tom", "Sam", "Kaven");
           

7.2 終止操作

要想得到結果,終止操作必不可少,是以本文先講解終止操作,再結合終止操作和中間操作來講解中間操作。

而終止操作有分為以下幾種:

  • 查找
  • 比對
  • 收集
  • 歸約

(1) 查找

 終止操作(查找)之  -- void forEach(Consumer<? super T> action);

 解釋 :    内部疊代( 用 使用  Collection  接口需要使用者去做疊

代,稱為 外部疊代 。相反, Stream API  使用内部

疊代 )

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
list.stream()
    .forEach((e) -> {System.out.println(e);});
           

 終止操作之(查找) -- Optional<T> min(Comparator<? super T> comparator);

 解釋 : 傳回流中最小值

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
	.min((e1, e2) -> {return e1 - e2;})
	.get();
System.out.println(result);
           

 終止操作之(查找) -- Optional<T> max(Comparator<? super T> comparator);

 解釋 : 傳回流中最大值

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
	.max((e1, e2) -> {return e1 - e2;})
	.get();
System.out.println(result);
           

 終止操作之(查找) -- long count();

 解釋: 傳回流中資料總數

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
long result = list.stream()
	.count();
System.out.println(result);
           

 終止操作之(查找) -- Optional<T> findAny();

 解釋 : 傳回目前流中的任意元素

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
	.findAny()
	.get();
System.out.println(result);
           

 終止操作(查找) -- Optional<T> findFirst();

 解釋 : 傳回目前流中第一個元素

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
	.findFirst()
	.get();
System.out.println(result);
           

注:在此有關查找隻舉例幾個典型的案例,其他的還有很多,但都是類似的變形和用法,讀者可在使用時檢視相關API文檔或者源碼即可。

(2)比對

 終止操作之(比對) -- boolean noneMatch(Predicate<? super T> predicate);

 解釋 : 檢查是否沒有比對所有元素,即流中所有元素都不比對才會傳回true,否則傳回false

 另外兩個類似的方法

a、檢查是否比對所有元素

boolean allMatch(Predicate<? super T> predicate);

b、檢查是否至少比對一個元素

boolean anyMatch(Predicate<? super T> predicate);

這兩個方法的用法都差不多,在此就不一一列舉了

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
boolean result = list.stream()
	.noneMatch((e) -> {return e > 0;});
System.out.println(result);
           

(3)收集

 終止操作(收集)之 -- <R, A> R collect(Collector<? super T, A, R> collector);

 解釋 : 将流中的元素收集起來,傳回一個集合

List<String> list2 = Arrays.asList("Jim", "Tom", "Sam", "Kaven");
//将集合中的元素全部轉化為大寫
List<String> result = list2.stream()
	.map((e) -> {return e.toUpperCase();})
	.collect(Collectors.toList());//collect可以做很多操作,因為Collectors的原因,是以它很強大,筆者會結合Collectors來單獨講collect
System.out.println(result.get(0));
           

注:其中,在collect操作中,Collectors是一個很強大的工具類,專門用來處理Stream流的,筆者會以另外單獨的講解,在此簡單提一下該collect方法

(4)歸約

 終止操作(歸約)之 -- Optional<T> reduce(BinaryOperator<T> accumulator);

 解釋 :    可以将流中元素反複結合起來,得到一個值。如本例的将流中的所有元素相加求和;

Map和Reduce操作是函數式程式設計的核心操作,因為其功能,reduce 又被稱為折疊操作。

另外,reduce 并不是一個新的操作,你有可能已經在使用它。

SQL中類似 sum()、avg() 、count() 的聚集函數,實際上就是 reduce 操作,它們接收多個值并傳回一個值。

流API定義的 reduce() 函數可以接受lambda表達式,并對所有值進行合并。

IntStream這樣的類有類似 average()、count()、sum() 的内建方法來做 reduce 操作,

也有mapToLong()、mapToDouble() 方法來做轉換。這并不會限制你,你可以用内建方法,也可以自己定義。

reduce重載一、Optional<T> reduce(BinaryOperator<T> accumulator);

該方法會傳回一個Optional<T>,其中Lambda表達式中的

第一個參數是上次該函數(Lambda表達式  ->右邊的函數體)執行的傳回值(也稱為中間結果),第二個參數是stream中的元素

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Optional<Integer> result = list.stream()
	.reduce((e1, e2) -> {
			System.out.println(1);//執行了8次,即9個數相加,執行了8次
			return e1 + e2;
		});
System.out.println(result.get());
           

從輸出的8個1結果可以分析得出,上述Lambda表達式的執行相當于以下代碼

int first = list.get(0);

int second = list.get(1);

int sum = 0, i = 1; 

while (i < list.size() - 1) {

sum = first + second;

first = sum;

second = list.get(++i);

}

reduce重載二、T reduce(T identity, BinaryOperator<T> accumulator);

該方法有兩參數,第一個是用來指定歸約結果的初始值,并且可以發現,第一個參數的類型與傳回值類型是相同的,因為指定了初始值,也就不存在null,是以該重載方法不必傳回Optional<T>,

第二個參數則是一個累加器

List<Integer> list2 = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result2 = list2.stream()
	.reduce(0, (e1, e2) -> {
			System.out.println("*");//執行了9次,即9個數相加,執行了9次
			return e1 + e2;
		});
System.out.println(result2);
           

由兩個重載的reduce的執行結果可見,指定初始值與未指定初始值的執行情況是不太一樣的,

變形1,未定義初始值,進而第一次執行的時候第一個參數的值是Stream的第一個元素,第二個參數是Stream的第二個元素,是以9個數相加執行了8次;

變形2,定義了初始值,進而第一次執行的時候第一個參數的值是初始值,第二個參數是Stream的第一個元素,是以9個數相加執行了9次。

reduce重載三、<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

該重載方法的前兩個參數與重載二是一樣的,而第三個參數是Stream為支援并發操作的,

為了避免競争,對于reduce線程都會有獨立的result,combiner的作用在于合并每個線程的result得到最終結果。

這也說明了了第三個函數參數的資料類型必須為傳回資料類型了。

本文不打算對該重載方法舉例

7.3 中間操作

注:中間操作必須和終止操作結合使用才會得到結果,否則中間操作不會執行

而中間操作又可以分為以下幾類:

  • 篩選與切片
  • 映射
  • 排序

(1)篩選與切片

 中間操作(篩選與切片)之 -- Stream<T> filter(Predicate<? super T> predicate);

 解釋 : 接收 Lambda , 從流中排除某些元素,篩選出想要的元素

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
//篩選出大于5的數,擷取過濾掉小于5的數
list.stream()
	.filter((e) -> {return e > 5;})
	.forEach((e) -> {System.out.println(e);});
           

 中間操作(篩選與切片)之 -- Stream<T> distinct();

 解釋 : 篩選,通過流所生成元素的 hashCode() 和 equals() 去除重複元素

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 6, 8, 9, 1);
//篩選出大于5的數,擷取過濾掉小于5的數,并且去重
list.stream()
	.filter((e) -> {return e > 5;})
	.distinct()
	.forEach((e) -> {System.out.println(e);});
           

 中間操作(篩選與切片)之 -- Stream<T> limit(long maxSize);

 解釋 : 截斷流,使其元素不超過給定數量

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
//篩選出大于5的數,擷取過濾掉小于5的數
list.stream()
	.filter((e) -> {return e > 5;})
	.limit(3)
	.forEach((e) -> {System.out.println(e);});
           

 中間操作(篩選與切片)之 -- Stream<T> skip(long n);

 解釋 : 跳過元素,傳回一個扔掉了前 n 個元素的流。若流中元素不足 n 個,則傳回一個空流。與 limit(n) 互補

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
list.stream()
	.skip(5)
	.forEach((e) -> {System.out.println(e);});
           

(2)映射

 中間操作(映射)之 -- <R> Stream<R> map(Function<? super T, ? extends R> mapper);

 解釋 : 接收一個函數作為參數,該函數會被應用到每個元素上,并将其映射成一個新的元素。

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
//兩集合中的元素全部過濾映射為自身的兩倍大小
list.stream()
	.map((o) -> {return o * 2;})
	.forEach((e) -> {System.out.println(e);});
			
List<String> list2 = Arrays.asList("Jim", "Tom", "Sam", "Kaven");
//将集合中的元素全部轉化為大寫
list2.stream()
	.map((e) -> {return e.toUpperCase();})
	.forEach((e) -> {System.out.println(e);});
		
//換句話說,map()就是将流中的元素重新處理,最後傳回處理後的新的流,而處理的規則就是自定義的Function<T, R>的函數式接口
//map與filter有本質差別,filter是在原本元素上進行過濾,得到的是原本的元素中已經過濾掉處理後的元素流,而map是根據自定義的映射規則來轉換,得到的是新的元素流
           

注:映射操作的其他方法還有mapToDouble、mapToInt、mapToLong,他們的思想和用法大同小異,本文不再一一列舉

(3)排序

 中間操作(排序)之 -- Stream<T> sorted();

 解釋 : 産生一個新流,其中按自然順序排序

List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
list.stream()
	.sorted()
	.forEach((e) -> {System.out.println(e);});
           

注:排序方法還有Stream<T> sorted(Comparator<? super T> comparator);,該方式是允許自定義排序規則,但用法一樣,故本文不再舉例

8. 總結

1. 通過本文,或許你已經了解到了Stream的強大之處了,那麼建議讀者在自己日後的代碼中,若有适合場景,盡量用上更為高效、簡潔,性能更好的Stream流;

2. 本文處理講解Stream流的基本概念,還講解了一些常用的Stream API,但是Stream的功能遠遠不僅與此,本文也沒法舉例出是以的API例子,建議讀者結合API文檔或者源碼慢慢學習,熟悉掌握Stream的用法。

注:希望本文對讀者有幫助,轉載請注明出處!

繼續閱讀