8000字長文讓你徹底瞭解 Java 8 的 Lambda、函式式介面、Stream 用法和原理
阿新 • • 發佈:2020-06-10
> 我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程式設計師鼓勵師,一個本打算寫詩卻寫起了程式碼的田園碼農!
文章會收錄在 [JavaNewBee](https://github.com/huzhicheng/JavaNewBee) 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裡面。公眾號回覆『666』獲取高清大圖。
就在今年 Java 25週歲了,可能比在座的各位中的一些少年年齡還大,但令人遺憾的是,竟然沒有我大,不禁感嘆,Java 還是太小了。(難道我會說是因為我老了?)
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gflsl3vuvgj308t04fq2w.jpg)
而就在上個月,Java 15 的試驗版悄悄釋出了,但是在 Java 界一直有個神祕現象,那就是「你發你發任你發,我的最愛 Java 8」.
據 Snyk 和 The Java Magazine 聯合推出釋出的 2020 JVM 生態調查報告顯示,在所有的 Java 版本中,仍然有 64% 的開發者使用 Java 8。另外一些開發者可能已經開始用 Java 9、Java 11、Java 13 了,當然還有一些神仙開發者還在堅持使用 JDK 1.6 和 1.7。
儘管 Java 8 釋出多年,使用者眾多,可神奇的是竟然有很多同學沒有用過 Java 8 的新特性,比如 **Lambda**表示式、比如**方法引用**,再比如今天要說的 **Stream**。其實 Stream 就是以 Lambda 和方法引用為基礎,封裝的簡單易用、函式式風格的 API。
Java 8 是在 2014 年釋出的,實話說,風箏我也是在 Java 8 釋出後很長一段時間才用的 Stream,因為 Java 8 釋出的時候我還在 C# 的世界中掙扎,而使用 Lambda 表示式卻很早了,因為 Python 中用 Lambda 很方便,沒錯,我寫 Python 的時間要比 Java 的時間還長。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gflsokyf1ij30c80ad74h.jpg)
要講 Stream ,那就不得不先說一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其實就是函式式的程式設計風格,其中的「函式」就是方法引用,「式」就是 Lambda 表示式。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfjlb7skv3j31a40kuq4y.jpg)
## Lambda 表示式
> Lambda 表示式是一個[匿名函式](https://baike.baidu.com/item/匿名函式/4337265),Lambda表示式基於數學中的[λ演算](https://baike.baidu.com/item/λ演算)得名,直接對應於其中的lambda抽象,是一個匿名函式,即沒有函式名的函式。Lambda表示式可以表示閉包。
在 Java 中,Lambda 表示式的格式是像下面這樣
```java
// 無引數,無返回值
() -> log.info("Lambda")
// 有引數,有返回值
(int a, int b) -> { a+b }
```
其等價於
```java
log.info("Lambda");
private int plus(int a, int b){
return a+b;
}
```
最常見的一個例子就是新建執行緒,有時候為了省事,會用下面的方法建立並啟動一個執行緒,這是匿名內部類的寫法,`new Thread`需要一個 implements 自`Runnable`型別的物件例項作為引數,比較好的方式是建立一個新類,這個類 `implements Runnable`,然後 new 出這個新類的例項作為引數傳給 Thread。而匿名內部類不用找物件接收,直接當做引數。
```java
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("快速新建並啟動一個執行緒");
}
}).run();
```
但是這樣寫是不是感覺看上去很亂、很土,而這時候,換上 Lambda 表示式就是另外一種感覺了。
```java
new Thread(()->{
System.out.println("快速新建並啟動一個執行緒");
}).run();
```
怎麼樣,這樣一改,瞬間感覺清新脫俗了不少,簡潔優雅了不少。
Lambda 表示式簡化了匿名內部類的形式,可以達到同樣的效果,但是 Lambda 要優雅的多。雖然最終達到的目的是一樣的,但其實內部的實現原理卻不相同。
匿名內部類在編譯之後會建立一個新的匿名內部類出來,而 Lambda 是呼叫 JVM `invokedynamic`指令實現的,並不會產生新類。
## 方法引用
方法引用的出現,使得我們可以將一個方法賦給一個變數或者作為引數傳遞給另外一個方法。`::`雙冒號作為方法引用的符號,比如下面這兩行語句,引用 `Integer`類的 `parseInt`方法。
```java
Function s = Integer::parseInt;
Integer i = s.apply("10");
```
或者下面這兩行,引用 `Integer`類的 `compare`方法。
```java
Comparator comparator = Integer::compare;
int result = comparator.compare(100,10);
```
再比如,下面這兩行程式碼,同樣是引用 `Integer`類的 `compare`方法,但是返回型別卻不一樣,但卻都能正常執行,並正確返回。
```java
IntBinaryOperator intBinaryOperator = Integer::compare;
int result = intBinaryOperator.applyAsInt(10,100);
```
相信有的同學看到這裡恐怕是下面這個狀態,完全不可理喻嗎,也太隨便了吧,返回給誰都能接盤。
先別激動,來來來,現在咱們就來解惑,解除蒙圈臉。
**Q:什麼樣的方法可以被引用?**
A:這麼說吧,任何你有辦法訪問到的方法都可以被引用。
**Q:返回值到底是什麼型別?**
A:這就問到點兒上了,上面又是 `Function`、又是`Comparator`、又是 `IntBinaryOperator`的,看上去好像沒有規律,其實不然。
返回的型別是 Java 8 專門定義的函式式介面,這類介面用 `@FunctionalInterface` 註解。
比如 `Function`這個函式式介面的定義如下:
```java
@FunctionalInterface
public interface Function {
R apply(T t);
}
```
還有很關鍵的一點,你的引用方法的引數個數、型別,返回值型別要和函式式介面中的方法宣告一一對應才行。
比如 `Integer.parseInt`方法定義如下:
```java
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}
```
首先`parseInt`方法的引數個數是 1 個,而 `Function`中的 `apply`方法引數個數也是 1 個,引數個數對應上了,再來,`apply`方法的引數型別和返回型別是泛型型別,所以肯定能和 `parseInt`方法對應上。
這樣一來,就可以正確的接收`Integer::parseInt`的方法引用,並可以呼叫`Funciton`的`apply`方法,這時候,呼叫到的其實就是對應的 `Integer.parseInt`方法了。
用這套標準套到 `Integer::compare`方法上,就不難理解為什麼即可以用 `Comparator`接收,又可以用 `IntBinaryOperator`接收了,而且呼叫它們各自的方法都能正確的返回結果。
`Integer.compare`方法定義如下:
```java
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
```
返回值型別 `int`,兩個引數,並且引數型別都是 `int`。
然後來看`Comparator`和`IntBinaryOperator`它們兩個的函式式介面定義和其中對應的方法:
```java
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
}
@FunctionalInterface
public interface IntBinaryOperator {
int applyAsInt(int left, int right);
}
```
對不對,都能正確的匹配上,所以前面示例中用這兩個函式式介面都能正常接收。其實不止這兩個,只要是在某個函式式介面中聲明瞭這樣的方法:兩個引數,引數型別是 `int`或者泛型,並且返回值是 `int`或者泛型的,都可以完美接收。
JDK 中定義了很多函式式介面,主要在 `java.util.function`包下,還有 `java.util.Comparator` 專門用作定製比較器。另外,前面說的 `Runnable`也是一個函式式介面。
## 自己動手實現一個例子
**1. 定義一個函式式介面,並新增一個方法**
定義了名稱為 KiteFunction 的函式式介面,使用 `@FunctionalInterface`註解,然後聲明瞭具有兩個引數的方法 `run`,都是泛型型別,返回結果也是泛型。
還有一點很重要,函式式介面中只能宣告一個可被實現的方法,你不能聲明瞭一個 `run`方法,又宣告一個 `start`方法,到時候編譯器就不知道用哪個接收了。而用`default` 關鍵字修飾的方法則沒有影響。
```java
@FunctionalInterface
public interface KiteFunction {
/**
* 定義一個雙引數的方法
* @param t
* @param s
* @return
*/
R run(T t,S s);
}
```
**2. 定義一個與 KiteFunction 中 run 方法對應的方法**
在 FunctionTest 類中定義了方法 `DateFormat`,一個將 `LocalDateTime`型別格式化為字串型別的方法。
```java
public class FunctionTest {
public static String DateFormat(LocalDateTime dateTime, String partten) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
return dateTime.format(dateTimeFormatter);
}
}
```
**3.用方法引用的方式呼叫**
正常情況下我們直接使用 `FunctionTest.DateFormat()`就可以了。
而用函式式方式,是這樣的。
```java
KiteFunction functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
```
而其實我可以不專門在外面定義 `DateFormat`這個方法,而是像下面這樣,使用匿名內部類。
```java
public static void main(String[] args) throws Exception {
String dateString = new KiteFunction() {
@Override
public String run(LocalDateTime localDateTime, String s) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
return localDateTime.format(dateTimeFormatter);
}
}.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
System.out.println(dateString);
}
```
前面第一個 `Runnable`的例子也提到了,這樣的匿名內部類可以用 Lambda 表示式的形式簡寫,簡寫後的程式碼如下:
```java
public static void main(String[] args) throws Exception {
KiteFunction functionDateFormat = (LocalDateTime dateTime, String partten) -> {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
return dateTime.format(dateTimeFormatter);
};
String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
System.out.println(dateString);
}
```
使用(LocalDateTime dateTime, String partten) -> { } 這樣的 Lambda 表示式直接返回方法引用。
## Stream API
為了說一下 Stream API 的使用,可以說是大費周章啊,知其然,也要知其所以然嗎,追求技術的態度和姿勢要正確。
當然 Stream 也不只是 Lambda 表示式就厲害了,真正厲害的還是它的功能,Stream 是 Java 8 中集合資料處理的利器,很多本來複雜、需要寫很多程式碼的方法,比如過濾、分組等操作,往往使用 Stream 就可以在一行程式碼搞定,當然也因為 Stream 都是鏈式操作,一行程式碼可能會呼叫好幾個方法。
`Collection`介面提供了 `stream()`方法,讓我們可以在一個集合方便的使用 Stream API 來進行各種操作。值得注意的是,我們執行的任何操作都不會對源集合造成影響,你可以同時在一個集合上提取出多個 stream 進行操作。
我們看 Stream 介面的定義,繼承自 `BaseStream`,機會所有的介面宣告都是接收方法引用型別的引數,比如 `filter`方法,接收了一個 `Predicate`型別的引數,它就是一個函式式介面,常用來作為條件比較、篩選、過濾用,`JPA`中也使用了這個函式式介面用來做查詢條件拼接。
```java
public interface Stream extends BaseStream> {
Stream filter(Predicate super T> predicate);
// 其他介面
}
```
下面就來看看 Stream 常用 API。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfjlk255zqj30po1ha77v.jpg)
### of
可接收一個泛型物件或可變成泛型集合,構造一個 Stream 物件。
```java
private static void createStream(){
Stream stringStream = Stream.of("a","b","c");
}
```
### empty
建立一個空的 Stream 物件。
### concat
連線兩個 Stream ,不改變其中任何一個 Steam 物件,返回一個新的 Stream 物件。
```java
private static void concatStream(){
Stream a = Stream.of("a","b","c");
Stream b = Stream.of("d","e");
Stream c = Stream.concat(a,b);
}
```
### max
一般用於求數字集合中的最大值,或者按實體中數字型別的屬性比較,擁有最大值的那個實體。它接收一個 `Comparator`,上面也舉到這個例子了,它是一個函式式介面型別,專門用作定義兩個物件之間的比較,例如下面這個方法使用了 `Integer::compareTo`這個方法引用。
```java
private static void max(){
Stream integerStream = Stream.of(2, 2, 100, 5);
Integer max = integerStream.max(Integer::compareTo).get();
System.out.println(max);
}
```
當然,我們也可以自己定製一個 `Comparator`,順便複習一下 Lambda 表示式形式的方法引用。
```java
private static void max(){
Stream integerStream = Stream.of(2, 2, 100, 5);
Comparator comparator = (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
Integer max = integerStream.max(comparator).get();
System.out.println(max);
}
```
### min
與 max 用法一樣,只不過是求最小值。
### findFirst
獲取 Stream 中的第一個元素。
### findAny
獲取 Stream 中的某個元素,如果是序列情況下,一般都會返回第一個元素,並行情況下就不一定了。
### count
返回元素個數。
```java
Stream a = Stream.of("a", "b", "c");
long x = a.count();
```
### peek
建立一個通道,在這個通道中對 Stream 的每個元素執行對應的操作,對應 `Consumer`的函式式介面,這是一個消費者函式式介面,顧名思義,它是用來消費 Stream 元素的,比如下面這個方法,把每個元素轉換成對應的大寫字母並輸出。
```java
private static void peek() {
Stream a = Stream.of("a", "b", "c");
List list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
}
```
### forEach
和 peek 方法類似,都接收一個消費者函式式介面,可以對每個元素進行對應的操作,但是和 peek 不同的是,`forEach` 執行之後,這個 Stream 就真的被消費掉了,之後這個 Stream 流就沒有了,不可以再對它進行後續操作了,而 `peek`操作完之後,還是一個可操作的 Stream 物件。
正好藉著這個說一下,我們在使用 Stream API 的時候,都是一串鏈式操作,這是因為很多方法,比如接下來要說到的 `filter`方法等,返回值還是這個 Stream 型別的,也就是被當前方法處理過的 Stream 物件,所以 Stream API 仍然可以使用。
```java
private static void forEach() {
Stream a = Stream.of("a", "b", "c");
a.forEach(e->System.out.println(e.toUpperCase()));
}
```
### forEachOrdered
功能與 `forEach`是一樣的,不同的是,`forEachOrdered`是有順序保證的,也就是對 Stream 中元素按插入時的順序進行消費。為什麼這麼說呢,當開啟並行的時候,`forEach`和 `forEachOrdered`的效果就不一樣了。
```java
Stream a = Stream.of("a", "b", "c");
a.parallel().forEach(e->System.out.println(e.toUpperCase()));
```
當使用上面的程式碼時,輸出的結果可能是 B、A、C 或者 A、C、B或者A、B、C,而使用下面的程式碼,則每次都是 A、 B、C
```java
Stream a = Stream.of("a", "b", "c");
a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));
```
### limit
獲取前 n 條資料,類似於 MySQL 的limit,只不過只能接收一個引數,就是資料條數。
```java
private static void limit() {
Stream a = Stream.of("a", "b", "c");
a.limit(2).forEach(e->System.out.println(e));
}
```
上述程式碼列印的結果是 a、b。
### skip
跳過前 n 條資料,例如下面程式碼,返回結果是 c。
```java
private static void skip() {
Stream a = Stream.of("a", "b", "c");
a.skip(2).forEach(e->System.out.println(e));
}
```
### distinct
元素去重,例如下面方法返回元素是 a、b、c,將重複的 b 只保留了一個。
```java
private static void distinct() {
Stream a = Stream.of("a", "b", "c","b");
a.distinct().forEach(e->System.out.println(e));
}
```
### sorted
有兩個過載,一個無引數,另外一個有個 `Comparator`型別的引數。
無參型別的按照自然順序進行排序,只適合比較單純的元素,比如數字、字母等。
```java
private static void sorted() {
Stream a = Stream.of("a", "c", "b");
a.sorted().forEach(e->System.out.println(e));
}
```
有引數的需要自定義排序規則,例如下面這個方法,按照第二個字母的大小順序排序,最後輸出的結果是 a1、b3、c6。
```java
private static void sortedWithComparator() {
Stream a = Stream.of("a1", "c6", "b3");
a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e));
}
```
**為了更好的說明接下來的幾個 API ,我模擬了幾條專案中經常用到的類似資料,10條使用者資訊。**
```java
private static List getUserData() {
Random random = new Random();
List users = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
User user = new User();
user.setUserId(i);
user.setUserName(String.format("古時的風箏 %s 號", i));
user.setAge(random.nextInt(100));
user.setGender(i % 2);
user.setPhone("18812021111");
user.setAddress("無");
users.add(user);
}
return users;
}
```
### filter
用於條件篩選過濾,篩選出符合條件的資料。例如下面這個方法,篩選出性別為 0,年齡大於 50 的記錄。
```java
private static void filter(){
List users = getUserData();
Stream stream = users.stream();
stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e));
/**
*等同於下面這種形式 匿名內部類
*/
// stream.filter(new Predicate() {
// @Override
// public boolean test(User user) {
// return user.getGender().equals(0) && user.getAge()>50;
// }
// }).forEach(e->System.out.println(e));
}
```
### map
`map`方法的介面方法宣告如下,接受一個 `Function`函式式介面,把它翻譯成對映最合適了,通過原始資料元素,映射出新的型別。
```java
Stream map(Function super T, ? extends R> mapper);
```
而 `Function`的宣告是這樣的,觀察 `apply`方法,接受一個 T 型引數,返回一個 R 型引數。用於將一個型別轉換成另外一個型別正合適,這也是 `map`的初衷所在,用於改變當前元素的型別,例如將 `Integer` 轉為 `String`型別,將 DAO 實體型別,轉換為 DTO 例項型別。
當然了,T 和 R 的型別也可以一樣,這樣的話,就和 `peek`方法沒什麼不同了。
```java
@FunctionalInterface
public interface Function {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
```
例如下面這個方法,應該是業務系統的常用需求,將 User 轉換為 API 輸出的資料格式。
```java
private static void map(){
List users = getUserData();
Stream stream = users.stream();
List userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
}
private static UserDto dao2Dto(User user){
UserDto dto = new UserDto();
BeanUtils.copyProperties(user, dto);
//其他額外處理
return dto;
}
```
### mapToInt
將元素轉換成 int 型別,在 `map`方法的基礎上進行封裝。
### mapToLong
將元素轉換成 Long 型別,在 `map`方法的基礎上進行封裝。
### mapToDouble
將元素轉換成 Double 型別,在 `map`方法的基礎上進行封裝。
### flatMap
這是用在一些比較特別的場景下,當你的 Stream 是以下這幾種結構的時候,需要用到 `flatMap`方法,用於將原有二維結構扁平化。
1. `Stream`
2. `Stream>`
3. `Stream
- >`
以上這三類結構,通過 `flatMap`方法,可以將結果轉化為 `Stream
- > `扁平處理,然後再使用 `map`或其他方法進行操作。
```java
private static void flatMap(){
List
- > userList = new ArrayList<>();
userList.add(users);
userList.add(users1);
Stream
- > stream = userList.stream();
List