本次課程的標(biāo)題不像之前那樣易懂,是一個(gè)陌生的概念,“流式編程”是個(gè)什么東西?
在了解流式編程之前先思考一下“流”,水流、電流、人流,這些都是流。而流式編程則是讓集合中的一個(gè)一個(gè)對象像水流一樣流動(dòng),分別進(jìn)行去重、過濾、映射等操作,就和批量化生產(chǎn)線一樣。利用流,我們無需迭代集合中的元素,就可以提取和操作它們,這些操作通常被組合在一起,在流上形成一條操作管道。
流的一個(gè)核心好處是,它使得程序更加短小并且更易理解,讓我們來看看上次作業(yè)中的“生成 50 個(gè) 1 到 100 之間的不重復(fù)的隨機(jī)數(shù)并輸出”,如果用流來解決的話代碼是怎樣的:
new Random().ints(0,100) //生成0到100的隨機(jī)數(shù)
.distinct() //去除重復(fù)的值
.limit(50) //只取前50個(gè)數(shù)
.forEach(System.out::println); //對每個(gè)元素調(diào)用println函數(shù)
這顯然非常簡單和直觀,而且你甚至都不需要寫任何一句循環(huán)!讓我們趕緊進(jìn)入流式編程的樂園吧。
一、創(chuàng)建流
下面用一組代碼來展示各種創(chuàng)建流的方法:
//產(chǎn)生從0到200的隨機(jī)浮點(diǎn)數(shù)的流
DoubleStream randomStream = new Random().doubles(0, 200);
//將數(shù)組轉(zhuǎn)換成流,可以產(chǎn)生基本數(shù)據(jù)類型的流,
//如IntStream、FloatStream、DoubleStream等,運(yùn)行效率比較高
DoubleStream arrayStream= Arrays.stream(new double[]{1,3,4,5,2,11});
//根據(jù)一組對象產(chǎn)生流,但不能產(chǎn)生基本數(shù)據(jù)類型的流,只能產(chǎn)生對應(yīng)包裝類的流
Stream<String> stream = Stream.of("happy", "sad", "bad", "yes", "no");
//產(chǎn)生從0到9的整數(shù)流
IntStream rangeStream=IntStream.range(0,10);
//以第一個(gè)參數(shù)為種子,迭代產(chǎn)生后面對象的流
Stream<Integer> iterateStream = Stream.iterate(0, i->2*i);
//大部分集合都有stream()方法用來產(chǎn)對應(yīng)的流
Stream<String> collectionStream = new ArrayList<String>().stream();
Stream<String> parallelStream = new ArrayList<String>().parallelStream();
//通過parallelStream()方法可以產(chǎn)生一個(gè)并行流,Java會(huì)將操作在多個(gè)核心上運(yùn)行
二、中間操作
中間操作具體包括去重、過濾、映射等操作,值得說明的是,在執(zhí)行中間操作的代碼的時(shí)候并不會(huì)執(zhí)行這些操作,而只會(huì)把這些操作保存在流里面,每次中間操作都會(huì)產(chǎn)生一個(gè)新的流對象,保存從開始到現(xiàn)在要進(jìn)行的所有操作序列,在執(zhí)行結(jié)束操作的時(shí)候才會(huì)真正執(zhí)行這些操作,這叫做懶加載。
跟蹤和調(diào)試
peek() 操作的目的是幫助調(diào)試。它允許你無修改地查看流中的元素。代碼示例:
// streams/Peeking.java
class Peeking {
public static void main(String[] args) throws Exception {
Stream.of("Will I eat the apple?".split(" "))
.map(w -> w + " ")
.peek(System.out::print)
.map(String::toUpperCase)
.peek(System.out::print)
.map(String::toLowerCase)
.forEach(System.out::print);
}
}
輸出結(jié)果:
Will WILL will I I i eat EAT eat the THE the apple APPLE apple
可以看出流是對每個(gè)元素都分別進(jìn)行同樣的操作,就和流水線一樣。
流元素排序
在最開始的代碼中,我們熟識了 sorted() 的無參數(shù)方法。其實(shí)它還有另一種形式的實(shí)現(xiàn):傳入一個(gè) Comparator 參數(shù)。代碼示例:
// streams/SortedComparator.java
import java.util.*;
public class SortedComparator {
public static void main(String[] args) throws Exception {
//PS:這里的FileToWords是一個(gè)可以將文本文件轉(zhuǎn)換成單詞流的類
FileToWords.stream("Cheese.dat")
.skip(10)
.limit(10)
.sorted(Comparator.reverseOrder())
.map(w -> w + " ")
.forEach(System.out::print);
}
}
輸出結(jié)果:
you what to the that sir leads in district And
sorted() 預(yù)設(shè)了一些默認(rèn)的比較器。這里我們使用的是反轉(zhuǎn)“自然排序”。你當(dāng)然也可以把 Lambda 函數(shù)作為參數(shù)傳遞給 sorted()。
移除元素
distinct():最開始的代碼中的distinct()可用于消除流中的重復(fù)元素。相比創(chuàng)建一個(gè) Set 集合,該方法的效率要高很多。filter(Predicate):過濾操作則會(huì)留下使過濾器方法返回值為true的元素。
在下例中,isPrime() 作為過濾器函數(shù),用于檢測質(zhì)數(shù)。
import java.util.stream.*;
public class Prime {
public static Boolean isPrime(long n) {
return LongStream.rangeClosed(2, (long)Math.sqrt(n))
.noneMatch(i -> n % i == 0);
//如果流中所有元素調(diào)用上述方法都返回false,則noneMatch()返回true
}
public LongStream numbers() {
return LongStream.iterate(2, i -> i + 1)
.filter(Prime::isPrime);
}
public static void main(String[] args) {
new Prime().numbers()
.limit(10)
.forEach(n -> System.out.format("%d ", n));
System.out.println();
new Prime().numbers()
.skip(90)
.limit(10)
.forEach(n -> System.out.format("%d ", n));
}
}
輸出結(jié)果:
2 3 5 7 11 13 17 19 23 29
467 479 487 491 499 503 509 521 523 541
與range()是左閉右開區(qū)間不同,rangeClosed() 是閉區(qū)間,左右的值都包括。如果不能整除,即余數(shù)不等于 0,則 noneMatch() 操作返回 true,如果出現(xiàn)任何等于 0 的結(jié)果則返回 false。
應(yīng)用函數(shù)到元素
map(Function):將原來流中的每個(gè)元素都調(diào)用參數(shù)里的方法,其返回值匯總起來產(chǎn)生一個(gè)新的流。mapToInt(ToIntFunction):操作同上,但結(jié)果是IntStream。mapToLong(ToLongFunction):操作同上,但結(jié)果是LongStream。mapToDouble(ToDoubleFunction):操作同上,但結(jié)果是DoubleStream。
之前的代碼中就多次用到了map方法,只需要知道它可以將流里的所有元素都變成與其對應(yīng)的新元素就可以了,這里就不進(jìn)行代碼展示了。
最后需要注意的一點(diǎn)是,同一個(gè)流只能進(jìn)行一次操作,例如下面的代碼就會(huì)報(bào)錯(cuò):
//以第一個(gè)參數(shù)為種子,迭代產(chǎn)生后面對象的流
Stream<Integer> iterateStream= Stream.iterate(0, i->2*i).limit(10);
iterateStream.map(Integer::doubleValue);
iterateStream.map(Integer::byteValue); //這句語句會(huì)報(bào)錯(cuò)
三、結(jié)束操作
這些操作接收一個(gè)流并產(chǎn)生一個(gè)最終結(jié)果;它們不會(huì)向后面的流提供任何東西。因此,結(jié)束操作總是你在管道中做的最后一個(gè)操作。
轉(zhuǎn)化為數(shù)組
toArray():將流轉(zhuǎn)換成適當(dāng)類型的數(shù)組。toArray(generator):在特殊情況下,生成器用于分配自定義的數(shù)組存儲。
遍歷元素
forEach(Consumer):你已經(jīng)看到過很多次System.out::println作為 Consumer 函數(shù)。forEachOrdered(Consumer): 確保按照原始流的順序執(zhí)行。
看著這兩種形式,似乎forEach方法并不會(huì)按順序輸出,但其實(shí)在沒有調(diào)用parallel()方法之前這兩個(gè)方法的輸出結(jié)果都是一樣的。
這里稍微簡單介紹下 parallel():可實(shí)現(xiàn)多處理器并行操作。實(shí)現(xiàn)原理是將流分割為多個(gè)(通常數(shù)目為 CPU 核心數(shù))并在不同處理器上分別執(zhí)行操作。而進(jìn)行并行操作的時(shí)候,forEach操作無法保證元素按原來的順序輸出,而forEachOrdered則可以確保按原來的順序輸出。
收集
collect(Collector):使用Collector收集流元素到結(jié)果集合中。collect(Supplier, BiConsumer, BiConsumer):收集流元素到結(jié)果集合中,第一個(gè)參數(shù)用于創(chuàng)建一個(gè)新的結(jié)果集合,第二個(gè)參數(shù)用于將下一個(gè)元素加入到現(xiàn)有結(jié)果合集中,第三個(gè)參數(shù)用于將兩個(gè)結(jié)果合集合并。
第一種形式中的的Collector參數(shù),Java核心庫為我們提供了很多Collector實(shí)現(xiàn)類,都在Collectors這個(gè)工具類里面,例如Colletors.toList、Collectos.toMap、Collections.toCollection等等,基本上都是故名思義,就不介紹了。當(dāng)然也可以自己寫寫一個(gè)類來繼承自Collector來實(shí)現(xiàn)自定義的收集要求
但對于自定義的元素收集要求,最好的辦法還是采用第二種形式,讓我們來看一個(gè)示例代碼:
import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
public static void main(String[] args) throws Exception {
ArrayList<String> words = FileToWords.stream("Cheese.dat")
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
words.stream()
.filter(s -> s.equals("cheese"))
.forEach(System.out::println);
}
}
匹配
allMatch(Predicate):如果流的每個(gè)元素根據(jù)提供的Predicate都返回 true 時(shí),最終結(jié)果返回為 true。這個(gè)操作將會(huì)在第一個(gè) false 之后短路,也就是不會(huì)在發(fā)生 false 之后繼續(xù)執(zhí)行計(jì)算。anyMatch(Predicate):如果流中的任意一個(gè)元素根據(jù)提供的Predicate返回 true 時(shí),最終結(jié)果返回為 true。這個(gè)操作將會(huì)在第一個(gè) true 之后短路,也就是不會(huì)在發(fā)生 true 之后繼續(xù)執(zhí)行計(jì)算。noneMatch(Predicate):如果流的每個(gè)元素根據(jù)提供的Predicate都返回 false 時(shí),最終結(jié)果返回為 true。這個(gè)操作將會(huì)在第一個(gè) true 之后短路,也就是不會(huì)在發(fā)生 true 之后繼續(xù)執(zhí)行計(jì)算。
元素查找
findFirst():返回一個(gè)含有第一個(gè)流元素的Optional類型的對象,如果流為空返回Optional.empty。findAny():返回含有任意流元素的Optional類型的對象,如果流為空返回Optional.empty。
findFirst() 無論流是否為并行化的,總是會(huì)選擇流中的第一個(gè)元素。對于非并行流,findAny()會(huì)選擇流中的第一個(gè)元素(但從定義上來看是選擇任意元素)。
統(tǒng)計(jì)信息
average():求取流元素平均值。max()和min():求元素的最大值和最小值,對于非數(shù)字流則要多一個(gè)Comparator參數(shù)。sum():對所有流元素進(jìn)行求和。count():流中的元素個(gè)數(shù)。