當前時間: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
等(可以推薦來看這篇文章)。
文章代碼已經上傳到 https://github.com/niumoo/jdk-feature) 。
<完>
個人網站:https://www.codingme.net
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。