當前時間:2019年 11月 11日,距離 JDK 14 釋出時間(2020年3月17日)還有多少天?
// 距離JDK 14 釋出還有多少天?
LocalDate jdk14 = LocalDate.of(2020, 3, 17);
LocalDate nowDate = LocalDate.now();
System.out.println("距離JDK 14 釋出還有:"+nowDate.until(jdk14,ChronoUnit.DAYS)+"天");
複製程式碼
1. 前言
Java 8
早已經在2014 年 3月 18日釋出,毫無疑問 Java 8
對 Java 來說絕對算得上是一次重大版本更新,它包含了十多項語言、庫、工具、JVM 等方面的新特性。比如提供了語言級的匿名函式,也就是被官方稱為 Lambda
的表示式語法(外界也稱為閉包,Lambda
的引入也讓流式操作成為可能,減少了程式碼編寫的複雜性),比如函式式介面,方法引用,重複註解。再比如 Optional
預防空指標,Stearm
流式操作,LocalDateTime
時間操作等。
在前面的文章裡已經介紹了 Java 8
的部分新特性。
這一次主要介紹一下 Lambda 的相關情況。
2. Lambda 介紹
Lambda
名字來源於希臘字母表中排序第十一位的字母 λ,大寫為Λ,英語名稱為 Lambda
。在 Java 中 Lambda
表示式(lambda expression)是一個匿名函式,在編寫 Java 中的 Lambda
的時候,你也會發現 Lambda
不僅沒有函式名稱,有時候甚至連入參和返回都可以省略,這也讓程式碼變得更加緊湊。
3. 函式介面介紹
上面說了這次是介紹 Lambda
表示式,為什麼要介紹函式介面呢?其實 Java 中的函式介面在使用時,可以隱式的轉換成 Lambda
表示式,在 Java 8
中已經有很多介面已經宣告為函式介面,如 Runnable、Callable、Comparator 等。
函式介面的例子可以看下 Java 8
中的 Runnable
原始碼(去掉了註釋)。
package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
複製程式碼
那麼什麼樣子的介面才是函式介面呢?有一個很簡單的定義,也就是隻有一個抽象函式
的介面,函式介面使用註解 @FunctionalInterface
進行宣告(註解宣告不是必須的,如果沒有註解,也是隻有一個抽象函式,依舊會被認為是函式介面)。多一個或者少一個抽象函式都不能定義為函式介面,如果使用了函式介面註解又不止一個抽象函式,那麼編譯器會拒絕編譯。函式介面在使用時候可以隱式的轉換成 Lambda 表示式。
Java 8
中很多有很多不同功能的函式介面定義,都放在了 Java 8
新增的 java.util.function
包內。下面是一些關於 Java 8
中函式介面功能的描述。
序號 | 介面 & 描述 |
---|---|
BiConsumer | 代表了一個接受兩個輸入引數的操作,並且不返回任何結果 |
BiFunction | 代表了一個接受兩個輸入引數的方法,並且返回一個結果 |
BinaryOperator | 代表了一個作用於於兩個同型別操作符的操作,並且返回了操作符同型別的結果 |
BiPredicate | 代表了一個兩個引數的boolean值方法 |
BooleanSupplier | 代表了boolean值結果的提供方 |
Consumer | 代表了接受一個輸入引數並且無返回的操作 |
DoubleBinaryOperator | 代表了作用於兩個double值操作符的操作,並且返回了一個double值的結果。 |
DoubleConsumer | 代表一個接受double值引數的操作,並且不返回結果。 |
DoubleFunction | 代表接受一個double值引數的方法,並且返回結果 |
DoublePredicate | 代表一個擁有double值引數的boolean值方法 |
DoubleSupplier | 代表一個double值結構的提供方 |
DoubleToIntFunction | 接受一個double型別輸入,返回一個int型別結果。 |
DoubleToLongFunction | 接受一個double型別輸入,返回一個long型別結果 |
DoubleUnaryOperator | 接受一個引數同為型別double,返回值型別也為double 。 |
Function | 接受一個輸入引數,返回一個結果。 |
IntBinaryOperator | 接受兩個引數同為型別int,返回值型別也為int 。 |
IntConsumer | 接受一個int型別的輸入引數,無返回值 。 |
IntFunction | 接受一個int型別輸入引數,返回一個結果 。 |
IntPredicate | 接受一個int輸入引數,返回一個布林值的結果。 |
IntSupplier | 無引數,返回一個int型別結果。 |
IntToDoubleFunction | 接受一個int型別輸入,返回一個double型別結果 。 |
IntToLongFunction | 接受一個int型別輸入,返回一個long型別結果。 |
IntUnaryOperator | 接受一個引數同為型別int,返回值型別也為int 。 |
LongBinaryOperator | 接受兩個引數同為型別long,返回值型別也為long。 |
LongConsumer | 接受一個long型別的輸入引數,無返回值。 |
LongFunction | 接受一個long型別輸入引數,返回一個結果。 |
LongPredicate | 接受一個long輸入引數,返回一個布林值型別結果。 |
LongSupplier | 無引數,返回一個結果long型別的值。 |
LongToDoubleFunction | 接受一個long型別輸入,返回一個double型別結果。 |
LongToIntFunction | 接受一個long型別輸入,返回一個int型別結果。 |
LongUnaryOperator | 接受一個引數同為型別long,返回值型別也為long。 |
ObjDoubleConsumer | 接受一個object型別和一個double型別的輸入引數,無返回值。 |
ObjIntConsumer | 接受一個object型別和一個int型別的輸入引數,無返回值。 |
ObjLongConsumer | 接受一個object型別和一個long型別的輸入引數,無返回值。 |
Predicate | 接受一個輸入引數,返回一個布林值結果。 |
Supplier | 無引數,返回一個結果。 |
ToDoubleBiFunction | 接受兩個輸入引數,返回一個double型別結果 |
ToDoubleFunction | 接受一個輸入引數,返回一個double型別結果 |
ToIntBiFunction | 接受兩個輸入引數,返回一個int型別結果。 |
ToIntFunction | 接受一個輸入引數,返回一個int型別結果。 |
ToLongBiFunction | 接受兩個輸入引數,返回一個long型別結果。 |
ToLongFunction | 接受一個輸入引數,返回一個long型別結果。 |
UnaryOperator | 接受一個引數為型別T,返回值型別也為T。 |
(上面表格來源於菜鳥教程)
3. Lambda 語法
Lambda 的語法主要是下面幾種。
-
(params) -> expression
-
(params) -> {statements;}
Lambda 的語法特性。
- 使用
->
分割 Lambda 引數和處理語句。 - 型別可選,可以不指定引數型別,編譯器可以自動判斷。
- 圓括號可選,如果只有一個引數,可以不需要圓括號,多個引數必須要圓括號。
- 花括號可選,一個語句可以不用花括號,多個引數則花括號必須。
- 返回值可選,如果只有一個表示式,可以自動返回,不需要 return 語句;花括號中需要 return 語法。 6. Lambda 中引用的外部變數必須為 final 型別,內部宣告的變數不可修改,內部宣告的變數名稱不能與外部變數名相同。
舉幾個具體的例子, params 在只有一個引數或者沒有引數的時候,可以直接省略不寫,像這樣。
// 1.不需要引數,沒有返回值,輸出 hello
()->System.out.pritnln("hello");
// 2.不需要引數,返回 hello
()->"hello";
// 3. 接受2個引數(數字),返回兩數之和
(x, y) -> x + y
// 4. 接受2個數字引數,返回兩數之和
(int x, int y) -> x + y
// 5. 兩個數字引數,如果都大於10,返回和,如果都小於10,返回差
(int x,int y) ->{
if( x > 10 && y > 10){
return x + y;
}
if( x < 10 && y < 10){
return Math.abs(x-y);
}
};
複製程式碼
通過上面的幾種情況,已經可以大致瞭解 Lambda 的語法結構了。
4. Lambda 使用
4.1 對於函式介面
從上面的介紹中已經知道了 Runnable 介面已經是函式介面了,它可以隱式的轉換為 Lambda 表示式進行使用,通過下面的建立執行緒並執行的例子看下 Java 8
中 Lambda 表示式的具體使用方式。
/**
* Lambda 的使用,使用 Runnable 例子
* @throws InterruptedException
*/
@Test
public void createLambda() throws InterruptedException {
// 使用 Lambda 之前
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("JDK8 之前的執行緒建立");
}
};
new Thread(runnable).start();
// 使用 Lambda 之後
Runnable runnable1Jdk8 = () -> System.out.println("JDK8 之後的執行緒建立");
new Thread(runnable1Jdk8).start();
// 更加緊湊的方式
new Thread(() -> System.out.println("JDK8 之後的執行緒建立")).start();
}
複製程式碼
可以發現 Java 8
中的 Lambda
碰到了函式介面 Runnable,自動推斷了要執行的 run 方法,不僅省去了 run 方法的編寫,也程式碼變得更加緊湊。
執行得到結果如下。
JDK8 之前的執行緒建立
JDK8 之後的執行緒建立
JDK8 之後的執行緒建立
複製程式碼
上面的 Runnable 函式介面裡的 run 方法是沒有引數的情況,如果是有引數的,那麼怎麼使用呢?我們編寫一個函式介面,寫一個 say
方法接受兩個引數。
/**
* 定義函式介面
*/
@FunctionalInterface
public interface FunctionInterfaceDemo {
void say(String name, int age);
}
複製程式碼
編寫一個測試類。
/**
* 函式介面,Lambda 測試
*/
@Test
public void functionLambdaTest() {
FunctionInterfaceDemo demo = (name, age) -> System.out.println("我叫" + name + ",我今年" + age + "歲了");
demo.say("金庸", 99);
}
複製程式碼
輸出結果。
我叫金庸,我今年99歲了。
複製程式碼
4.2 對於方法引用
方法引用這個概念前面還沒有介紹過,方法引用可以讓我們直接訪問類的例項或者方法,在 Lambda 只是執行一個方法的時候,就可以不用 Lambda
的編寫方式,而用方法引用的方式:例項/類::方法
。這樣不僅程式碼更加的緊湊,而且可以增加程式碼的可讀性。
通過一個例子檢視方法引用。
@Getter
@Setter
@ToString
@AllArgsConstructor
static class User {
private String name;
private Integer age;
}
public static List<User> userList = new ArrayList<User>();
static {
userList.add(new User("A", 26));
userList.add(new User("B", 18));
userList.add(new User("C", 23));
userList.add(new User("D", 19));
}
/**
* 測試方法引用
*/
@Test
public void methodRef() {
User[] userArr = new User[userList.size()];
userList.toArray(userArr);
// User::getAge 呼叫 getAge 方法
Arrays.sort(userArr, Comparator.comparing(User::getAge));
for (User user : userArr) {
System.out.println(user);
}
}
複製程式碼
得到輸出結果。
Jdk8Lambda.User(name=B, age=18) Jdk8Lambda.User(name=D, age=19) Jdk8Lambda.User(name=C, age=23) Jdk8Lambda.User(name=A, age=26)
4.3 對於遍歷方式
Lambda 帶來了新的遍歷方式,Java 8
為集合增加了 foreach
方法,它可以接受函式介面進行操作。下面看一下 Lambda
的集合遍歷方式。
/**
* 新的遍歷方式
*/
@Test
public void foreachTest() {
List<String> skills = Arrays.asList("java", "golang", "c++", "c", "python");
// 使用 Lambda 之前
for (String skill : skills) {
System.out.print(skill+",");
}
System.out.println();
// 使用 Lambda 之後
// 方式1,forEach+lambda
skills.forEach((skill) -> System.out.print(skill+","));
System.out.println();
// 方式2,forEach+方法引用
skills.forEach(System.out::print);
}
複製程式碼
執行得到輸出。
java,golang,c++,c,python,
java,golang,c++,c,python,
javagolangc++cpython
複製程式碼
4.4 對於流式操作
得益於 Lambda
的引入,讓 Java 8
中的流式操作成為可能,Java 8
提供了 stream 類用於獲取資料流,它專注對資料集合進行各種高效便利操作,提高了程式設計效率,且同時支援序列和並行的兩種模式匯聚計算。能充分的利用多核優勢。
流式操作如此強大, Lambda
在流式操作中怎麼使用呢?下面來感受流操作帶來的方便與高效。
流式操作一切從這裡開始。
// 為集合建立序列流
stream()
// 為集合建立並行流
parallelStream()
複製程式碼
流式操作的去重 distinct
和過濾 filter
。
@Test
public void streamTest() {
List<String> skills = Arrays.asList("java", "golang", "c++", "c", "python", "java");
// Jdk8 之前
for (String skill : skills) {
System.out.print(skill + ",");
}
System.out.println();
// Jdk8 之後-去重遍歷
skills.stream().distinct().forEach(skill -> System.out.print(skill + ","));
System.out.println();
// Jdk8 之後-去重遍歷
skills.stream().distinct().forEach(System.out::print);
System.out.println();
// Jdk8 之後-去重,過濾掉 ptyhon 再遍歷
skills.stream().distinct().filter(skill -> skill != "python").forEach(skill -> System.out.print(skill + ","));
System.out.println();
// Jdk8 之後轉字串
String skillString = String.join(",", skills);
System.out.println(skillString);
}
複製程式碼
執行得到結果。
java,golang,c++,c,python,java,
java,golang,c++,c,python,
javagolangc++cpython
java,golang,c++,c,
java,golang,c++,c,python,java
複製程式碼
流式操作的資料轉換(也稱對映)map
。
/**
* 資料轉換
*/
@Test
public void mapTest() {
List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5);
// 資料轉換
numList.stream().map(num -> num * num).forEach(num -> System.out.print(num + ","));
System.out.println();
// 資料收集
Set<Integer> numSet = numList.stream().map(num -> num * num).collect(Collectors.toSet());
numSet.forEach(num -> System.out.print(num + ","));
}
複製程式碼
執行得到結果。
1,4,9,16,25,
16,1,4,9,25,
複製程式碼
流式操作的數學計算。
/**
* 數學計算測試
*/
@Test
public void mapMathTest() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
IntSummaryStatistics stats = list.stream().mapToInt(x -> x).summaryStatistics();
System.out.println("最小值:" + stats.getMin());
System.out.println("最大值:" + stats.getMax());
System.out.println("個數:" + stats.getCount());
System.out.println("和:" + stats.getSum());
System.out.println("平均數:" + stats.getAverage());
// 求和的另一種方式
Integer integer = list.stream().reduce((sum, cost) -> sum + cost).get();
System.out.println(integer);
}
複製程式碼
執行得到結果。
得到輸出
最小值:1
最大值:5
個數:5
和:15
平均數:3.0
15
複製程式碼
5. Lambda 總結
Lamdba
結合函式介面,方法引用,型別推導以及流式操作,可以讓程式碼變得更加簡潔緊湊,也可以藉此開發出更加強大且支援平行計算的程式,函式程式設計也為 Java 帶來了新的程式設計方式。但是缺點也很明顯,在實際的使用過程中可能會發現調式困難,測試表示 Lamdba
的遍歷效能並不如 for 的效能高,同事可能沒有學習導致看不懂 Lamdba
等(可以推薦來看這篇文章)。
文章程式碼已經上傳到 github.com/niumoo/jdk-… 。
<完>
個人網站:www.codingme.net
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回覆資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。