概述
閱讀專案程式碼時,尤其是閱讀一些原始碼時,經常會遇到 Lambda 表示式。對此之前看過相關文章,但是停留在模模糊糊的印象上。今天趁著有時間,通過一些 demo 示例,梳理一下它的用法,以備後期遺忘的時候快速查詢它的用法!
Lambda 表示式是 Java 8 的重要更新,它支援將程式碼塊作為方法引數、允許使用更簡潔的程式碼來建立只有一個抽象方法的介面的例項。
描述中提到的介面稱為函式式介面
語法
Lambda 表示式的主要作用就是可以用於簡化建立匿名內部類物件,Lambda 表示式的程式碼塊將會用於實現抽象方法的方法體,Lambda 表示式就相當於一個匿名方法。
Lambda 表示式由三部分組成:
- 形參列表:形參列表允許省略型別,如果形參列表中只有一個引數,形參列表的圓括號也可以省略;
- 箭頭(
->
):通過英文畫線和大於符號組成; - 程式碼塊:如果程式碼塊只有一條語句,花括號可以省略。Lambda 程式碼塊只有一條 return 語句,可以省略 return 關鍵字,Lambda 表示式會自動返回這條語句的值作為返回值。
示例
interface Eatable {
void taste();
}
interface Flyable {
void fly(String weather);
}
interface Addable {
int add(int a, int b);
}
public class LambdaQs {
// 呼叫該方法需要傳入一個 Eatable 型別的物件
public void eat(Eatable e) {
System.out.println(e);
e.taste();
}
// 呼叫該方法需要傳入 Flyable 型別的物件
public void drive(Flyable f) {
System.out.println("我正在駕駛:" + f);
f.fly("「夏日晴天」");
}
// 呼叫該方法需要 Addable 型別的物件
public void calc(Addable add) {
System.out.println("5 + 3 = " + add.add(5, 3));
}
public static void main(String[] args) {
LambdaQs lq = new LambdaQs();
// Lambda 表示式的程式碼塊只有一句,因此省略了花括號
lq.eat(() -> System.out.println("雪糕的味道不錯!"));
// Lambda 表示式的形參只有一個引數,因此省略了圓括號
lq.drive(weather -> {
// 對介面中抽象方法 fly 的重寫
System.out.println("今天天氣是:" + weather);
System.out.println("飛機平穩飛行!");
});
// Lambda 表示式只有一條語句,即使該表示式需要返回值,也可以省略 return
lq.calc((a, b) -> a + b);
// 如果不用 Lambda 表示式,就需要如下匿名類的方式去重寫抽象方法
lq.calc(new Addable() {
@Override
public int add(int a, int b) {
return a + b;
}
});
}
}
輸出結果:
oop.lambda.LambdaQs$$Lambda$1/1607521710@7ef20235
雪糕的味道不錯!
我正在駕駛:oop.lambda.LambdaQs$$Lambda$2/1329552164@15aeb7ab
今天天氣是:「夏日晴天」
飛機平穩飛行!
5 + 3 = 8
5 + 3 = 8
以上示例可以說明,Lambda 表示式實際上可以被當做一個具體的物件。
Lambda 表示式與函式式介面
Lambda 表示式的型別,也被稱為「目標型別(target type
)」。Lambda 表示式的目標型別必須是「函式式介面(functional interface
)」。函式式介面代表只包含一個抽象方法的介面。函式式介面可以包含多個預設方法、類方法,但僅能宣告一個抽象方法。
查詢 Java 8 的 API 文件,可以發現大量的函式式介面,例如:Runnable、ActionListener 等介面都是函式式介面。
Java 8 專門為函式式介面提供了
@FunctionalInterface
註解。該註解就是用於告訴編譯器校驗介面必須是函式式介面,否則就報錯。
由於 Lambda 表示式的結果就是被當做物件/例項,因此,可以使用 Lambda 表示式進行賦值,示例:
Runnable r = () -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
};
我們看一下 Runnable 介面的定義:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
看一個錯誤示例:
Object obj = () -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
};
上面這段程式碼會報錯:Target type of a lambda conversion must be an interface
。Lambda 表示式的目標型別必須是明確的函式式介面!將 Lambda 表示式賦值給 Object 型別的變數,編譯器只能推斷出它的表達型別為 Object,而 Object 並不是函式式介面,因此就報錯了!
為了保證 Lambda 表示式的目標型別是明確的函式式介面,有如下三種常見方式:
- 將 Lambda 表示式賦值給函式式介面型別的變數;
- 將 Lambda 表示式作為函式式介面型別的引數傳給某個方法;
- 使用函式式介面對 Lambda 表示式進行強制型別轉換;
將上面出錯的程式碼可以進行如下的改寫:
Object obj1 = (Runnable)() -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
};
綜上,Lambda 表示式的本質很簡單,就是使用簡單的語法來建立函式式介面的例項,避免匿名內部類的繁瑣。
方法引用於構造器引用
如果 Lambda 表示式的程式碼塊只有一條程式碼,還可以在程式碼中使用方法引用和構造器引用。
方法引用和構造器引用的好處是使 Lambda 表示式的程式碼塊更加簡潔。方法引用和構造器引用都需要使用兩個英文冒號 ::
。
種類 | 示例 | 說明 | 對應的 Lambda 表示式 |
---|---|---|---|
引用類方法 | 類名::類方法 | 函式式介面中被實現的方法的全部引數傳給該類方法作為引數 | (a,b,...) -> 類名.類方法(a,b,...) |
引用特定物件的例項方法 | 特定物件::例項方法 | 函式式介面中被實現的方法的全部引數傳給該方法作為引數 | (a,b,...) -> 特定物件.例項方法(a,b,...) |
引用某類物件的例項方法 | 類名::例項方法 | 函式式介面中被實現的方法的第一個引數作為呼叫者,後面的引數全部傳給該方法作為引數 | (a,b,...)->a.例項方法(b,...) |
引用構造器 | 類名::new | 函式式介面中被實現方法的全部引數傳給該構造器作為引數 | (a,b,...)->new 類名(a,b,...) |
@FunctionalInterface
interface Converter {
Integer convert(String from);
}
@FunctionalInterface
interface MyTest {
String test(String a, int b, int c);
}
@FunctionalInterface
interface YourTest {
// 抽象方法負責根據 String 引數生成一個 JFrame 返回值
JFrame win(String title);
}
public class LambdaRef {
public static void main(String[] args) {
// 1 引用類方法
// 下面使用 Lambda 表示式建立 Converter 物件
Converter converter1 = from -> Integer.valueOf(from);
Integer val = converter1.convert("99");
// 函式式介面中被實現方法的全部引數傳給該類方法作為引數
Converter converter2 = Integer::valueOf;
Integer val2 = converter2.convert("100");
// 2 引用特定物件的例項方法
// 使用 Lmabda 表示式建立 Converter 物件
Converter converter3 = from -> "hello michael翔".indexOf(from);
// 呼叫 "hello michael翔"的indexOf()例項方法
// 函式式介面中被實現的全部引數傳給該方法作為引數
Converter converter4 = "hello michael翔"::indexOf;
// 3 引用某類物件的例項方法
// 使用 Lambda 表示式建立 MyTest 物件
MyTest mt = (a, b, c) -> a.substring(b, c);
String str = mt.test("Hello World, Hello Michael翔", 2,9);
// 上面 Lambda 表示式只有一行,因此可以使用如下引用進行替換
// 函式式介面中被實現方法的第一個引數作為呼叫者
// 後面的引數全部傳給該方法作為引數
MyTest str2 = String::substring;
// 4 引用構造器
// 使用 Lambda 表示式建立 YourTest 物件
YourTest yt = a -> new JFrame(a);
JFrame jf = yt.win("視窗");
// 使用構造器引用進行替換
// 函式式介面中被實現方法的全部引數傳給該構造器作為引數
YourTest yt2 = JFrame::new;
JFrame jf2 = yt.win("視窗2");
}
}
Lambda 表示式與匿名內部類的聯絡與區別
Lambda 表示式與匿名內部類存在如下相同點:
- Lambda 表示式與匿名內部類一樣,都可以直接訪問
effectively final
的區域性變數,以及外部類的成員變數(包括示例變數和類變數); - Lambda 表示式建立的物件與匿名內部類生成的物件一樣,都可以直接呼叫從介面中繼承的預設方法;
Lambda 表示式與匿名內部類的區別:
- 匿名內部類可以為任意介面建立例項,不管介面包含多少個抽象方法,只要匿名內部類實現所有抽象方法即可;但是 Lambda 表示式只能為函式式介面建立例項;
- 匿名內部類可以為抽象類甚至普通類建立例項,但是 Lambda 表示式只能為函式式介面建立例項;
- 匿名內部類實現的抽象方法體允許呼叫介面中定義的預設方法,但是 Lambda 表示式的程式碼塊不允許呼叫介面中定義的預設方法;
@FunctionalInterface
interface Converter {
Integer convert(String from);
}
@FunctionalInterface
interface MyTest {
String test(String a, int b, int c);
}
@FunctionalInterface
interface YourTest {
// 抽象方法負責根據 String 引數生成一個 JFrame 返回值
JFrame win(String title);
}
public class LambdaRef {
public static void main(String[] args) {
// 1 引用類方法
// 下面使用 Lambda 表示式建立 Converter 物件
Converter converter1 = from -> Integer.valueOf(from);
Integer val = converter1.convert("99");
// 函式式介面中被實現方法的全部引數傳給該類方法作為引數
Converter converter2 = Integer::valueOf;
Integer val2 = converter2.convert("100");
// 2 引用特定物件的例項方法
// 使用 Lmabda 表示式建立 Converter 物件
Converter converter3 = from -> "hello michael翔".indexOf(from);
// 呼叫 "hello michael翔"的indexOf()例項方法
// 函式式介面中被實現的全部引數傳給該方法作為引數
Converter converter4 = "hello michael翔"::indexOf;
// 3 引用某類物件的例項方法
// 使用 Lambda 表示式建立 MyTest 物件
MyTest mt = (a, b, c) -> a.substring(b, c);
String str = mt.test("Hello World, Hello Michael翔", 2,9);
// 上面 Lambda 表示式只有一行,因此可以使用如下引用進行替換
// 函式式介面中被實現方法的第一個引數作為呼叫者
// 後面的引數全部傳給該方法作為引數
MyTest str2 = String::substring;
// 4 引用構造器
// 使用 Lambda 表示式建立 YourTest 物件
YourTest yt = a -> new JFrame(a);
JFrame jf = yt.win("視窗");
// 使用構造器引用進行替換
// 函式式介面中被實現方法的全部引數傳給該構造器作為引數
YourTest yt2 = JFrame::new;
JFrame jf2 = yt.win("視窗2");
}
}
Lambda 表示式呼叫 Arrays 的類方法
Arrays 類的有些方法需要 Comparator、XxxOperator、XxxFunction 等介面的例項,這些介面都是函式式介面。因此,可以使用 Lambda 表示式來呼叫 Arrays 的方法。
public class LambdaArrays {
public static void main(String[] args) {
String[] arr1 = new String[]{"java", "python", "rust", "go"};
Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
System.out.println(Arrays.toString(arr1));
int[] arr2 = {3, -4, 25, 16, 30, 18};
// left 代表陣列中前一個索引處的元素,計算第一個元素時,left 為 1;
// right 代表陣列中的當前索引處的元素
Arrays.parallelPrefix(arr2, (left, right) -> left * right);
System.out.println(Arrays.toString(arr2));
long[] arr3 = new long[5];
// a 代表正在計算的元素索引
Arrays.parallelSetAll(arr3, a -> a * 5);
System.out.println(Arrays.toString(arr3));
// 等價於用匿名內部類重寫 applyAsLong 抽象方法
Arrays.parallelSetAll(arr3, new IntToLongFunction() {
@Override
public long applyAsLong(int value) {
return value * 5;
}
});
System.out.println(Arrays.toString(arr3));
}
}
輸出:
[go, java, rust, python]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
[0, 5, 10, 15, 20]
因為這些要出入 Comparator、XxxOperator、XxxFunction 等介面的例項往往都是一次性的,使用 Lambda 表示式也不用考慮重用等,反而讓程式更加簡潔了。
總結
本文主要參考的是 《瘋狂 Java 講義第 5 版》的第 6 章的物件導向下,通過實際的示例 demo 應該可以將 Lambda 的常用場景和用法掌握了。這樣,看專案程式碼或者原始碼的話,會更加易於理解!基本功紮實,才能走得更快!