文章目錄
1、概念
Lambda 表達式(Lambda expression)是一個匿名函數,基於數學中的λ演算得名,也可稱爲閉包(Closure)。現在很多語言都支持 Lambda 表達式,如 C++、C#、Java、 Python 和 JavaScript 等。
Lambda 表達式是推動 Java 8 發佈的重要新特性,它允許把函數作爲一個方法的參數(函數作爲參數傳遞進方法中)
首先我們先來看一下一個程序演示:
使用匿名內部類來實現一個接口
1)接口Caculation
package Lambda表達式;
public interface Caculation{
int caculationInt(int x,int y);//這是默認的抽象方法,如果不加聲明,實現計算兩個數的和,差
}
2)類Practice1
package Lambda表達式;
public class Practice1 {//我們是通過匿名內部類來實現該接口
public Caculation caculation(char op) {
Caculation result;
if (op == '+') {
result = new Caculation() {//使用匿名內部類來實現該接口
@Override
public int caculationInt(int x, int y) {
return x + y;
}
};
} else{
result = new Caculation() {//發現編譯器會提示可以使用lambda表達式取代
@Override
public int caculationInt(int x, int y) {
return x - y;
}
};
}
return result;
}
public static void main(String[]args){
//
Practice1 pr = new Practice1();
System.out.println(pr.caculation('+').caculationInt(3,4));
}
}
觀察:使用匿名內部類的方法 calculate 代碼很臃腫
,Java 8 採用 Lambda 表達式可以替代匿名內部類。
修改如下:
package Lambda表達式;
public class Practice1 {//我們是通過匿名內部類來實現接口
public Caculation caculation(char op) {
Caculation result;
if (op == '+') {
//使用匿名內部類來實現該接口
result = (x, y) -> x + y;
} else{
//發現編譯器會提示可以使用lambda表達式取代
result = (x, y) -> x - y;
}
return result;
}
public static void main(String[]args){
//
Practice1 pr = new Practice1();
System.out.println(pr.caculation('+').caculationInt(3,4));
}
}
用 Lambda 表達式替代匿名內部類,可見代碼變得簡潔。通過以上示例我們發現,Lambda 表達式是一個匿名函數(方法)代碼塊,可以作爲表達式、方法參數和方法返回值。
Java Lambda 表達式的優缺點
優點:
- 代碼簡潔,開發迅速
- 方便函數式編程
- 非常容易進行並行計算
- Java 引入 Lambda,改善了集合操作(引入 Stream API)
缺點:
- 代碼可讀性變差
- 在非並行計算中,很多計算未必有傳統的 for 性能要高
- 不容易進行調試
2、函數式接口
Lambda 表達式實現的接口不是普通的接口,而是函數式接口。
函數式接口定義:
如果一個接口中,有且只有一個抽象的方法(Object 類中的方法不包括在內),那這個接口就可以被看做是函數式接口。
這種接口只能有一個抽象方法。如果接口中聲明多個抽象方法,那麼 Lambda 表達式會發生編譯錯誤
:
The target type of this expression must be a functional interface
這說明該接口不是函數式接口.
爲了防止在函數式接口中聲明多個抽象方法,Java 8 提供了一個聲明函數式接口註解@FunctionalInterface
,示例代碼如下。
// 可計算接口
@FunctionalInterface
public interface Calculable {
// 計算兩個int數值
int calculateInt(int a, int b);
}
在接口之前使用 @FunctionalInterface 註解修飾,那麼試圖增加一個抽象方法時會發生編譯錯誤。但可以添加默認方法和靜態方法。
@FunctionalInterface 註解與 @Override 註解的作用類似。Java 8 中專門爲函數式接口引入了一個新的註解 @FunctionalInterface。該註解可用於一個接口的定義上,一旦使用該註解來定義接口,編譯器將會強制檢查該接口是否確實有且僅有一個抽象方法,否則將會報錯。需要注意的是,即使不使用該註解,只要滿足函數式接口的定義,這仍然是一個函數式接口,使用起來都一樣。
Lambda 表達式是一個匿名方法代碼,Java 中的方法必須聲明在類或接口中,那麼 Lambda 表達式所實現的匿名方法是在函數式接口中聲明的。
3、Lambda的三種簡寫形式
使用 Lambda 表達式是爲了簡化程序代碼,Lambda 表達式本身也提供了多種簡化形式,這些簡化形式雖然簡化了代碼,但客觀上使得代碼可讀性變差。
3.1、省略參數類型
Lambda 表達式可以根據上下文環境推斷出參數類型。
calculate 方法中 Lambda 表達式能推斷出參數 a 和 b 是 int 類型,簡化形式如下:
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// Lambda表達式實現Calculable接口
//標準形式
result = (int a, int b) -> {
return a + b;
};
} else {
// Lambda表達式實現Calculable接口
//省略形式1
result = (a, b) -> {//不用寫參數名字
return a - b;
};
}
return result;
}
3.2、省略參數小括號(只有一個參數的時候)
如果 Lambda 表達式中的參數只有一個,可以省略參數小括號
。修改 Calculable 接口中的 calculateInt 方法,代碼如下。
// 可計算接口
@FunctionalInterface
public interface Calculable {
// 計算一個int數值
int calculateInt(int a);
}
其中 calculateInt 方法只有一個 int 類型參數,返回值也是 int 類型。
public static Calculable calculate(int power) {
Calculable result;
if (power == 2) {
// Lambda表達式實現Calculable接口
// 標準形式
result = (int a) -> {
return a * a;
};
} else {
// Lambda表達式實現Calculable接口
// 省略形式2
result = a -> {//參數類型省略,小括號也省略
return a * a * a;
};
}
return result;
}
3.3、省略return和大括號(Lambda的方法體只有一句時)
如果 Lambda 表達式體中只有一條語句,那麼可以省略 return 和大括號
,代碼如下:
public static Calculable calculate(int power) {
Calculable result;
if (power == 2) {
// Lambda表達式實現Calculable接口
// 標準形式
result = (int a) -> {
return a * a;
};
} else {
// Lambda表達式實現Calculable接口
// 省略形式3
result = a -> a * a * a;
}
return result;
}
這是最簡化形式的 Lambda 表達式了,代碼太簡潔了!
4、Lambda的使用
4.1、作爲函數的參數(可以接受接口的對象,Lambda表達式)
package Lambda表達式;
public class Practice {
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 打印加法計算結果
display((a, b) -> {
return a + b;
}, n1, n2);
// 打印減法計算結果
display((a, b) -> a - b, n1, n2);
}
/**
* 打印計算結果
*
* @param calc Lambda表達式,因爲時函數式接口,這個參數即可以接收實現 Calculable 接口的對象,也可以接收 Lambda 表達式
* @param n1 操作數1
* @param n2 操作數2
*/
public static void display(Caculation calc, int n1, int n2) {
System.out.println(calc.caculationInt(n1, n2));
}
}
4.2、訪問變量(局部,實例,靜態變量)
Lambda 表達式可以訪問所在外層作用域定義的變量,包括成員變量(實例變量、靜態變量)和局部變量。
1.訪問成員變量
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;
}
}
當訪問實例成員變量或實例方法時可以使用 this,如果不與局部變量發生衝突情況下可以省略 this。
2.訪問局部變量
對於成員變量的訪問 Lambda 表達式與普通方法沒有區別,但是訪問局部變量時,變量必須是 final 類型的(不可改變)注意不聲明時java8開始默認局部變量爲final,除非你重賦值。
package Lambda表達式;
public class LambdaDemo {
// 實例成員變量
private int value = 10;
// 靜態成員變量
private static int staticValue = 5;
// 靜態方法,進行加法運算
public static Caculation add() {
// 局部變量
int localValue = 20;
Caculation result = (int a, int b) -> {
//localValue++;重賦值編譯器則認爲不是final型
// 編譯錯誤
int c = a + b + localValue;
return c;
};
return result;
}
// 實例方法,進行減法運算
public Caculation sub() {
// final局部變量
final int localValue = 20;
Caculation result = (int a, int b) -> {
int c = a - b - staticValue - this.value;
// localValue = c;重賦值編譯器則認爲不是final型
int l = localValue;//只有不重新賦值就沒問題,當然這個量多餘
// 編譯錯誤
return c;
};
return result;
}
}
不管這個變量是否顯式地使用 final 修飾,它都不能在 Lambda 表達式中修改變量,Lambda 表達式只能訪問局部變量而不能修改,否則會發生編譯錯誤,但對靜態變量和成員變量可讀可寫。
4.3、方法引用(::)
方法引用可以理解爲 Lambda 表達式的快捷寫法,它比 Lambda 表達式更加的簡潔,可讀性更高,有很好的重用性。如果實現比較簡單,複用的地方又不多,推薦使用 Lambda 表達式,否則應該使用方法引用。
Java 8 之後增加了雙冒號::
運算符,該運算符用於“方法引用”,注意不是調用方法。“方法引用”雖然沒有直接使用 Lambda 表達式,但也與 Lambda 表達式有關,與函數式接口有關。 方法引用的語法格式如下:
ObjectRef::methodName
其中,ObjectRef 是類名或者實例名,methodName 是相應的方法名。
注意:被引用方法的參數列表和返回值類型,必須與函數式接口方法參數列表和方法返回值類型一致
,示例代碼如下。
public class LambdaDemo {
// 靜態方法,進行加法運算
// 參數列表要與函數式接口方法calculateInt(int a, int b)兼容
public static int add(int a, int b) {
return a + b;
}
// 實例方法,進行減法運算
// 參數列表要與函數式接口方法calculateInt(int a, int b)兼容
public int sub(int a, int b) {
return a - b;
}
}
LambdaDemo 類中提供了一個靜態方法 add,一個實例方法 sub。這兩個方法必須與函數式接口方法參數列表一致,方法返回值類型也要保持一致。
public class HelloWorld {
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 打印加法計算結果
display(LambdaDemo::add, n1, n2);
LambdaDemo d = new LambdaDemo();
// 打印減法計算結果 ,使用方法引用,因爲sub方法與接口的方法實現相同
display(d::sub, n1, n2);
}
/**
* 打印計算結果
*
* @param calc Lambda表達式
* @param n1 操作數1
* @param n2 操作數2
*/
public static void display(Calculable calc, int n1, int n2) {
System.out.println(calc.calculateInt(n1, n2));
}
}
代碼第 18 行聲明 display 方法,第一個參數 calc 是 Calculable 類型,它可以接受三種對象:Calculable 實現對象、Lambda 表達式和方法引用。代碼第 6 行中第一個實際參數LambdaDemo::add是靜態方法的方法引用。代碼第 9 行中第一個實際參數d::sub,是實例方法的方法引用,d 是 LambdaDemo 實例。
提示:代碼第 6 行的LambdaDemo::add和第 9 行的d::sub是方法引用,此時並沒有調用方法,只是將引用傳遞給 display 方法,在 display 方法中才真正調用方法。
再來看一個Demo
package Lambda表達式;
public class Practice1 {//我們是通過匿名內部類來實現接口
public Caculation caculation(char op) {
Caculation result;
if (op == '+') {
//使用匿名內部類來實現該接口,使用方法引用,引用Integer類的求和方法sum
result = Integer::sum;
} else{
//發現編譯器會提示可以使用lambda表達式取代
result = (x, y) -> x - y;
}
return result;
}
public static void main(String[]args){
//
Practice1 pr = new Practice1();
System.out.println(pr.caculation('+').caculationInt(3,4));
}
}
5、Lambda表達式與匿名內部類的聯繫
Java Lambda 表達式的一個重要用法是簡化某些匿名內部類的寫法,因此它可以部分取代匿名內部類的作用。
5.1、相同點
- Lambda 表達式與匿名內部類一樣,都可以直接訪問 effectively final 的局部變量,以及外部類的成員變量(包括實例變量和類變量)。
- Lambda 表達式創建的對象與匿名內部類生成的對象一樣,都可以直接調用從接口中繼承的默認方法。
下面程序示範了 Lambda 表達式與匿名內部類的相似之處。
package Lambda表達式;
//定義一個函數式接口
@FunctionalInterface
interface Displayable {
// 定義一個抽象方法和默認方法
void display();
default int add(int a, int b) {
return a + b;
}
}
public class LambdaAndNei {
private int age = 12;
private static String name = "演示Lambda與匿名內部類聯繫";
public void test() {
int local_value=4;//局部變量不會默認初始化,這裏默認爲final變量
Displayable dis = () -> {//程序使用 Lambda 表達式創建了 Displayable 的對象之後,該對象不僅可調用接口中唯一的抽象方法,也可調用接口中的默認方法
// 訪問的局部變量
System.out.println("local_value 局部變量爲:" + local_value);
// 訪問外部類的實例變量和類變量
System.out.println("外部類的 age 實例變量爲:" + age);
System.out.println("外部類的 name 類變量爲:" + name);
};
dis.display();{//抽象方法的重寫
// 調用dis對象從接口中繼承的add()方法
System.out.println(dis.add(3, 5));
}
}
public static void main(String[] args) {
LambdaAndNei lambda = new LambdaAndNei();
lambda.test();
}
}
輸出結果爲:
上面程序使用 Lambda 表達式創建了一個 Displayable 的對象,Lambda 表達式的代碼塊中的代碼第 19、21 和 22 行分別示範了訪問“effectively final”的局部變量、外部類的實例變量和類變量。從這點來看, Lambda 表達式的代碼塊與匿名內部類的方法體是相同的。
當程序使用 Lambda 表達式創建了 Displayable 的對象之後,該對象不僅可調用接口中唯一的抽象方法,也可調用接口中的默認方法,如上面程序代碼第 26 行所示。
5.2、區別
匿名內部類可以爲任意接口創建實例——不管接口包含多少個抽象方法,只要匿名內部類實現所有的抽象方法即可;但 Lambda 表達式只能爲函數式接口創建實例。
- 匿名內部類可以爲抽象類甚至普通類創建實例;
但 Lambda 表達式只能爲函數式接口創建實例。
- 匿名內部類實現的抽象方法的方法體允許調用接口中定義的默認方法;但 Lambda 表達式的
代碼塊
不允許調用接口中定義的默認方法。
對於 Lambda 表達式的代碼塊不允許調用接口中定義的默認方法的限制,可以嘗試對上面的程序稍做修改,在 Lambda 表達式的代碼塊中增加如下一行:
// 嘗試調用接口中的默認方法,編譯器會報錯
System.out.println(add(3, 5));
雖然 Lambda 表達式的目標類型 Displayable 中包含了 add() 方法,但 Lambda 表達式的代碼塊不允許調用這個方法;如果將上面的 Lambda 表達式改爲匿名內部類的寫法,當匿名內部類實現 display() 抽象方法時,則完全可以調用這個 add() 方法
,如下面代碼所示。
package Lambda表達式;
//定義一個函數式接口
@FunctionalInterface
interface Displayable {
// 定義一個抽象方法和默認方法
void display();
default int add1(int a, int b) {
return a + b;
}
}
public class LambdaAndNei {
private int age = 12;
private static String name = "演示Lambda與匿名內部類聯繫";
public void test() {
int local_value=4;//局部變量不會默認初始化,這裏默認爲final變量
Displayable dis = new Displayable() {
@Override
public void display() {//使用匿名內部類實現接口,該抽象方法可以調用默認方法
// 訪問的局部變量
System.out.println("local_value 局部變量爲:" + local_value);
// 訪問外部類的實例變量和類變量
System.out.println("外部類的 age 實例變量爲:" + age);
System.out.println("外部類的 name 類變量爲:" + name);
System.out.println(add1(1,2));
}
};
dis.display();
}
public static void main(String[] args) {
LambdaAndNei lambda = new LambdaAndNei();
lambda.test();
}
}