Java8集合類庫的批量資料操作
第三章:Java集合類庫的批量資料操作
引入批量資料操作的目的是應用lambda函式來實現包含並行操作在內的多種資料處理功能,而支援並行資料操作是其關鍵內容。這個並行操作是在Java7 java.util.concurrency的Fork/Join機制上實現的。
批量操作介面
正如最初在變更說明書上說的,引入批量操作介面的目的是:
給Java集合類庫增加批量操作資料的支援。通常稱這種批量資料操作為 “Java中的filter/map/reduce”。批量資料操作有序列(在當前執行緒上)和並行(使用多執行緒)兩種操作模式。一般用Lambda函式來定義對資料的操作。
由於Lambda表示式已經應用到Java語言和新集合API中,因此我們可以更加高效地利用底層平臺的並行特性。
流式API
JDK中已經增加了一個新包java.util.stream,使我們能夠使用Java8集合類庫執行類似filter/map/reduce的操作。
這個流式API使我們能在資料流之上編寫序列或者並行的操作,如下所示:
List persons = .. //序列操作 Stream stream=persons.stream(); //並行操作 Stream parallelStream=persons.parallelStream(); |
處理一條資料流有點像迭代,只是一條資料流只能遍歷一次,然後就結束了,然而資料流也可能是無盡的,這通常是說流是“斷斷續續”的-我們不能提前知道有多少流元素要處理。
java.util.stream.Stream介面是批量資料操作的入口。我們在拿到流式介面的引用後,就可以使用集合類庫做些有趣的事情了。
關於資料流API要特別注意一點,就是在資料處理過程中不會改動源資料。這是考慮到資料來源可能根本不存在,或者是由於原始資料還要在程式碼的其它地方使用。
Stream原始碼
資料流介面可以使用多種資料來源來處理資料,使用這些流式方法擴充套件標準JDK類庫,可以獲得更好的資料處理體驗。
毫無疑問,首選的用於流式操作的資料來源是集合(collections),如下所示:
List list; Stream stream |
另外,還有一種有趣的資料來源是所謂的生成器(generators),如下所示:
Random random=new Random(); Stream randomNumbers=Stream.generate(random::nextInt); |
有幾種工具方法可以設定操作多大範圍的資料:
IntStream range =IntStream.range(0, 50, 10); range.forEach(System.out::println);// 0, 10, 20, 30, 40 |
標準類庫中也已經存在一些類可以充當資料來源。例如, Random類已經擴充套件了一些有用的方法,如下所示:
newRandom() .ints()// 隨機生成一條的整數資料流 .limit(10)// 我們只要10個隨機整數 .forEach(System.out::println); |
中間操作
中間操作用來描述在資料流之上執行的轉換操作(可以理解為一種對映操作)。filter() 和 map()是不錯的中間操作的例子,它們的返回值是Stream型別,因此可以允許鏈式執行多箇中間操作。
以下是一些有用的中間操作:
- filter 排除所有不滿足條件的元素,具體條件通過Predicate介面來定義;
- map 執行元素的對映轉換,具體的對映方式使用Function介面定義;
- flatMap 通過另外一種 Stream介面將每個流元素轉換成零個或者更多流元素
- peek 對遇到的每個流元素執行一些操作。
- distinct 根據流元素的equals(..)結果排除所有重複的元素
- sorted 使後續操作中的流元素強制按Comparator定義的比較邏輯排列。
- limit 使後續操作只能看到有限數量的元素。
- substream 使後續操作只能看到某個範圍內的元素(使用索引)。
中間操作中的一些,如sorted, distinct 和 limit等是有狀態的,有狀態的意思是這些操作返回的資料流結果依賴之前進行的操作的結果。另外,正如Javadoc上講的,所有中間操作是“延遲執行(lazy)”的。接下來讓我們更詳細的瞭解一些中間操作。
Filter
資料流過濾是我們需要做的初始且固有的操作。Stream介面中有一個filter(..)方法,它以SAM型別的Predicate介面為引數,Predicate介面使我們能夠使用Lambda表示式來定義過濾規則:
List persons = ... Stream personsOver18 =persons.stream().filter(p ->p.getAge()>18); |
Map
Map操作允許我們使用一個Function介面,Function介面接收一種型別的引數,然後返回其他型別。首先,我們來看看在傳統方式下,使用匿名內部類是怎麼定義Map操作的:
Stream students =persons.stream() .filter(p ->p.getAge()>18) .map(new Function<Person, Student>(){ @Override
public Student apply(Person person){ return new Student(person); }
}
); |
現在把上面的實現改用Lambda表示式語法,程式碼如下:
Stream map =persons.stream() .filter(p ->p.getAge()>18) .map(person ->new Student(person)); |
既然作為map(..)方法引數的Lambda表示式僅僅是使用了引數(person),而沒有用此引數做其他操作,那麼我們可以更進一步地把Lambda表示式改寫為方法引用:
Stream map =persons.stream() .filter(p ->p.getAge()>18) .map(Student::new); |
終結操作
資料流處理過程通常包含下面幾個步驟:
1. 從某個資料來源頭獲取到資料流;
2. 執行像filter,map等等這樣的一個或者多箇中間操作;
3. 執行一個終結操作.
終結操作必須是最後一個在資料流上執行的操作。一旦執行了終結操作,資料流就“消耗完了”,不可再用了。
現有如下一些可用的終結操作型別:
- reducers ,如reduce(..), count(..), findAny(..), findFirst(..),可以終結資料流處理過程。根據意圖,終結操作可以是“短路”操作(不用完整的遍歷所有資料流)。例如,findFirst(..)在一遇到匹配的元素就會馬上終結資料流的處理過程。
- collectors,就像其名字表示的,用來把處理過的元素收集到一個結果集中。
- forEach 對資料流中的每一個元素執行某個操作。
- iterators ,如果上面的操作都不能滿足我們的需求,那麼還是採用iterators這種傳統的集合操作方式。
其中最有趣的終結操作型別是所謂的“收集器(collectors)”:
收集器
雖然抽象資料流本質上是連續的,而且我們可以定義資料流上的操作,但是要獲得最終的結果,我們需要以某種方式收集到資料。資料流API提供了一些所謂的“終結”操作,而collect() 方法就是終結操作的其中一個,它使我們能夠收集結果資料。
List students =persons.stream() .filter(p ->p.getAge()>18) .map(Adult::new) .collect(new Collector<Student, List>(){ ... }); |
幸好你在大多數情況下不需要自己實現Collector介面。為了方便起見,已經實現了一個Collectors工具類:
List students =persons.stream() .filter(p ->p.getAge()>18) .map(Student::new) .collect(Collectors.toList()); |
那萬一我們想使用特定的收集邏輯來收集結果,可以像下面這樣做:
List students =persons.stream() .filter(p ->p.getAge()>18) .map(Student::new) .collect(Collectors.toCollection(ArrayList::new)); |
並行和序列
新式資料流API 的一個有意思的特性是它不要求從頭到尾都一定是並行操作或者序列操作。一開始併發地處理資料,然後切換到序列處理,並回到處理流程中的任意步驟,這是可以實現的,如下所示:
List students =persons.stream() .parallel() .filter(p ->p.getAge()>18)// 併發地執行過濾操作 .sequential() .map(Student::new) .collect(Collectors.toCollection(ArrayList::new)); |
資料處理流程中的併發操作會自治地管理自身,不需要我們來處理併發問題,這簡直太酷了。
總結和一則告別漫畫
我們在本文中講了不久就要釋出的java8的三個的主題:
1. Lambd表示式
2. Default 方法
3. Java集合的批量資料操作
就像我們看到的,Lambda表示式極大地提升的程式碼可讀性並使Java語言更加具有表現力,尤其當我們使用新增的資料流API時。相應地,Default方法對API升級至關重要,它用來把Lambda表示式整合到集合API中,為我們使用提供便利。然而,所有新特性的終極目標卻是引入並行類庫和無縫地利用多核硬體的優勢。
長話短說,JVM本身是一項偉大的工程,無論你信不信,Java這個平臺依然會活力無限。這些新的變化,將使我們能夠以更加高效的方式充分利用Java平臺和Java語言,另外也會絕好地給予批評Java的人們更多的東西去琢磨琢磨。
ANTON ARHIPOV,ZeroTurnaround公司的JRebel產品負責人。他熱愛Java,vim和IntelliJ。職業興趣包括程式語言、中介軟體和建模。Anton喜愛喝茶但不喝咖啡。他的tweeter賬號是@antonarhipov,也可以在 LinkedIn上找到他。