Java8爲什麼引入Lambda表達式(老文章很實用)

函數編程在C#、Python、JavaScript中都得到充分體現。而Java直到最新的Java 8才開始正式支持函數編程,最明顯的改進就是對Lamba表達式的支持。正如C#之父Anders Hejlsberg在那篇文章 編程語言大趨勢 中所講,未來的編程語言將逐漸融合各自的特性,而不存在單純的聲明式語言(如之前的Java)或者單純的函數編程語言。將來聲明式編程語言借鑑函數編程思想,函數編程語言融合聲明式編程特性...這幾乎是一種必然趨勢。

那具體而言我們為什麼需要Lambda表達式呢?難道Java的OO和命令式編程(imperative programming)特性不夠強大嗎?下面讓我們來分析下其原因。

1、內部循環和外部循環

先看一個大家耳熟能詳的例子:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

for (int number : numbers) {

System.out.println(number);

}

是不是很常見呢?這個叫外部循環(External Iteration)。但是外部循環有什麼問題呢?簡單來說存在下面三個缺點:

1.只能順序處理List中的元素(process one by one)

2.不能充分利用多核CPU

3.不利於編譯器優化

而如果利用內部循環,代碼寫成下面這樣:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

numbers.forEach((Integer value) -> System.out.println(value));

這樣就能規避上面的三個問題:

1.不一定需要順序處理List中的元素,順序可以不確定

2.可以並行處理,充分利用多核CPU的優勢

3.有利於JIT編譯器對代碼進行優化

2、傳遞行為,而不僅僅是傳值

如果你使用C#有一段時間的話,那麼你很可能已經明白這個標題的意思了。

我們先來看一個體現傳值侷限性的場景吧,上代碼:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

public int sumAll(List numbers) {

int total = 0;

for (int number : numbers) {

total += number;

}

return total;

}

sumAll算法很簡單,完成的是將List中所有元素相加。某一天如果我們需要增加一個對List中所有偶數求和的方法sumAllEven,如下:

public int sumAllEven(List numbers) {

int total = 0;

for (int number : numbers) {

if (number % 2 == 0) {

total += number;

}

}

return total;

}

又有一天,我們需要增加第三個方法:對List中所有大於3的元素求和,那是不是繼續加下面的方法呢?

public int sumAllEven(List numbers) {

int total = 0;

for (int number : numbers) {

if (number > 3) {

total += number;

}

}

return total;

}

比較這三個方法,我們發現了一個很明顯的“代碼臭味”—— 代碼重複(詳情參考《重構》),三個方法的唯一區別在於if判斷這一行代碼。如果脫離這裡的上下文,我們會怎麼做呢?我首先會先想到利用策略模式重構代碼如下:

public interface Strategy {

public boolean test(int num);

}

public class SumAllStrategy implements Strategy {

public boolean test(int num) {

return true;

}

}

public class SumAllEvenStrategy implements Strategy {

public boolean test(int num) {

return num % 2 == 0;

}

}

public class ContextClass {

private Strategy stragegy = null;

private final static Strategy DEFAULT_STRATEGY = new SumAllStrategy();

public ContextClass() {

this(null);

}

public ContextClass(Stragegy stragegy) {

if(strategy != null) {

this.strategy = strategy;

}

else {

this.strategy = DEFAULT_STRATEGY;

}

}

public int sumAll(List numbers) {

int total = 0;

for (int number : numbers) {

if (strategy.test(number)) {

total += number;

}

}

return total;

}

}

// 調用

ContextClass context = new ContextClass();

context.sumAll(numbers);

設計模式在這裡發揮了作用,OO特性還是蠻強大的!但這是唯一的解決方案嗎(當然不考慮用其他設計模式來解決,因為都是OO範疇!)?當然有,該輪到Java 8 Lambda表達式中的謂詞(Predicate)發揮作用了!

public int sumAll(List numbers, Predicate p) {

int total = 0;

for (int number : numbers) {

if (p.test(number)) {

total += number;

}

}

return total;

}

sumAll(numbers, n -> true);

sumAll(numbers, n -> n % 2 == 0);

sumAll(numbers, n -> n > 3);

代碼是不是比上面簡潔很多了?語義應該也很明確,就不多解釋了。從這裡也可以看出未引入Lambda表達式之前的Java代碼的冗長(Java這點被很多人詬病)。

3、Consumer與Loan Pattern

比如我們有一個資源類Resource:

public class Resource {

public Resource() {

System.out.println("Opening resource");

}

public void operate() {

System.out.println("Operating on resource");

}

public void dispose() {

System.out.println("Disposing resource");

}

}

我們必須這樣調用:

Resource resource = new Resource();

try {

resource.operate();

} finally {

resource.dispose();

}

因為對資源對象resource執行operate方法時可能拋出RuntimeException,所以需要在finally語句塊中釋放資源,防止可能的內存洩漏。

但是有一個問題,如果很多地方都要用到這個資源,那麼就存在很多段類似這樣的代碼,這很明顯違反了DRY(Don't Repeat Yourself)原則。而且如果某位程序員由於某些原因忘了用try/finally處理資源,那麼很可能導致內存洩漏。那咋辦呢?Java 8提供了一個Consumer接口,代碼改寫為如下:

public class Resource {

private Resource() {

System.out.println("Opening resource");

}

public void operate() {

System.out.println("Operating on resource");

}

public void dispose() {

System.out.println("Disposing resource");

}

public static void withResource(Consumer consumer) {

Resource resource = new Resource();

try {

consumer.accept(resource);

} finally {

resource.dispose();

}

}

}

調用代碼如下:

Resource.withResource(resource -> resource.operate());

外部要訪問Resource不能通過它的構造函數了(private),只能通過withResource方法了,這樣代碼清爽多了,而且也完全杜絕了因人為疏忽而導致的潛在內存洩漏。

4、stream+laziness => efficiency

像之前一樣先來一段非常簡單的代碼:

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

for (int number : numbers) {

if (number % 2 == 0) {

int n2 = number * 2;

if (n2 > 5) {

System.out.println(n2);

break;

}

}

}

這段代碼有什麼問題? 沒錯,可讀性非常差。第一步,我們利用《重構》一書中的最基礎的提取小函數重構手法來重構代碼如下:

public boolean isEven(int number) {

return number % 2 == 0;

}

public int doubleIt(int number) {

return number * 2;

}

public boolean isGreaterThan5(int number) {

return number > 5;

}

for (int number : numbers) {

if (isEven(number)) {

int n2 = doubleIt(number);

if (isGreaterThan5(n2)) {

System.out.println(n2);

break;

}

}

}

OK,代碼的意圖清晰多了,但是可讀性仍然欠佳,因為循環內嵌套一個if分支,if分支內又嵌套另外一個分支,於是繼續重構代碼如下:

public boolean isEven(int number) {

return number % 2 == 0;

}

public int doubleIt(int number) {

return number * 2;

}

public boolean isGreaterThan5(int number) {

return number > 5;

}

List l1 = new ArrayList();

for (int n : numbers) {

if (isEven(n)) l1.add(n);

}

List

l2 = new ArrayList();

for (int n : l1) {

l2.add(doubleIt(n));

}

List l3 = new ArrayList();

for (int n : l2) {

if (isGreaterThan5(n)) l3.add(n);

}

System.out.println(l3.get(0));

現在代碼夠清晰了,這是典型的“流水線”風格代碼。但是等等,現在的代碼執行會佔用更多空間(三個List)和時間,我們來分析下。首先第二版代碼的執行流程是這樣的:

isEven: 1

isEven: 2

doubleIt: 2

isGreaterThan5: 2

isEven: 3

isEven: 4

doubleIt: 4

isGreaterThan5: 4

8

而我們的第三版代碼的執行流程是這樣的:

isEven: 1

isEven: 2

isEven: 3

isEven: 4

isEven: 5

isEven: 6

doubleIt: 2

doubleIt: 4

doubleIt: 6

isGreaterThan5: 2

isGreaterThan5: 4

isGreaterThan5: 6

8

步驟數是13:9,所以有時候重構得到可讀性強的代碼可能會犧牲一些運行效率(但是一切都得實際衡量之後才能確定)。那麼有沒有“三全其美”的實現方法呢?即:

1.代碼可讀性強

2.代碼執行效率不比第一版代碼差

3.空間消耗小

Streams come to rescue! Java 8提供了stream方法,我們可以通過對任何集合對象調用stream()方法獲得Stream對象,Stream對象有別於Collections的幾點如下:

1.不存儲值:Streams不會存儲值,它們從某個數據結構的流水線型操作中獲取值(“酒肉穿腸過”)

2.天生的函數編程特性:對Stream對象操作能得到一個結果,但是不會修改原始數據結構

3.Laziness-seeking(延遲搜索):Stream的很多操作如filter、map、sort和duplicate removal(去重)可以延遲實現,意思是我們只要檢查到滿足要求的元素就可以返回

4.可選邊界:Streams允許Client取足夠多的元素直到滿足某個條件為止。而Collections不能這麼做

上代碼:

System.out.println(

numbers.stream()

.filter(Lazy::isEven)

.map(Lazy::doubleIt)

.filter(Lazy::isGreaterThan5)

.findFirst()

);

現在的執行流程是:

isEven: 1

isEven: 2

doubleIt: 2

isGreaterThan5: 4

isEven: 3

isEven: 4

doubleIt: 4

isGreaterThan5: 8

IntOptional[8]

流程基本和第二版代碼一致,這歸功於Laziness-seeking特性。怎麼理解呢?讓我來構造下面這個場景:

Stream流對象要經過下面這種流水線式處理:

過濾出偶數 => 乘以2 => 過濾出大於5的數 => 取出第一個數

注意:=> 左邊的輸出是右邊的輸入

而Laziness-seeking意味著 我們在每一步只要一找到滿足條件的數字,馬上傳遞給下一步去處理並且暫停當前步驟。比如先判斷1是否偶數,顯然不是;繼續判斷2是否偶數,是偶數;好,暫停過濾偶數操作,將2傳遞給下一步乘以2,得到4;4繼續傳遞給第三步,4不滿足大於5,所以折回第一步;判斷3是否偶數,不是;判斷4是否偶數,是偶數;4傳遞給第二步,乘以2得到8;8傳遞給第三步,8大於5;所以傳遞給最後一步,直接取出得到 IntOptional[8]。

IntOptional[8]只是簡單包裝了下返回的結果,這樣有什麼好處呢?如果你接觸過Null Object Pattern的話就知道了,這樣可以避免無謂的null檢測。

轉自https://blog.csdn.net/youzhouliu/article/details/51816315


分享到:


相關文章: