[TOC]
*****
# 第 18 章 Java 8函數式編程基礎——Lambda表達式
函數式編程將程序代碼看作數學中的函數,函數本身作為另一個函數的參數或返回值,即高階函數。
## 18.1 Lambda表達式概述
### 18.1.1 從一個示例開始
為了理解Lambda表達式的概念,下面先從一個示例開始。
假設有這樣的一個需求:設計一個通用方法,能夠實現兩個數值的加法和減法運算。Java中方法不能單獨存在,必須定義在類或接口中,考慮是一個通用方法,可以設計一個數值計算接口,其中定義該通用方法,代碼如下:
~~~
//Calculable.java文件
package com.a51work6;
//可計算接口
public interface Calculable {
// 計算兩個int數值
int calculateInt(int a, int b);
}
~~~
Calculable接口只有一個方法calculateInt,參數是兩個int類型,返回值也是int類型。通過方法如下:
~~~
//HelloWorld.java文件
…
/**
* 通過操作符,進行計算
* @param opr 操作符
* @return 實現Calculable接口對象
*/
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// 匿名內部類實現Calculable接口,生成實例對象
result = new Calculable() { ①
// 實現加法運算
@Override
public int calculateInt(int a, int b) { ②
return a + b;
}
};
} else {
// 匿名內部類實現Calculable接口,生成實例對象
result = new Calculable() { ③
// 實現減法運算
@Override
public int calculateInt(int a, int b) { ④
return a - b;
}
};
}
return result;
}
~~~
通用方法calculate中的參數opr是運算符,返回值是實現Calculable接口對象。代碼第①行和第③行都采用匿名內部類實現Calculable接口。代碼第②行實現加法運算。代碼第④行實現減法運算。
調用通用方法代碼如下:
~~~
//HelloWorld.java文件
…
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 實現加法計算Calculable對象
Calculable f1 = calculate('+'); ①
// 實現減法計算Calculable對象
Calculable f2 = calculate('-'); ②
// 調用calculateInt方法進行加法計算
System.out.printf("%d + %d = %d \n", n1, n2,
f1.calculateInt(n1, n2)); ③
// 調用calculateInt方法進行減法計算
System.out.printf("%d - %d = %d \n", n1, n2,
f2.calculateInt(n1, n2)); ④
}
~~~
代碼第①行中f1是實現加法計算Calculable對象,代碼第②行中f2是實現減法計算Calculable對象。代碼第③行和第④行才進行方法調用。
### 18.1.2 Lambda表達式實現
Java 8采用Lambda表達式可以替代匿名內部類。修改之后的通用方法calculate代碼如下:
lambda表達式用于生成只有一個方法的接口的實例對象,并重寫該接口的方法。
~~~
// HelloWorld.java文件
…
/**
* 通過操作符,進行計算
* @param opr 操作符
* @return 實現Calculable接口對象
*/
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// Lambda表達式實現Calculable接口
result = (int a, int b) -> { //1
return a + b;
};
} else {
// result是Calculable類,Lambda表達式實現Calculable接口,并返回實現Calculable接口的實例對象。
result = (int a, int b) -> { //2
return a - b;
};
}
return result;
}
~~~
代碼第①行和第②行用Lambda表達式替代匿名內部類,可見代碼變得簡潔。
**Lambda表達式定義**:Lambda表達式是一個匿名函數(方法)代碼塊,可以作為表達式、方法參數和方法返回值。
Lambda表達式標準語法形式如下:
~~~
(參數列表) -> {
//Lambda表達式體
}
~~~
其中,Lambda表達式參數列表與接口中方法參數列表形式一樣,Lambda表達式體實現接口方法。
*****
result是Calculable類,Lambda表達式實現Calculable接口,并返回實現Calculable接口的實例對象。
### 18.1.3 函數式接口
Lambda表達式實現的接口是函數式接口,這種接口只能有一個方法。
如果接口中聲明多個抽象方法,那么Lambda表達式會發生編譯錯誤:
~~~
The target type of this expression must be a functional interface
~~~
這說明該接口不是函數式接口,為了防止在函數式接口中聲明多個抽象方法,Java 8提供了一個聲明函數式接口注解@FunctionalInterface,示例代碼如下。
~~~
//Calculable.java文件
package com.a51work6;
//可計算接口
@FunctionalInterface
public interface Calculable {
// 計算兩個int數值
int calculateInt(int a, int b);
}
~~~
在接口之前使用@FunctionalInterface注解修飾,那么試圖增加一個抽象方法時會發生編譯錯誤。但可以添加默認方法和靜態方法。
> **提示** Lambda表達式是一個匿名方法代碼,Java中的方法必須聲明在類或接口中,那么Lambda表達式所實現的匿名方法是在函數式接口中聲明的。
## 18.2 Lambda表達式簡化形式
使用Lambda表達式是為了簡化程序代碼,Lambda表達式本身也提供了多種簡化形式,這些簡化形式雖然簡化了代碼,但客觀上使得代碼可讀性變差。本節介紹Lambda表達式本幾種簡化形式。
### 18.2.1 省略參數類型
Lambda表達式可以根據上下文環境推斷出參數類型,所以可以省略參數類型:
~~~
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// Lambda表達式實現Calculable接口
result = (a, b) -> { //1
return a + b;
};
} else {
// Lambda表達式可以省略參數類型
result = (a, b) -> { //2
return a - b;
};
}
return result;
}
~~~
上述代碼第①行和第②行的Lambda表達式是上一節示例的簡化寫法,其中a和b是參數。
### 18.2.2 省略參數小括號
Lambda表達式中參數只有一個時,可以省略參數小括號。修改Calculable接口,代碼如下。
~~~
//Calculable.java文件
package com.a51work6;
//可計算接口
@FunctionalInterface
public interface Calculable {
// 計算一個int數值
int calculateInt(int a);
}
~~~
其中calculateInt方法只有一個int類型參數,返回值也是int類型。調用代碼如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
int n1 = 10;
// 實現二次方計算Calculable對象
Calculable f1 = calculate(2);
// 實現三次方計算Calculable對象
Calculable f2 = calculate(3);
// 調用calculateInt方法進行加法計算
System.out.printf("%d二次方 = %d \n", n1, f1.calculateInt(n1));
// 調用calculateInt方法進行減法計算
System.out.printf("%d三次方 = %d \n", n1, f2.calculateInt(n1));
}
/**
* 通過冪計算
* @param power 冪
* @return 實現Calculable接口對象
*/
public static Calculable calculate(int power) {
Calculable result;
if (power == 2) {
// Lambda表達式實現Calculable接口
result = (int a) -> { //標準形式 //1
return a * a;
};
} else {
// Lambda表達式實現Calculable接口
result = a -> { //函數式接口方法只有一個參數,lambda表達式省略了小括號 //2
return a * a * a;
};
}
return result;
}
}
~~~
上述代碼第①行和第②行都是實現Calculable接口的Lambda表達式。代碼第①行是標準形式沒有任何的簡化。代碼第②行省略了參數類型和小括號。
### 18.2.3 省略return和大括號
如果Lambda表達式體中只有一條語句,那么可以省略return和大括號,代碼如下:
~~~
public static Calculable calculate(int power) {
Calculable result;
if (power == 2) {
// Lambda表達式實現Calculable接口
result = (int a) -> { //標準形式
return a * a;
};
} else {
// Lambda表達式實現Calculable接口,返回一個實例對象
result = a -> a * a * a;
//函數式接口方法中只有一個參數和方法體只有一條語句,省略了小括號,花括號和return
}
return result;
}
~~~
上述代碼第①行是省略了return和大括號,這是最簡化形式的Lambda表達式。## 18.3 作為參數使用Lambda表達式
## 18.3 作為參數使用Lambda表達式
Lambda表達式一種常見的用途是作為參數傳遞給方法。需要聲明參數的類型聲明為函數式接口類型。
示例代碼如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 打印計算結果加法計算結果,Lambda表達式傳參
display((a, b) -> {
return a + b;
}, n1, n2); ①
// 打印計算結果減法計算結果, Lambda表達式傳參
display((a, b) -> a - b, n1, n2); ②
}
/**
* 打印計算結果
*
* @param calc Lambda表達式
* @param n1 操作數1
* @param n2 操作數2
* Calculable 是函數式接口,只有一個方法的接口
*/
public static void display(Calculable calc, int n1, int n2) { ③
System.out.println(calc.calculateInt(n1, n2));
}
}
~~~
上述代碼第③行定義display打印計算結果方法,其中參數calc類型是Calculable,這個參數即可以接收實現Calculable接口的對象,也可以接收Lambda表達式,因為Calculable是函數式接口。
代碼第①行和第②行兩次調用display方法,它們第一個參數都是Lambda表達式。
## 18.4 訪問變量
Lambda表達式可以訪問所在外層作用域內定義的變量,包括:成員變量和局部變量。
### 18.4.1 訪問成員變量
成員變量包括:實例成員變量和靜態成員變量。在Lambda表達式中可以訪問這些成員變量,此時的Lambda表達式與普通方法一樣,可以讀取成員變量,也可以修改成員變量。
示例代碼如下:
~~~
//LambdaDemo.java文件
package com.a51work6;
public class LambdaDemo {
// 實例成員變量
private int value = 10;
// 靜態成員變量
private static int staticValue = 5;
// 靜態方法,進行加法運算
public static Calculable add() { ①
Calculable result = (int a, int b) -> { ②
// 訪問靜態成員變量,不能訪問實例成員變量
staticValue++;
int c = a + b + staticValue; // this.value;
return c;
};
return result;
}
// 實例方法,進行減法運算
public Calculable sub() { ③
Calculable result = (int a, int b) -> { ④
// 訪問靜態成員變量和實例成員變量
staticValue++;
this.value++;
int c = a - b - staticValue - this.value;
return c;
};
return result;
}
}
~~~
LambdaDemo類中聲明一個實例成員變量value和一個靜態成員變量staticValue。此外,還聲明了靜態方法add(見代碼第①行)和實例方法sub(見代碼第③行)。add方法是靜態方法,靜態方法中不能訪問實例成員變量,所以代碼第②行的Lambda表達式中也不能訪問實例成員變量,也不能訪問實例成員方法。
sub方法是實例方法,實例方法中能夠訪問靜態成員變量和實例成員變量,所以代碼第④行的Lambda表達式中可以訪問這些變量,當然實例方法和靜態方法也可以訪問,當訪問實例成員變量或實例方法時可以使用this。
### 18.4.2 捕獲局部變量
對于成員變量的訪問Lambda表達式與普通方法沒有區別,但是對于訪問外層局部變量時,會發生“捕獲變量”情況。Lambda表達式中捕獲變量時,會將變量當成final的,在Lambda表達式中不能修改那些捕獲的變量。
示例代碼如下:
~~~
//LambdaDemo.java文件
package com.a51work6;
public class LambdaDemo {
// 實例成員變量
private int value = 10;
// 靜態成員變量
private static int staticValue = 5;
// 靜態方法,進行加法運算
public static Calculable add() {
//局部變量
int localValue = 20; ①
Calculable result = (int a, int b) -> {
// localValue++; //編譯錯誤 ②
int c = a + b + localValue; ③
return c;
};
return result;
}
// 實例方法,進行減法運算
public Calculable sub() {
//final局部變量
final int localValue = 20; ④
Calculable result = (int a, int b) -> {
int c = a - b - staticValue - this.value; ⑤
// localValue = c; //編譯錯誤 ⑥
return c;
};
return result;
}
}
~~~
上述代碼第①行和第④行都聲明一個局部變量localValue,Lambda表達式中捕獲這個變量,見代碼第③行和第⑤行。不管這個變量是否顯式地使用final修飾,它都不能在Lambda表達式中修改變量,所以代碼第②行和第⑥行如果去掉注釋會發生編譯錯誤。