我們期待了很久lambda爲java帶來閉包的概念,但是如果我們不在集合中使用它的話,就損失了很大價值。現有接口遷移成爲lambda風格的問題已經通過default methods解決了,在這篇文章將深入解析Java集合裏面的批量數據操作(bulk operation),解開lambda最強作用的神祕面紗。
1.關於JSR335
JSR是Java Specification Requests的縮寫,意思是Java 規範請求,Java 8 版本的主要改進是 Lambda 項目(JSR 335),其目的是使 Java 更易於爲多核處理器編寫代碼。JSR 335=lambda表達式+接口改進(默認方法)+批量數據操作。加上前面兩篇,我們已是完整的學習了JSR335的相關內容了。
2.外部VS內部迭代
以前Java集合是不能夠表達內部迭代的,而只提供了一種外部迭代的方式,也就是for或者while循環。
1
2
3
4
|
List
persons = asList( new Person( "Joe" ), new Person( "Jim" ), new Person( "John" )); for (Person
p : persons) { p.setLastName( "Doe" ); } |
要描述內部迭代,我們需要用到Lambda這樣的類庫,下面利用lambda和Collection.forEach重寫上面的循環
1
|
persons.forEach(p->p.setLastName( "Doe" )); |
內部迭代其實和集合的批量操作並沒有密切的聯繫,藉助它我們感受到語法表達上的變化。真正有意思的和批量操作相關的是新的流(stream)API。新的java.util.stream包已經添加進JDK 8了。
3.Stream API
流(Stream)僅僅代表着數據流,並沒有數據結構,所以他遍歷完一次之後便再也無法遍歷(這點在編程時候需要注意,不像Collection,遍歷多少次裏面都還有數據),它的來源可以是Collection、array、io等等。
3.1中間與終點方法
流作用是提供了一種操作大數據接口,讓數據操作更容易和更快。它具有過濾、映射以及減少遍歷數等方法,這些方法分兩種:中間方法和終端方法,“流”抽象天生就該是持續的,中間方法永遠返回的是Stream,因此如果我們要獲取最終結果的話,必須使用終點操作才能收集流產生的最終結果。區分這兩個方法是看他的返回值,如果是Stream則是中間方法,否則是終點方法。具體請參照Stream的api。
簡單介紹下幾個中間方法(filter、map)以及終點方法(collect、sum)
3.1.1Filter
在數據流中實現過濾功能是首先我們可以想到的最自然的操作了。Stream接口暴露了一個filter方法,它可以接受表示操作的Predicate實現來使用定義了過濾條件的lambda表達式。
1
2
|
List
persons = … Stream
personsOver18 = persons.stream().filter(p -> p.getAge() > 18 ); //過濾18歲以上的人 |
3.1.2Map
假使我們現在過濾了一些數據,比如轉換對象的時候。Map操作允許我們執行一個Function的實現(Function<T,R>的泛型T,R分別表示執行輸入和執行結果),它接受入參並返回。首先,讓我們來看看怎樣以匿名內部類的方式來描述它:
1
2
3
4
5
6
7
8
9
|
Stream
adult= persons .stream() .filter(p
-> p.getAge() > 18 ) .map( new Function()
{ @Override public Adult
apply(Person person) { return new
Adult(person); //將大於18歲的人轉爲成年人 } }); |
1
2
3
|
Stream
map = persons.stream() .filter(p
-> p.getAge() > 18 ) .map(person
-> new Adult(person)); |
3.1.3Count
count方法是一個流的終點方法,可使流的結果最終統計,返回int,比如我們計算一下滿足18歲的總人數
1
2
3
4
|
int countOfAdult=persons.stream() .filter(p
-> p.getAge() > 18 ) .map(person
-> new Adult(person)) .count(); |
3.1.4Collect
collect方法也是一個流的終點方法,可收集最終的結果
1
2
3
4
|
List
adultList= persons.stream() .filter(p
-> p.getAge() > 18 ) .map(person
-> new Adult(person)) .collect(Collectors.toList()); |
1
2
3
4
5
|
List
adultList = persons .stream() .filter(p
-> p.getAge() > 18 ) .map(person
-> new Adult(person)) .collect(Collectors.toCollection(ArrayList:: new )); |
篇幅有限,其他的中間方法和終點方法就不一一介紹了,看了上面幾個例子,大家明白這兩種方法的區別即可,後面可根據需求來決定使用。
3.2順序流與並行流
每個Stream都有兩種模式:順序執行和並行執行。
順序流:
1
|
List
<Person> people = list.getStream.collect(Collectors.toList()); |
1
|
List
<Person> people = list.getStream.parallel().collect(Collectors.toList()); |
3.2.1並行流原理:
1
2
3
4
5
6
|
List
originalList = someData; split1
= originalList( 0 ,
mid); //將數據分小部分 split2
= originalList(mid,end); new Runnable(split1.process()); //小部分執行操作 new Runnable(split2.process()); List
revisedList = split1 + split2; //將結果合併 |
3.2.2順序與並行性能測試對比
如果是多核機器,理論上並行流則會比順序流快上一倍,下面是測試代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
long t0
= System.nanoTime(); //初始化一個範圍100萬整數流,求能被2整除的數字,toArray()是終點方法 int a[]=IntStream.range( 0 ,
1_000_000).filter(p -> p % 2 == 0 ).toArray(); long t1
= System.nanoTime(); //和上面功能一樣,這裏是用並行流來計算 int b[]=IntStream.range( 0 ,
1_000_000).parallel().filter(p -> p % 2 == 0 ).toArray(); long t2
= System.nanoTime(); //我本機的結果是serial:
0.06s, parallel 0.02s,證明並行流確實比順序流快 System.out.printf( "serial:
%.2fs, parallel %.2fs%n" ,
(t1 - t0) * 1e- 9 ,
(t2 - t1) * 1e- 9 ); |
3.3關於Folk/Join框架
應用硬件的並行性在java 7就有了,那就是 java.util.concurrent 包的新增功能之一是一個 fork-join 風格的並行分解框架,同樣也很強大高效,有興趣的同學去研究,這裏不詳談了,相比Stream.parallel()這種方式,我更傾向於後者。
4.總結
如果沒有lambda,Stream用起來相當彆扭,他會產生大量的匿名內部類,比如上面的3.1.2map例子,如果沒有default method,集合框架更改勢必會引起大量的改動,所以lambda+default method使得jdk庫更加強大,以及靈活,Stream以及集合框架的改進便是最好的證明。