1 什么是Stream(流)
計(jì)算機(jī)科學(xué)中有很多帶“流”的概念惧所,例如字符流辅肾,字節(jié)流陪毡,比特流等等日月,很少有書籍在講到這些概念的時(shí)候會詳解介紹什么是流,所以有時(shí)候會導(dǎo)致讀者感到迷惑缤骨,在這里爱咬,我大膽嘗試簡單解釋一下“流”到底是個(gè)什么東西。
舉個(gè)例子绊起,水流大家都見過吧(無論是水管中的水流精拟,還是海流或者河流),從微觀的角度看水流虱歪,它就是由一個(gè)一個(gè)的水分子和其他物質(zhì)組合形成的(至于怎么流動的蜂绎,這就是流體力學(xué)的事了,先不管)笋鄙,從一個(gè)或者多個(gè)源流動到一個(gè)或者多個(gè)目的地师枣,例如大家的生活用水就是從水庫流到各位的家中。在水流動的過程中萧落,可以采取一些處理践美,使得水變得更加潔凈,安全找岖。
從這個(gè)例子中陨倡,不難看到有幾個(gè)關(guān)鍵詞:分子,源许布,目的地兴革,處理等。現(xiàn)在蜜唾,再來看看計(jì)算機(jī)中的所謂的比特流杂曲,比特流里的“分子”就是一個(gè)一個(gè)的比特(0和1),源就是計(jì)算機(jī)本身(從宏觀的角度看)袁余,而目的地則是其他的計(jì)算機(jī)擎勘,在源和目的地之間,我們同樣可以加入一些處理操作來處理比特泌霍,使得目的地收到的數(shù)據(jù)是符合需求的货抄。
經(jīng)過這么一個(gè)類比述召,各位應(yīng)該大概知道什么是“流”了吧,現(xiàn)在給出一個(gè)比較簡短的定義(來源是《Java8實(shí)戰(zhàn)》):“從支持?jǐn)?shù)據(jù)處理操作的源生成的元素序列”蟹地』看起來不像是人話對吧,不要害怕怪与,把這句話拆開來看就好了:
- 元素序列夺刑。一個(gè)一個(gè)的分子根據(jù)一定的規(guī)則排列形成的集合,例如比特流從一個(gè)一個(gè)比特組成的序列叫做比特序列分别,字符流中一個(gè)一個(gè)字符組成的序列叫做字符序列遍愿。
- 數(shù)據(jù)處理操作。對序列的元素進(jìn)行處理耘斩,例如污水處理等沼填。
- 源。生成元素?cái)?shù)據(jù)的機(jī)器或者程序括授。
除此之外坞笙,流還有一些特點(diǎn):
- 在同一地點(diǎn),不同時(shí)刻荚虚,看到的元素是不同的薛夜,即流是具有動態(tài)性的,錯(cuò)過了就是錯(cuò)過了版述,無法再次拿出來做處理梯澜。
- 可以有多個(gè)處理操作,某個(gè)處理操作的輸出就是下一個(gè)處理操作的輸入渴析,就想生產(chǎn)手機(jī)的流水線一樣晚伙。
- .....
作為補(bǔ)充理解,可以看看下面這張圖檬某,來源也是《Java8 實(shí)戰(zhàn)》一書撬腾,描述的是集合和流的區(qū)別(個(gè)人覺得是一個(gè)很形象的比喻):
2 為什么需要流呢
假設(shè)有一個(gè)需求:現(xiàn)在有一個(gè)Car對象的集合,我們希望從集合中找到并返回所有符合age <= 2條件的對象恢恼,然后根據(jù)age字段對對象集合進(jìn)行排序,最后返回對象的brand字段的集合胰默。
如果使用傳統(tǒng)的方式編寫代碼场斑,代碼可能是下面這樣的:
List<Car> filteredCars = new ArrayList<>();
for (Car car : cars) {
if (car.getAge() <= 2) {
filteredCars.add(car);
}
}
Collections.sort(filteredCars, new Comparator<Car>() {
@Override
public int compare(Car o1, Car o2) {
return o1.compareTo(o2);
}
});
List<String> carNames = new ArrayList<>();
for (Car car : filteredCars) {
carNames.add(car.getBrand());
}
注意到代碼中使用了一個(gè)filteredCars集合,該集合既不是源集合牵署,也不是目標(biāo)集合漏隐,只是一個(gè)中間集合,如果我們采用這種方式編程奴迅,這個(gè)中間集合是不得不使用的青责,但中間集合是有空間消耗的挺据,如果集合數(shù)據(jù)很多,那么這個(gè)中間集合的影響就會很大脖隶。除此之外扁耐,這種方式編寫的代碼并不簡潔,如果沒有注釋的話产阱,想要完全弄清楚這段代碼是在干什么應(yīng)該不是一件容易的事婉称。那有沒有辦法簡化代碼,讓人一看就知道代碼的目的呢构蹬?答案是例如Java8的Stream API王暗,下面來看看使用這種新的方式編寫代碼是怎樣的:
List<String> carNames = cars.stream().filter((car) -> car.getAge() <= 2)
.sorted(Car::compareTo)
.map(Car::getBrand)
.collect(Collectors.toList());
非常簡單,就四行代碼W病(如果你愿意俗壹,寫成一行也可以,但是不推薦)而且非常易讀藻烤,看了第一行绷雏,就能發(fā)現(xiàn):“哦,這里要做一個(gè)filter的操作”隐绵,看了第二行就能發(fā)現(xiàn)這是一個(gè)sorted排序操作之众,剩下的同理。
先不用管stream()是什么依许,sorted()是什么棺禾,后面會介紹到。
上述例子表現(xiàn)了Stream的一個(gè)優(yōu)點(diǎn)峭跳,除此之外膘婶,Stream還可以用于應(yīng)對數(shù)據(jù)是無限的情況,例如素?cái)?shù)流蛀醉,偶數(shù)流等等悬襟,更多的應(yīng)用無法在這短短一篇文章中特性,還需要多多修煉拯刁!
3 使用Stream API
Stream API的操作很多脊岳,例如filter,sorted,map,reduce,collect等等,大致可以分為兩類操作:中間操作和終端操作垛玻。流經(jīng)過中間操作后割捅,其輸出仍然可以作為下一個(gè)操作的輸入,但經(jīng)過終端操作之后帚桩,就無法繼續(xù)進(jìn)行操作了亿驾,所以一般終端操作都是一些具有“聚合”功能的操作,例如collect將流中的數(shù)據(jù)“收集”成集合或者其他什么用于保存數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)(用戶可以自定義這個(gè)收集操作账嚎,也可以使用內(nèi)置的API)莫瞬。而中間操作一般都是對數(shù)據(jù)進(jìn)行“處理”儡蔓,例如filter用于篩選數(shù)據(jù),map用于對數(shù)據(jù)進(jìn)行映射疼邀,即將數(shù)據(jù)轉(zhuǎn)換成另一種形式等喂江。下圖是常用的API:
下面我將選擇幾個(gè)最常用的操作進(jìn)行介紹:
- filter。對數(shù)據(jù)進(jìn)行過濾操作檩小,參數(shù)類型是Predicate<T>开呐,這是一個(gè)函數(shù)式接口,故可以傳遞lambda表達(dá)式规求。
- map筐付。對數(shù)據(jù)進(jìn)行映射,參數(shù)類型是Function<T,R>阻肿,也是一個(gè)函數(shù)式接口瓦戚,可以傳遞lambda表達(dá)式。
- flatMap丛塌。扁平化的map较解,在一些場景下尤其重要。
- anyMatch赴邻。是一個(gè)終端操作印衔,參數(shù)是Predicate<T>,語義是一旦找到任何符合條件的元素姥敛,就立即返回了奸焙。其他幾個(gè)match也類似。
- forEach彤敛。遍歷与帆,比較常用,參數(shù)是Consumer<T>墨榄,也是函數(shù)式接口玄糟。
- collect。收集袄秩,是數(shù)據(jù)聚合操作阵翎,如果我們的最終目的是返回一個(gè)集合,那么就可以使用這個(gè)API之剧。
先給出共用的數(shù)據(jù)集合以及類:
public class Car implements Comparable<Car> {
private String brand;
private Color color;
private Integer age;
public Car(String brand, Color color, Integer age) {
this.brand = brand;
this.color = color;
this.age = age;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", color=" + color +
", age=" + age +
'}';
}
@Override
public int compareTo(Car o) {
return this.getAge() < o.getAge() ? 1 : this.getAge() == o.getAge() ? 0 : -1;
}
public enum Color {
RED,WHITE,PINK,BLACK,BLUE;
}
//getter and setter
}
private static final List<Car> cars = Arrays.asList(
new Car("BWM",Car.Color.BLACK, 2),
new Car("Tesla", Car.Color.WHITE, 1),
new Car("BENZ", Car.Color.RED, 3),
new Car("Maserati", Car.Color.BLACK,1),
new Car("Audi", Car.Color.PINK, 5));
3.1 filter
filter的作用就是篩選數(shù)據(jù)贮喧,如果數(shù)據(jù)符合條件,就讓他繼續(xù)在流里流動猪狈,否則直接取出來,使其離開所在的流辩恼。假設(shè)現(xiàn)在我們要篩選cars集合中所有顏色是黑色的car對象雇庙,該怎么做呢谓形?非常簡單,如下所示:
cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
.forEach(System.out::println);
forEach先不用管疆前,后面會講到寒跳。
嘗試一下,驗(yàn)證一下答案是不是BWM和Maserati呢竹椒?filter接受一個(gè)參數(shù)童太,參數(shù)類型是Predicate<? super T>,在上一篇文章中胸完,我已經(jīng)介紹了函數(shù)式接口以及l(fā)ambda表達(dá)式书释,所以這里就不再贅述了。
3.2 map
Google有一個(gè)很著名的大數(shù)據(jù)框架赊窥,即Map-Reduce爆惧,如果對Hadoop有一些了解的話,應(yīng)該都知道锨能。Map其實(shí)就是一個(gè)映射扯再,即將原始數(shù)據(jù)轉(zhuǎn)換成另一種形式。現(xiàn)在我們繼續(xù)上面filter的例子址遇,如果現(xiàn)在我想讓返回的僅僅是篩選后的對象的brand字段集合熄阻,該如何做呢?可以使用map倔约,如下所示:
cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
.map(Car::getBrand)
.forEach(System.out::println);
這里的map就是將Car對象實(shí)例轉(zhuǎn)換成brand字符串秃殉,這個(gè)操作是非常有意義的,因?yàn)槿绻绻覀儍H僅需要一個(gè)brand字符串跺株,對其他的根本不關(guān)系复濒,又有什么必要還留著其他數(shù)據(jù)來影響后續(xù)的處理呢?
3.3 flatMap
這是扁平化的map操作乒省,和普通的map操作最大的不同就是flatMap能把流中的某個(gè)元素都換成另一個(gè)流巧颈,然后把所有的流連接起來形成一個(gè)新的流。還是有些難以理解是吧袖扛,借用書上的一個(gè)例子來說明一下:
String[] arrayOfWords = {"Hello", "World"};
Arrays.stream(arrayOfWords)
.map(word -> word.split(""))
.distinct()
.forEach(System.out::println);
這段代碼的目的是找出所有字母砸泛,這些字母是在數(shù)組中的單詞里出現(xiàn)的,例如Helllo蛆封,World中出現(xiàn)了H,e,l,l,o,r,d,w這幾個(gè)字母唇礁。但如果你運(yùn)行一下上面這段代碼,會發(fā)現(xiàn)返回的并不是我們所想的那樣惨篱,而是類似這樣的:
[Ljava.lang.String;@15aeb7ab
[Ljava.lang.String;@7b23ec81
即返回是兩個(gè)數(shù)組盏筐,怎么會這樣呢?簡單剖析一下砸讳,map操作的對象類型是String琢融,即每個(gè)單詞界牡,然后調(diào)用split()方法,該方法的返回值是Spring[]類型漾抬,所以map的返回類型是Stream<String[]>類型宿亡,而后面又沒有太多的處理了,故最后forEach遍歷的起始是String[]類型的對象纳令,并不是我們想要的字符串挽荠。那么,怎么修改呢平绩?答案是:使用flatMap使其扁平化:
Arrays.stream(arrayOfWords)
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.forEach(System.out::println);
只是在map后面多了一個(gè)flatMap操作就能解決問題了嗎圈匆?剛剛說了,map的返回值類型是Stream<String[]>馒过,即現(xiàn)在流中的元素類型是String[]臭脓,flatMap嘗試把String[]數(shù)組類的內(nèi)容展開,即如果數(shù)組里的內(nèi)容是"Hello"腹忽,那么flatMap(Arrays::stream)来累,就把H,e,l,l,o當(dāng)做新的流,然后再組合成一個(gè)新的更大的流窘奏。一圖勝千言嘹锁,來看看具體的分析圖:
3.4 anyMatch
即找到任何一個(gè)符合條件的元素就立即返回一個(gè)true,如果都沒找到着裹,那么就返回false领猾。如下所示:
boolean IsExist = cars.stream()
.anyMatch(car -> car.getAge() <= 2);
非常簡單,不多作解釋了骇扇。
3.5 forEach
即遍歷摔竿,在Java8以前,我們要么顯式的使用迭代器或者用增強(qiáng)for循環(huán)的方式少孝,要么就使用索引的方式(前提是集合支持索引)來遍歷集合的元素继低,在Java8之后,遍歷集合元素變得更加簡單了稍走,不再需要顯式的構(gòu)造for循環(huán)袁翁,這種方式被稱作“內(nèi)部迭代”。關(guān)于其使用婿脸,在上面的例子已經(jīng)多次使用到了粱胜,這里就不再寫例子了,但我想提的是內(nèi)部迭代的效率和外部顯式迭代的效率對比狐树,網(wǎng)上有很多文章有提到焙压,內(nèi)部迭代的效率比外部迭代更低,我在我的機(jī)器上稍微測試了一下(有預(yù)熱),發(fā)現(xiàn)確實(shí)如此冗恨,但并不會低很多答憔。我想表達(dá)的是,如果真的對性能特別敏感掀抹,那么用傳統(tǒng)的外部顯式迭代也許會是一個(gè)更好的選擇,否則我更推薦基于Stream API的內(nèi)部迭代心俗,因?yàn)樗啙嵃廖洌Z義更明確(個(gè)人看法)。
其實(shí)這里提到的“內(nèi)部迭代”的概念并不僅僅存在于forEach操作城榛,可以說整個(gè)流API的操作都是基于“內(nèi)部迭代”的揪利,這里特別說明一下。
3.6 collect
最后就是collect操作了狠持,這是一個(gè)“聚合”或者叫做“歸約”操作疟位。其參數(shù)是Collector<? super T, A, R> collector類型,但這并不是一個(gè)函數(shù)式接口喘垂,在Collectors這個(gè)類里提供了一些常用的聚集成集合操作甜刻,例如toList就是聚集成一個(gè)List,toSet就是聚集成一個(gè)Set正勒,同時(shí)得院,這是一個(gè)終端操作,一旦調(diào)用了該操作章贞,就無法繼續(xù)進(jìn)行其他操作了祥绞。如下所示:
cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
.map(Car::getBrand)
.collect(Collectors.toList());
其實(shí)我們也可以自定義聚集成其他更多類型的集合或者一些自定義的數(shù)據(jù)結(jié)構(gòu),具體的實(shí)現(xiàn)方法可以參考Collectors里的幾個(gè)方法鸭限,就不多說了蜕径。
4 小結(jié)
本文簡單介紹了什么是流,為什么要使用流以及如何使用Java8提供的Stream API败京,但其實(shí)Stream API的功能遠(yuǎn)不止這樣兜喻,還有一些更加強(qiáng)大的功能,例如count()操作等等等等.....Java8除了提供實(shí)現(xiàn)好的API喧枷,還可以自定義一些符合用戶需求的功能虹统,這也是Stream API強(qiáng)大的原因之一。
Stream是一個(gè)非常龐大的體系隧甚,我這一篇短短幾千詞的文章遠(yuǎn)遠(yuǎn)不能囊括所有车荔。如果本文有什么地方有錯(cuò)誤或者不足,真誠的希望您能指出戚扳,大家共同進(jìn)步忧便!