Lambda
表達式因其簡潔、易讀、直觀、易理解的特點,顯然已經成爲各大編程語言的開發者最喜愛的語法之一。
首先,我們先來了解一下 Lambda
表達式在幾種主流語言中的寫法,研究一下其共同的特點。
Python
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
print filter(lambda x: x % 3 == 0, arr)
# 輸出: [3, 6, 9]
C#
// C# 中的 Lambda 表達式常用於 Func 和 Action 委託
Func<int, int, int> add = (c1, c2) => c1 + c2;
int sum = add(2, 5);
// sum: 7
JavaScript
//ES6 爲 JS 也新增了 Lambda 表達式寫法,也稱之爲箭頭函數
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
var result = arr.map(i => i * 10)
// result: [10, 20, 30, 40, 50, 60, 70, 80, 90, 0]
Java8
public interface Add{
void add(int t1, int t2);
}
public class Test{
public static void main(String[] args){
Add s = (t1, t2) -> System.out.println(t1 + t2);
s.add(1, 2);
// 輸出: 3
}
}
分析,綜合以上四種語言的 Lambda
表達式寫法,可以看出,不同語言的 Lambda
表達式大同小異,主要由三部分構成:參數、特殊代表符號、函數/表達式
我們主要來研究 Java 8
中的 Lambda
表達式
Lamda 表達式的組成
在 Java 8
中, Lambda
表達式由三部分組成:參數、箭頭、主體
例如:
(int i, int j) -> { return i + j; }
- 參數
- 參數含參數類型、參數名稱,由小括號包裹,逗號隔開。
(int i, int j) -> { return i + j; }
- 參數類型可以省略。
(i, j) -> { return i + j; }
- 參數只有一個時,小括號可以省略。
i -> { return i * 10; }
- 無參數時,小括號中爲空。
() -> System.out.print("Hello")
- 參數含參數類型、參數名稱,由小括號包裹,逗號隔開。
- 特殊符號
- 箭頭符號,由短橫線和大於號組成.
->
- 箭頭符號,由短橫線和大於號組成.
- 函數語句/表達式
- 主體爲語句時,需要使用花括號。
i -> { return i * 10; }
- 主體爲表達式時,不能使用花括號。
i -> i * 10
- 主體爲語句時,需要使用花括號。
函數式接口
剛開始我們舉例了 4 種開發語言的
Lambda
表達式語法,不知有沒有細心的小夥伴發現一個問題:
Python
和JavaScript
的例子中,我們調用了原有庫中自帶的filter
和map
方法演示;而
C#
和 Java 的例子中,我們是創建了自定義的add
方法來演示的。
C#
中,我們把幫助定義add
方法的Func
稱之爲委託;那麼,
Java
中,名爲Add
的Interface
又是什麼呢?它又有什麼作用呢?這個
Add
接口,在Java
中,稱之爲 函數式接口。
在 Java
中, 函數式接口是使用 Lambda
表達式的必要條件,二者是不可分割的。
簡單來說,函數式接口就是只定義一個抽象方法的接口。
Lambda
表達式允許以內聯的形式爲函數式接口的抽象方法直接提供實現,並將整個表達式作爲函數式接口的實例。
舉個例子: 我們查看一下熟悉的 java.lang.Runnable
接口的源碼
public interface Runnable {
void run();
}
發現,其內部有且僅有一個返回值爲 void
的抽象方法 run()
,滿足函數式接口的定義,所以,Runnable
接口就是一個函數式接口。
在 Java 8
之前,我們使用 Runnable
接口創建多線程:
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.print("Hello World");
}
}
public class Test{
public static void main(String[] args){
MyRunnable r1 = new MyRunnable();
new Thread(r1).start();
}
}
或者,可以使用匿名方法簡化代碼:
public class Test{
public static void main(String[] args){
new Thread(new Runnable(){
@Override
public void run(){
System.out.print("Hello World");
}
}).start();
}
}
而在 Java 8
中,我們可以使用 Lambda
表達式:
public class Test{
public static void main(String[] args){
/* 使用 Lambda 表達式, 一行代碼創建多線程*/
new Thread(() -> System.out.print("Hello World")).start();
}
}
看到這裏,你應該也明白爲什麼
Lambda
表達式一定要與函數式接口相依存了。因爲,
Lambda
表達式是以內聯形式直接爲接口的抽象方法提供實現的。如果接口中有多個抽象方法,Lambda
表達式就無法確定傳遞的是哪個抽象方法的實現了。那如何來區分某個接口是需要被定義爲函數式接口,還是僅僅臨時只有一個抽象方法,以後還有可能追加其他方法的普通接口呢?
又或者說,如何來保證某個接口是函數式接口,避免後期被誤改呢?
@FunctionalInterface
註解,就是專門用來檢測某個接口是否爲函數式接口的。當標註該註解時,如果標註的接口不是函數式接口,編譯器將返回一個錯誤提示,如:
Multiple non-overriding abstract methods found in interface foo
註解不是必須的,但是必要的。
四大常用函數式接口
上面提到,
Lambda
表達式必須有函數式接口的支持,那是否意味着我們每次寫Lambda
表達式都需要自定義函數式接口呢?其實大多數時候,我們並不需要去自定義函數式接口,因爲
Java 8
的API
中,已經爲我們封裝好了一些實用的函數式接口。
這些接口封裝在 java.util.function
包中,主要分爲以下四類:
-
Predicate 類型
Predicate
類型接口又稱之爲斷言型接口或者謂詞接口。其抽象方法爲
test()
, 接收泛型T
對象, 返回boolean
值。因其返回
boolean
值,且擁有特有的謂詞方法(點此直達),所以經常被用來做判斷、過濾條件等。例如,可以使用它擴展一個
filter
方法來按照指定條件過濾集合:// 利用 Predicate 定義 filter 方法按照指定條件過濾集合 public static <T> List<T> filter(List<T> list, Predicate<T> pre){ List<T> result = new ArrayList<>(); for(T t: list){ // 遍歷傳入的泛型集合 if(pre.test(t)){ // 如果集合中元素滿足 test 方法 result.add(t); // 將元素追加到新集合 } } return result; } // 使用 filter 方法過濾集合,只返回 能被 3 整除的數字 List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0); List<Integer> result = filter(nums, i -> i % 3 == 0); // result: [3, 6, 9, 0]
-
Consumer 類型
Consumer
類型接口又稱之爲消費型接口。其抽象方法爲
accept()
, 接收泛型T
對象,無返回值。因其無返回值,所以經常被用來執行某個操作。
例如,可以使用它擴展一個
foreach
方法遍歷集合://利用 Consumer 定義 foreach 方法遍歷集合並執行指定操作 public static <T> void foreach(List<T> list, Consumer<T> con) { for (T t : list) { con.accept(t); } } // 使用 foreach 方法輸出集合 List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0); foreach(nums, i -> System.out.println("當前遍歷到的數字: " + i)); // 當前遍歷到的數字: 1 // 當前遍歷到的數字: 2 // ...
-
Function 類型
Function
類型接口又稱之爲方法型接口或者函數型接口。其抽象方法爲
apply()
,接收泛型T
對象,返回R
泛型對象。因其返回
R
泛型對象,且擁有特有的函數方法(點此直達),應用相對比較廣泛。例如, 可以使用它擴展一個
map
方法對集合進行指定操作並返回。// 利用 Function 定義 map 方法對集合元素進行指定的操作並返回 public static <T, R> List<R> map(List<T> list, Function<T, R> fun) { List<R> result = new ArrayList<>(); for (T t : list) { result.add(fun.apply(t)); } return result; } // 使用 map 方法對集合的每個元素計算平方並返回 List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0); List<Integer> result = map(nums, i -> i * i); // result: [1, 4, 9, 16, 25, 36, 49, 64, 81, 0]
-
Supplier 類型
Supplier
類型接口又稱之爲供應型接口。其抽象方法爲
get()
,不接受參數,返回R
泛型對象。因其無參數,所以一般被用在不依賴參數的處理場景中。
例如, 可以使用它擴展一個
randomList
方法生成一個一定數量的隨機數集合。// 利用 Supplier 定義 randomList 方法返回一定數量的隨機數集合 List<Double> result = randomList(() -> Math.random(), 3); // result: [0.5441047654548076, 0.055580576644587886, 0.6309881540663]
其他常用函數式接口
除了以上四類最常用的函數式接口之外,
Java 8
中還有一些比較常用的函數式接口,但方法基本都大同小異,便不多做介紹。 如下:
- UnaryOperator
T -> T
- BinaryOperator
(T, T) -> T
- BiPredicate<L, R>
(L, R) -> boolean
- BiConsumer<T, U>
(T, U) -> void
- BiFunction(T, U, R)
(T, U) -> R
除此之外,爲了避免裝箱拆箱操作(在性能方面消耗較大),大多數函數式接口還擁有其對應的原始類型接口。如下:
- DoublePredicate
- IntConsumer
- LongBinaryOperator
- IntFunction
- …
捕獲 Lambda
一般來說,
Lambda
表達式中都只會使用其主體中的參數和變量。但是Lambda
表達式是允許使用自由變量(即外層變量)的。我們把
Lambda
表達式使用主體外的變量的這種行爲稱爲捕獲變量,這種Lambda
表達式稱之爲捕獲Lambda。
Lambda
雖然可以無限制的捕獲其他實例變量和靜態變量,但是,局部變量必須只賦值一次,或者顯示聲明爲final
,換句話說,Lambda
表達式捕獲的變量只能賦值一次。如下:
正確寫法:
int i = 1;
Runnable r = () -> System.out.println(i);
或者:
final int i = 1;
Runnable r = () -> System.out.println(i);
錯誤寫法:
int i = 1;
Runnable r = () -> System.out.println(i); //Variable used in lambda expression should be final or effectively final
i = 2;
爲什麼會有這樣的限制呢?
因爲實例變量存儲在堆中,局部變量存儲在棧中,如果允許
Lambda
直接訪問局部變量,並且Lambda
在一個線程中使用,,則Lambda
線程有可能會在分配該變量的線程已經將該變量回收之後纔去訪問。因此,Java 在訪問自由的局部變量時,實際上是在訪問這個變量的副本,而並非原始變量。只有當該變量只允許賦值一次的情況下,Lambda
表達式訪問原始變量還是副本變量纔不會造成影響。另外,我們並不建議在
Lambda
表達式中去捕獲局部變量。準確來說,我們不建議使用改變外部變量的這種命令式編程模式。
方法引用
看了這麼多例子,
Lambda
表達式簡潔、易讀的特點已經可以說是顯而易見了。那麼,
Lambda
表達式還可以更簡潔一點嗎?當然,當
Lambda
表達式的主體爲一個已有方法時,Lambda
表達式還可以進一步簡寫爲方法引用。
方法引用主要分三類:
-
靜態方法引用
例如:
class User{ public static String getName(String name){ return "My name is " + name; } } public class Test{ @Test public void test(){ /* 常規Lambda寫法 */ Function<String, String> myName1 = (args) -> User.getName(args); /* 方法引用寫法 */ Function<String, String> myName2 = User::getName; } }
-
其他實例方法引用
例如:
class User{ public String sayHello(String words){ return "Hello everyone, Nice to meet you !" + words; } } public class Test{ @Test public void test(){ /* 常規Lambda寫法 */ BiFunction<User, String, String> sayHello1 = (user, words) -> user.sayHello(words); /* 方法引用寫法 */ BiFunction<User, String, String> sayHello12 = User::sayHello; } }
-
當前對象實例方法引用
例如:
class User{ public String sayHello(String words){ return "Hello everyone, Nice to meet you !" + words; } } public class Test{ @Test public void test(){ /* 創建實例 */ User user = new User(); /* 常規Lambda寫法 */ Function<String, String> sayH1 = (words) -> user.sayHello(words); /* 方法引用寫法 */ Function<String, String> sayH2 = user::sayHello; } }
構造函數引用
與方法引用類似,也可以使用
類名+new
關鍵字創建一個對象
例如:
class User{
public User(){
}
public User(String name){
}
public User(String name, int age){
}
}
空參構造:
Supplier<User> user = User::new;
一參構造:
Function<String, User> user2 = User::new;
二參構造:
BiFunction<String, Integer, User> user3 = User::new;
複合使用
實際上,
Java 8
所提供的四大函數式接口中,並不是所有的都只包含一個接口的,比如斷言接口(謂詞接口)中,還包含了一些謂詞(如or
、and
等)來協助Lambda
複合實現更加強大的邏輯。
不是說函數式接口只可以包含一個抽象方法嗎?
沒錯。的確只能包含一個抽象方法,查看源碼可以很容易發現,這裏使用了另一種
Java 8
的新特性 —— 默認方法。也就是說,or
、and
等函數式接口中的方法,其實並不是抽象方法。
謂詞複合
- 且
and
- 或
or
- 非
negate
看一個例子:
class User {
private String firstName;
private String lastName;
private String gender;
private int age;
/* Constructor */
/* Getter & Setter */
}
public class Test {
@Test
public void test() {
Predicate<User> men = user -> "male".equals(user.getGender()); // 所有男性
Predicate<User> women = men.negate(); //所有女性(非男性) ———— 這裏不考慮不男不女的情況 ~
Predicate<User> womenXu = women.and(user -> "Xu".equals(user.getFirstName())); // Xu 姓的女性
Predicate<User> womenXuOrMenWang = women.and(user -> "Xu".equals(user.getFirstName()))
.or(men.and(user -> "Wang".equals(user.getFirstName()))); // Xu 姓的女性或者 Wang 姓的男性
}
}
函數複合
andThen
compose
看一個例子:
//定義兩個函數
Function<Integer, Integer> f = x -> x * 2; // f(x) = x * 2
Function<Integer, Integer> g = x -> x * x; // g(x) = x * x
Function<Integer, Integer> t1 = f.andThen(g); // t1(x) = g(f(x))
Function<Integer, Integer> t2 = f.compose(g); // t2(x) = f(g(x))
/* 調用查看結果 */
int r1 = t1.apply(10); // 400 t1(10) = (10 * 2) * (10 * 2)
int r2 = t2.apply(10); // 200 t2(10) = (10 * 10) * 2
比較器複合
comparing
thenComparing
sort
reversed
- …
看一個例子:
List<User> users = new ArrayList<>();
users.add(new User("FirstName", "LastName", "Male", 18));
/* 構造數據 */
Comparator<User> userOrderByAge = Comparator.comparing(User::getAge); //按年齡排序
Comparator<User> userOrderByAgeReversed = userOrderByAge.reversed(); //按年齡逆序
Comparator<User> userOrderByAgeReversedThenOrderByFirstName =
Comparator.comparing(User::getAge)
.reversed()
.thenComparing(User::getFirstName); //按年齡逆序,相同年齡按姓名首字母排序
users.sort(userOrderByAge);