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可以通過下圖來簡易了解:
而和疊代器又不同的是,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的用法。
注:希望本文對讀者有幫助,轉載請注明出處!