我們期待了很久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循環。
List persons = asList(new Person("Joe"), new Person("Jim"), new Person("John")); for (Person p : persons) { p.setLastName("Doe"); }
上面的例子是我們先前的做法,也就是所謂的外在迭代,循環是固定的順序循環。在現在多核心的時代,如果我們想並行循環,不得不修改以上程式碼。效率能有多大提升也說定,且會帶來一定的風險(線程安全問題等等)。
要描述內部迭代,我們需要用到Lambda這樣的類別庫,下面利用lambda和Collection.forEach重寫上面的循環
persons.forEach(p->p.setLastName("Doe"));
現在是由jdk 庫來控制循環了,我們不需要關心last name是怎麼被設定到每個person物件裡面去的,函式庫可以依照運作環境來決定怎麼做,並行,亂序或懶載入方式。這就是內部迭代,客戶端將行為p.setLastName當做資料傳入api裡面。
內部迭代其實和集合的批次操作並沒有密切的聯繫,借助它我們感受到語法表達上的變化。真正有趣的和批量操作相關的是新的流(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表達式。
List persons = … Stream personsOver18 = persons.stream().filter(p -> p.getAge() > 18);//过滤18岁以上的人
3.1.2Map
假使我們現在過濾了一些數據,例如轉換物件的時候。 Map操作允許我們執行一個Function的實作(Function
Stream adult= persons .stream() .filter(p -> p.getAge() > 18) .map(new Function() { @Override public Adult apply(Person person) { return new Adult(person);//将大于18岁的人转为成年人 } });
現在,把上述例子轉換成使用lambda表達式的寫法:
Stream map = persons.stream() .filter(p -> p.getAge() > 18) .map(person -> new Adult(person));
3.1.3Count
count方法是一個流的終點方法,可使流的結果最終統計,返回int,例如我們計算一下滿足18歲的總人數:
int countOfAdult=persons.stream() .filter(p -> p.getAge() > 18) .map(person -> new Adult(person)) .count();
3.1.4Collect
collect方法也是一個流的終點方法,可收集最終的結果
List adultList= persons.stream() .filter(p -> p.getAge() > 18) .map(person -> new Adult(person)) .collect(Collectors.toList());
或者,如果我們想使用特定的實作類別來收集結果:
List adultList = persons .stream() .filter(p -> p.getAge() > 18) .map(person -> new Adult(person)) .collect(Collectors.toCollection(ArrayList::new));
篇幅有限,其他的中間方法和終點方法就不一一介紹了,看了上面幾個例子,大家明白這兩種方法的區別即可,後面可依需求決定使用。
3.2順序流與並行流
每個Stream都有兩種模式:順序執行和並行執行。
順序流:
List <Person> people = list.getStream.collect(Collectors.toList());
並行流:
List <Person> people = list.getStream.parallel().collect(Collectors.toList());
顧名思義,當使用順序方式去遍歷時,每個item讀完後再讀下一個item。而使用並行去遍歷時,陣列會被分成多個段,其中每一個都在不同的執行緒中處理,然後將結果一起輸出。
3.2.1並行流原理:
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順序與平行性能測試對比
如果是多核心機器,理論上並行流則會比順序流快上一倍,下面是測試程式碼
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以及集合框架的改進便是最好的證明。
更多Java8新特性lambda表達式有什麼用(用法實例)相關文章請關注PHP中文網!