流是Java API的新成員,它允許你以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)
Java 8中的Stream API可以讓你寫出這樣的代碼:
聲明性——更簡潔,更易讀
可複合——更靈活
可並行——性能更好
首先看一下使用流和不使用流的區別,需求: 把集合中年齡小於等於20的人的名字取出來並排序
不使用流:
public List<String> beforeJava7(List<User> users){
// 取年齡 <= 20的用戶
List<User> tmpList = new ArrayList<>();
for (User user : users) {
if (user.getAge() <= 20){
tmpList.add(user);
}
}
// 排序
Collections.sort(tmpList, new Comparator<User>() {
public int compare(User u1, User u2) {
return u1.getName().compareTo(u2.getName());
}
});
// 取名字
List<String> userNames = new ArrayList<>();
for(User user : tmpList){
userNames.add(user.getName());
}
return userNames;
}
使用流:
public List<String> java8(List<User> users){
//為了利用多核架構並行執行這段代碼,只需要把stream()換成parallelStream():
List<String> userNames = users.stream()
.filter(user -> user.getAge() <= 20)
.sorted(Comparator.comparing(User::getName))
.map(User::getName)
.collect(Collectors.toList());
return userNames;
}
從支持數據處理操作的源生成的元素序列。
讓我們一步步剖析這個定義:
1. 元素序列
就像集合一樣,流也提供了一個接口,可以訪問特定元素類型的一組有序值。因為集合是數據結構,所以它的主要目的是以特定的時間/空間複雜度存儲和訪問元 素(如ArrayList 與 LinkedList)。但流的目的在於表達計算,集合講的是數據,流講的是計算。
2. 源
流會使用一個提供數據的源,如集合、數組或輸入/輸出資源。 請注意,從有序集合生成流時會保留原有的順序。由列表生成的流,其元素順序與列表一致。
3. 數據處理操作
流的數據處理功能支持類似於資料庫的操作,以及函數式程式語言中的常用操作,如filter、 map、 reduce、 find、 match、 sort等。流操作可以順序執行,也可並行執行。
流操作有兩個重要的特點。
1. 流水線
很多流操作本身會返回一個流,這樣多個操作就可以連結起來,形成一個大的流水線。流水線的操作可以 看作對數據源進行資料庫式查詢。
2. 內部迭代
與使用迭代器顯式迭代的集合不同,流的迭代操作是在背後進行的。
看一段能夠顯示這些概念的代碼,需求是: 把集合中年齡小於等於20的人的名字取出來並排序
public List<String> java8(List<User> users){
List<String> userNames = users.stream()
.filter(user -> user.getAge() <= 20)
.sorted(Comparator.comparing(User::getName))
.map(User::getName)
.collect(Collectors.toList());
return userNames;
}
本例中,我們先是對users調用stream方法,由用戶列表得到一個流。 數據源是用戶列表,它給流提供一個元素序列。接下來,對流應用一系列數據處理操作: filter、 map、 sorted和collect。除了collect之外,所有這些操作都會返回另一個流,這樣它們就可以接成一條流 水線,於是就可以看作對源的一個查詢。最後, collect操作開始處理流水線,並返回結果(它 和別的操作不一樣,因為它返回的不是流,在這裡是一個List)。在調用collect之前,沒有任 何結果產生,實際上根本就沒有從users裡選擇元素。你可以這麼理解:鏈中的方法調用都在排 隊等待,直到調用collect。
集合與流之間的差異就在於什麼時候進行計算。集合是一個內存中的數據結構,它包含數據結構中目前所有的值——集合中的每個元素都得先算出來才能添加到集合中。(你可以往集合裡加東西或者刪東西,但是不管什麼時候,集合中的每個元素都是放在內存裡的,元素都得先算出來才能成為集合的一部分。)
相比之下,流則是在概念上固定的數據結構(你不能添加或刪除元素),其元素則是按需計算的。 從另一個角度來說,流就像是一個延遲創建的集合:只有在消費者要求的時候才會計算值
以質數為例,要是想創建一個包含所有質數的集合,那這個程序算起來就沒完沒了了,因為總有新的質數要算,然後把它加到集合裡面。
而流的話,僅僅從流中提取需要的值,而這些值——在用戶看不見的地方,只會按需生成。
流只能被消費一次,如果被消費多次,則會拋出異常:java.lang.IllegalStateException: stream has already been operated upon or closed
如下代碼所示: 這段代碼的意思是遍歷 lists 集合
List<String> lists = Arrays.asList("java8","lambda","stream");
Stream<String> stringStream = lists.stream();
Consumer<String> consumer = (x) -> System.out.println(x);
stringStream.forEach(consumer);
//stream has already been operated upon or closed
stringStream.forEach(consumer);
使用Collection接口需要用戶去做迭代(比如用for-each),這稱為外部迭代。 相反,Streams庫使用內部迭代——它幫你把迭代做了,還把得到的流值存在了某個地方,你只要給出 一個函數說要幹什麼就可以了。
java.util.stream.Stream中的Stream接口定義了許多操作。它們可以分為兩大類。
1. 中間操作
2. 終端操作
在上述例子中,filter,sorted,map 等都是中間操作,collect等是終端操作
中間操作可以連成一條流水線,終端觸發流水線執行並關閉它。
中間操作:諸如filter或sorted等中間操作會返回另一個流。這讓多個操作可以連接起來形成一個查詢。重要的是,除非流水線上觸發一個終端操作,否則中間操作不會執行任何處理。這是因為中間操作一般都可以合併起來,在終端操作時一次性全部處理。
終端操作:終端操作會從流的流水線生成結果。其結果是任何不是流的值,比如List、 Integer,甚至void。
總而言之,流的使用一般包括三件事:
1. 一個數據源(如集合)來執行一個查詢;
2. 一個中間操作鏈,形成一條流的流水線;
3. 一個終端操作,執行流水線,並能生成結果
篩選filter()方法Streams接口的filter方法,該操作會接受一個謂詞(一個返回boolean的函數)作為參數,並返回一個包括所有符合謂詞的元素的流。
filter(Predicate<? super T> predicate)
該方法接受一個 Predicate 作為參數。 如下代碼所示,
/**
* 把用戶集合中性別是男的用戶選擇出來
* user1, users2, users3是三種不同的寫法,結果都一樣
* @param users 原始用戶集合
* @return 性別是男性的用戶
*/
public List<User> filterOfStream(List<User> users){
List<User> users1 = users.stream().filter(User::isMen).collect(Collectors.toList());
List<User> users2 = users.stream().filter(u -> u.getGender().equals("男")).collect(Collectors.toList());
Predicate<User> predicate = (u) -> u.getGender().equals("男");
List<User> users3 = users.stream().filter(predicate).collect(Collectors.toList());
//return users1;
//return users2;
return users3;
}
該方法對流中重複的元素去重
/**
* distinct(): 去除流中重複的元素
* 列印集合中的偶數,並且不能重複
*/
public void distinctOfStream(){
List<Integer> lists = Arrays.asList(1, 2, 3, 4, 2, 6, 4, 7, 8, 7);
//2468
lists.stream().filter(x -> x % 2 == 0).distinct().forEach(System.out::print);
// 另一種寫法
Consumer<Integer> consumer = (x) -> System.out.print(x);
//2468
lists.stream().filter(x -> x % 2 == 0).distinct().forEach(consumer);
}
該方法會返回一個不超過給定長度的流。如果流是有序的,則最多會返回前n個元素。 如果是無序的,如set,limit的結果不會以任何順序排列。
/**
* limit():返回流中指定長度的流
*/
public void limitOfStream(){
List<Integer> lists = Arrays.asList(1, 2, 3, 4, 2, 6, 4, 7, 8, 7);
//獲取 lists 中前三個元素, 有序
// 123
lists.stream().limit(3).forEach(System.out::print);
}
該方法會跳過前 n 個元素,返回 n+1 後面的元素的一個流
/**
* skip(n):跳過前n個元素,返回n後面的元素
*/
public void skipOfStream(){
List<Integer> lists = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//跳過前5個元素,從第6個元素開始列印
//678910
lists.stream().skip(5).forEach(System.out::print);
}
一個非常常見的數據處理套路就是從某些對象中選擇信息。比如在SQL裡,你可以從表中選擇一列。 Stream API也通過map和flatMap方法提供了類似的工具。
map()方法:
它會接受一個函數作為參數。這個函數會被應用到每個元素上,並將其映射成一個新的元素
註:map不是我們理解的集合Map,應該理解為映射,將一個值映射為另一個值
如下的例子為:取出集合中用戶的名字,返回一個名字集合
/**
* @param users 用戶集合
* @return 用戶名字集合
*/
public List<String> mapOfStream(List<User> users){
List<String> usersNames = users.stream().map(User::getName).collect(Collectors.toList());
// 另一種寫法
Function<User, String> function = (user) -> user.getName();
List<String> usersNames2 = users.stream().map(function).collect(Collectors.toList());
//return usersNames2;
// 獲取每個用戶的名字的長度
// 寫法一
List<Integer> userNameLength = users.stream()
.map(User::getName)// 獲取用戶名
.map(String::length) // 獲取每個用戶名的長度
.collect(Collectors.toList()); // 返回一個集合
// 寫法二
Function<User, String> name = user -> user.getName();
Function<String, Integer> len = s -> s.length();
List<Integer> userNameLength2 = users.stream().map(name).map(len).collect(Collectors.toList());
// 寫法三
List<Integer> userNameLength3 = users.stream().map(s -> s.getName()).map(s -> s.length()).collect(Collectors.toList());
return usersNames;
}
flatmap方法把一個流中的每個值都換成另一個流,然後把所有的流連接起來成為一個流。
查找和匹配另一個常見的數據處理套路是看看數據集中的某些元素是否匹配一個給定的屬性。 StreamAPI通過allMatch、 anyMatch、 noneMatch、 findFirst和findAny方法提供了這樣的工具。
anyMatch()方法:該方法的意思是 流中是否有一個元素能匹配給定的謂詞,只要有一個能夠匹配,就返回 true
/**
* anyMatch(): 流中是否有一個元素能匹配給定的謂詞,只要有一個能夠匹配,就返回 true
*/
public void anyMatchOfStream(){
List<Integer> lists = Arrays.asList(1, 2, 3, 3, 4, 5);
Stream<Integer> stream = lists.stream();
if (stream.anyMatch(i -> i == 3)){
System.out.println("包含 3");
}else{
System.out.println("不包含 3");
}
}
檢查流中的元素是否都能匹配給定的謂詞,只有所有的值和給定的謂詞相等,才返回 true
/**
* allMatch():檢查流中的元素是否都能匹配給定的謂詞
*/
public void allMatch(){
List<Integer> lists = Arrays.asList(3, 3);
if (lists.stream().allMatch(i -> i == 3)){
System.out.println("完全匹配");
}else{
System.out.println("不完全匹配");
}
}
確保流中沒有任何元素與給定的謂詞匹配,沒有匹配,返回 true
anyMatch、 allMatch和noneMatch這三個操作都用到了我們所謂的短路,這就是大家熟悉的Java中&&和||運算符短路在流中的版本。
findAny()方法:findAny方法將返回當前流中的任意元素。它可以與其他流操作結合使用
public void findAnyOfStream(List<User> users){
Optional<User> user =
users.stream().filter(u -> u.getName().equals("男")).findAny();
}
該方法返回流中的第一個元素
歸約reduce()方法:
reduce 操作可以實現從Stream中生成一個值,其生成的值不是隨意的,而是根據指定的Lambda表達式。
/**
* reduce()
*/
public void reduceOfStream(){
List<Integer> lists = Arrays.asList(1, 2, 3, 3, 4, 5);
// 元素的總和
int sum = lists.stream().reduce(0, (x, y) -> x + y);
Optional<Integer> sum2 = lists.stream().reduce(Integer::sum);
System.out.println("sum = " + sum);
System.out.println("sum2 = " + sum2.get());
// 求最大值
int max = lists.stream().reduce(0, (x, y) -> x > y ? x : y);
Optional<Integer> max2 = lists.stream().reduce(Integer::max);
System.out.println("max = " + max);
System.out.println("max2 = " + max2.get());
// 最小值
int min = lists.stream().reduce(1, (x, y) -> x > y ? y : x);
Optional<Integer> min2 = lists.stream().reduce(Integer::min);
System.out.println("min = " + min);
System.out.println("min2 = " + min2.get());
}
三個重載的方法:
reduce(T identity, BinaryOperator<T> accumulator)
提供一個初始值,後面是一個Lambda表達式,如計算兩個值的最大值:
int max = lists.stream().reduce(0, (x, y) -> x > y ? x : y);
Optional<T> reduce(BinaryOperator<T> accumulator)
只是提供一個 Lambda表達式,返回一個Optional對象
Optional<Integer> max2 = lists.stream().reduce(Integer::max);
reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
第三個重載的方法的第三個參數的意思: Stream是支持並發操作的,為了避免競爭,對於reduce線程都會有獨立的result,combiner的作用在於合併每個線程的result得到最終結果。
數值流前面看到了可以使用reduce方法計算流中元素的總和
int sum = lists.stream().reduce(0, Integer::sum);
這段代碼的問題是,它有一個暗含的裝箱成本。每個Integer都必須拆箱成一個原始類型,再進行求和。
Java 8引入了三個原始類型特化流接口來解決這個問題: IntStream、 DoubleStream和 LongStream,分別將流中的元素特化為int、 long和double,從而避免了暗含的裝箱成本。
每個接口都帶來了進行常用數值歸約的新方法,比如對數值流求和的sum,找到最大元素的max。 此外還有在必要時再把它們轉換回對象流的方法。
將流轉換為特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。要把特型流轉換成一般流(每個int都會裝箱成一個Integer),可以使用boxed方法
Stream<Integer> stream = intStream.boxed();
java 8引入了兩個可以用於IntStream和LongStream的靜態方法,幫助生成數值的範圍: range和rangeClosed。這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但 range是不包含結束值的,而rangeClosed則包含結束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
可以使用靜態方法Stream.of,通過顯式值創建一個流。它可以接受任意數量的參數。 以下代碼直接使用Stream.of創建了一個字符串流。然後,你可以將字符串轉換為大寫,再一個個列印出來:
/**
* of() 方法創建流
*/
public void ofOfStream(){
Stream<String> stringStream = Stream.of("java", "lambda", "stream");
stringStream.map(s -> s.toUpperCase()).forEach(System.out::println);
可以使用靜態方法Arrays.stream從數組創建一個流。它接受一個數組作為參數。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers)
Java中用於處理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files中的很多靜態方法都會返回一個流。
4. 由函數生成流:創建無限流 Stream API提供了兩個靜態方法來從函數生成流: Stream.iterate和Stream.generate。 這兩個操作可以創建所謂的無限流:不像從固定集合創建的流那樣有固定大小的流。由iterate 和generate產生的流會用給定的函數按需創建值,因此可以無窮無盡地計算下去!一般來說, 應該使用limit(n)來對這種流加以限制,以避免列印無窮多個值。
一般來說,在需要依次生成一系列值的時候應該使用iterate,比如一系列日期: 1月31日, 2月1日,依此類推。 與iterate方法類似, generate方法也可讓你按需生成一個無限流。但generate不是依次 對每個新生成的值應用函數的。它接受一個Supplier<T>類型的Lambda提供新的值。