詳解 Java 中的 Lambda
什么是 Lambda
我們知道,對于一個 Java 變量,我們可以給它賦一個“值”,然后可以用它做一些操作。
Integer a = 1;
String s = "Hello";
System.out.println(s + a);
如果你想給一個 Java 變量賦一段“代碼”,該怎么辦呢?例如,我想把右邊的代碼塊賦給一個名為 codeBlock 的 Java 變量。
在 Java 8 之前,這是不可能的。但在 Java 8 出現(xiàn)后,可以使用 Lambda 特性來做到這一點。
以下就是最直觀的寫法:
實際上是不允許這樣寫的會編譯失敗,這里只是為了讓大家方便理解
codeBlock = public void doSomething(String s) {
System.out.println(s);
}
這種寫法不是很簡潔。我們可以去掉一些無用的聲明對代碼進行簡化。
codeBlock = public void doSomething(String s) {
System.out.println(s);
}
// 這里的 public 是多余的,因為在這個上下文中不需要訪問修飾符。
codeBlock = void doSomething(String s) {
System.out.println(s);
}
// 函數(shù)名 doSomething 也是多余的,因為已經將函數(shù)體賦值給了 codeBlock。
codeBlock = void (String s) {
System.out.println(s);
}
// 編譯器可以自行推斷返回類型,這里不需要顯式地寫出 void。
codeBlock = (String s) {
System.out.println(s);
}
// 編譯器可以自行推斷輸入?yún)?shù)類型,這里不需要顯式地寫出 String 類型。
codeBlock = (s) -> System.out.println(s);
這樣,我們就將一段“代碼”賦給了一個變量。而“這段代碼”,或者說“賦給變量的這個函數(shù)”,就是一個 Lambda 表達式。
但這里還有一個問題,即變量 codeBlock 應該是什么類型呢?在 Java 8 中,所有 Lambda 類型都是一個接口,而 Lambda 表達式本身,也就是“這段代碼”,需要是這個接口的一個實現(xiàn)。在我看來,這是理解 Lambda 的關鍵。簡而言之,Lambda 表達式本身就是一個接口的實現(xiàn)。直接這么說可能還是有點讓人困惑,所以我們繼續(xù)舉例。我們給上面的 codeBlock 添加一個類型:
codeBlock = (s)->System.out.println(s);
interface LambdaInterface {
public void doSomething(String s);
}
這種只有一個函數(shù)需要實現(xiàn)的接口稱為“函數(shù)式接口”。為了防止后來的人給這個接口添加接口函數(shù),導致有多個接口函數(shù)需要實現(xiàn)而變成“非函數(shù)式接口”,我們可以給這個接口添加一個聲明@FunctionalInterface,這樣其他人就不能給它添加新函數(shù)了。
@FunctionalInterface
interface LambdaInterface {
public void doSomething(String s);
}
這樣,我們就得到了一個完整的 Lambda 表達式聲明。
LambdaInterface codeBlock =(s)System.out.println(s);
Lambda 表達式的作用是什么
最直觀的作用就是使代碼極其簡潔。我們可以比較一下 Lambda 表達式和傳統(tǒng) Java 對同一接口的實現(xiàn):
interface LambdaInterface {
public void doSomething(String s);
}
// Java 8
LambdaInterface codeBlock = (s) -> System.out.println(s);
// Java 7
publicclass LambdaInterfaceImpl implements LambdaInterface {
@Override
public void doSomething(String s) {
System.out.println(s);
}
}
這兩種寫法本質上是等價的。但顯然,Java 8 中的寫法更優(yōu)雅簡潔。而且,由于 Lambda 可以直接賦給變量,我們可以直接將 Lambda 作為參數(shù)傳遞給函數(shù),而 java7 必須有明確的接口實現(xiàn)和初始化定義:
// 定義了一個靜態(tài)方法 useLambda,它接受一個 LambdaInterface 類型的參數(shù)和一個 String 類型的參數(shù)。
public static void useLambda(LambdaInterface lambdaInterface, String s) {
lambdaInterface.doSomething(s);
}
// Java 8
// 直接使用 Lambda 表達式調用 useLambda 方法。
useLambda(s -> System.out.println(s), "Hello");
// Java 7
// 定義了一個 LambdaInterface 接口和一個實現(xiàn)該接口的 LambdaInterfaceImpl 類。
interface LambdaInterface {
public void doSomething(String s);
}
publicclass LambdaInterfaceImpl implements LambdaInterface {
@Override
public void doSomething(String s) {
System.out.println(s);
}
}
// 實例化 LambdaInterfaceImpl 類,并將實例傳遞給 useLambda 方法。
LambdaInterface myLambdaInterface = new LambdaInterfaceImpl();
useLambda(myLambdaInterface, "Hello");
在某些情況下,這個接口實現(xiàn)只需要使用一次。Java 7 要求你定義一個接口然后實現(xiàn)它。相比之下,Java 8 的 Lambda 看起來干凈得多。Lambda 結合了函數(shù)式接口庫、forEach、stream()、方法引用等新特性,使代碼更加簡潔!我們直接看例子。
@Getter
@AllArgsConstructor
public static class Student {
private String name;
private Integer age;
}
List<Student> students = Arrays.asList(
new Student("Bob", 18),
new Student("Ted", 17),
new Student("Zeka", 18)
);
現(xiàn)在你需要打印出 students 中所有 18 歲學生的名字。
原始的 Lambda 寫法:定義兩個函數(shù)式接口,定義一個靜態(tài)函數(shù),調用靜態(tài)函數(shù)并給參數(shù)賦值 Lambda 表達式。
@FunctionalInterface
interface AgeMatcher {
boolean match(Student student);
}
@FunctionalInterface
interface Executor {
boolean execute(Student student);
}
public static void matchAndExecute(List<Student> students, AgeMatcher matcher, Executor executor) {
for (Student student : students) {
if (matcher.match(student)) {
executor.execute(student);
}
}
}
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Bob", 18),
new Student("Ted", 17),
new Student("zeka", 18)
);
matchAndExecute(students,
s -> s.getAge() == 18,
s -> System.out.println(s.getName())
);
}
這段代碼實際上已經比較簡潔了,但我們還能更簡潔嗎?當然可以,Java 8 中有一個函數(shù)式接口包,它定義了大量可能用到的函數(shù)式接口(java.util.function (Java Platform SE 8))。
因此,我們根本不需要在這里定義 AgeMatcher 和 Executor 這兩個函數(shù)式接口。我們可以直接使用 Java 8 函數(shù)式接口包中的 Predicate(T) 和 Consumer(T),因為它們的一對接口定義實際上與 AgeMatcher/Executor 相同。
第一步簡化:利用函數(shù)式接口
public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {
for (Student student : students) {
if (predicate.test(student)) {
consumer.accept(student);
}
}
}
matchAndExecute 中的 forEach 循環(huán)實際上很煩人。這里可以使用 Iterable 自帶的 forEach 代替。forEach 本身可以接受一個 Consumer(T) 參數(shù)。
第二步簡化:用 Iterable.forEach 代替 forEach 循環(huán):
public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {
students.forEach(s -> {
if (predicate.test(s)) {
consumer.accept(s);
}
});
}
由于 matchAndExecute 實際上只是對 List 的一個操作,這里我們可以去掉 matchAndExecute,直接使用 stream() 特性來完成它。stream() 的幾個方法接受 Predicate(T) 和 Consumer(T) 等參數(shù)(java.util.stream (Java Platform SE 8))。一旦你理解了上面的內容,stream() 就很容易理解,不需要進一步解釋。
第三步簡化:用 stream() 代替靜態(tài)函數(shù):
students.stream()
.filter(s -> s.getAge() == 18)
.forEach(s -> System.out.println(s.getName()));
與最初的 Lambda 寫法相比代碼量已經減少了非常多。但如果我們要求改為打印學生的所有信息,并且s -> System.out.println(s);那么我們可以使用方法引用來繼續(xù)簡化。所謂方法引用,就是用已經編寫好的其他 Object/Class 的方法來代替 Lambda 表達式。格式如下:
第四步簡化:可以在 forEach 中使用方法引用代替 Lambda 表達式:
students.stream()
.filter(s -> s.getAge() == 18)
.map(Student::getName)
.forEach(System.out::println);
這基本上是我能寫出的最簡潔的版本了。
關于 Java 中的 Lambda 還有一些需要討論和學習的地方。例如,如何利用 Lambda 的特性進行并行處理等??傊?,我只是給你一個大致的介紹,讓你有個概念。網(wǎng)上有很多關于 Lambda 的相關教程,多讀多練,隨著時間的推移肯定能夠掌握它。