Java8 Lambda表示式詳解手冊及例項
Java8 Lambda表示式詳解手冊及例項
版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。
本文連結:https://blog.csdn.net/wo541075754/article/details/102530810
先販賣一下焦慮,Java8發於2014年3月18日,距離現在已經快6年了,如果你對Java8的新特性還沒有應用,甚至還一無所知,那你真得關注公眾號“程式新視界”,好好系列的學習一下Java8的新特性。Lambda表示式已經在新框架中普通使用了,如果你對Lambda還一無所知,真得認真學習一下本篇文章了。
現在進入正題Java8的Lambda,首先看一下發音 ([ˈlæmdə])表示式。注意該詞的發音,b是不發音的,da發[də]音。
為什麼要引入Lambda表示式
簡單的來說,引入Lambda就是為了簡化程式碼,允許把函式作為一個方法的引數傳遞進方法中。如果有JavaScript的程式設計經驗,馬上會想到這不就是閉包嗎。是的,Lambda表示式也可以稱作Java中的閉包。
先回顧一下Java8以前,如果想把某個介面的實現類作為引數傳遞給一個方法會怎麼做?要麼建立一個類實現該介面,然後new出一個物件,在呼叫方法時傳遞進去,要麼使用匿名類,可以精簡一些程式碼。以建立一個執行緒並列印一行日誌為例,使用匿名函式寫法如下:
new Thread(new Runnable() {
@Override
public void run () {
System.out.println("歡迎關注公眾號:程式新視界");
}
}).start();
在java8以前,使用匿名函式已經算是很簡潔的寫法了,再來看看使用Lambda表示式,上面的程式碼會變成什麼樣子。
new Thread(() -> System.out.println("歡迎關注公眾號:程式新視界")).start();
是不是簡潔到爆!
我們都知道java是面向物件的程式語言,除了部分簡單資料型別,萬物皆物件。因此,在Java中定義函式或方法都離不開物件,也就意味著很難直接將方法或函式像引數一樣傳遞,而Java8中的Lambda表示式的出現解決了這個問題。
Lambda表示式使得Java擁有了函數語言程式設計的能力,但在Java中Lambda表示式是物件,它必須依附於一類特別的物件型別——函式式介面(functional interface),後面詳細講解。
Lambda表示式簡介
Lambda表示式是一種匿名函式(對Java而言這並不完全準確),通俗的說,它是沒有宣告的方法,即沒有訪問修飾符、返回值宣告和名字的方法。使用Lambda表示式的好處很明顯就是可以使程式碼變的更加簡潔緊湊。
Lambda表示式的使用場景與匿名類的使用場景幾乎一致,都是在某個功能(方法)只使用一次的時候。
Lambda表示式語法結構
Lambda表示式通常使用(param)->(body)語法書寫,基本格式如下:
//沒有引數
() -> body
// 1個引數
(param) -> body
// 或
(param) ->{ body; }
// 多個引數
(param1, param2...) -> { body }
// 或
(type1 param1, type2 param2...) -> { body }
常見的Lambda表示式如下:
// 無引數,返回值為字串“公眾號:程式新視界”
() -> "公眾號:程式新視界";
// 1個String引數,直接列印結果
(System.out::println);
// 或
(String s) -> System.out.print(s)
// 1個引數(數字),返回2倍值
x -> 2 * x;
// 2個引數(數字),返回差值
(x, y) -> x – y
// 2個int型整數,返回和值
(int x, int y) -> x y
對照上面的示例,我們再總結一下Lambda表示式的結構:
- Lambda表示式可以有0~n個引數。
- 引數型別可以顯式宣告,也可以讓編譯器從上下文自動推斷型別。如(int x)和(x)是等價的。
- 多個引數用小括號括起來,逗號分隔。一個引數可以不用括號。
- 沒有引數用空括號表示。
- Lambda表示式的正文可以包含零條,一條或多條語句,如果有返回值則必須包含返回值語句。如果只有一條可省略大括號。如果有一條以上則必須包含在大括號(程式碼塊)中。
函式式介面
函式式介面(Functional Interface)是Java8對一類特殊型別的介面的稱呼。這類介面只定義了唯一的抽象方法的介面(除了隱含的Object物件的公共方法),因此最開始也就做SAM型別的介面(Single Abstract Method)。
比如上面示例中的java.lang.Runnable就是一種函式式介面,在其內部只定義了一個void run()的抽象方法,同時在該介面上註解了@FunctionalInterface。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface註解是用來表示該介面要符合函式式介面的規範,除了隱含的Object物件的公共方法以外只可有一個抽象方法。當然,如果某個介面只定義一個抽象方法,不使用該註解也是可以使用Lambda表示式的,但是沒有該註解的約束,後期可能會新增其他的抽象方法,導致已經使用Lambda表示式的地方出錯。使用@FunctionalInterface從編譯層面解決了可能的錯誤。
比如當註解@FunctionalInterface之後,寫兩個抽象方法在介面內,會出現以下提示:
Multiple non-overriding abstract methods found in interface com.secbro2.lambda.NoParamInterface
通過函式式介面我們也可以得出一個簡單的結論:可使用Lambda表示式的介面,只能有一個抽象方法(除了隱含的Object物件的公共方法)。
注意此處的方法限制為抽象方法,如果介面內有其他靜態方法則不會受限制。
方法引用,雙冒號操作
[方法引用]的格式是,類名::方法名。
像如ClassName::methodName或者objectName::methodName的表示式,我們把它叫做方法引用(Method Reference),通常用在Lambda表達中。
看一下示例:
// 無引數情況
NoParamInterface paramInterface2 = ()-> new HashMap<>();
// 可替換為
NoParamInterface paramInterface1 = HashMap::new;
// 一個引數情況
OneParamInterface oneParamInterface1 = (String string) -> System.out.print(string);
// 可替換為
OneParamInterface oneParamInterface2 = (System.out::println);
// 兩個引數情況
Comparator c = (Computer c1, Computer c2) -> c1.getAge().compareTo(c2.getAge());
// 可替換為
Comparator c = (c1, c2) -> c1.getAge().compareTo(c2.getAge());
// 進一步可替換為
Comparator c = Comparator.comparing(Computer::getAge);
再比如我們用函式式介面java.util.function.Function來實現一個String轉Integer的功能,可以如下寫法:
Function<String, Integer> function = Integer::parseInt;
Integer num = function.apply("1");
根據Function介面的定義Function<T,R>,其中T表示傳入型別,R表示返回型別。具體就是實現了Function的apply方法,在其方法內呼叫了Integer.parseInt方法。
通過上面的講解,基本的語法已經完成,以下內容通過例項來逐一演示在不同的場景下如何使用。
Runnable執行緒初始化示例
Runnable執行緒初始化是比較典型的應用場景。
// 匿名函類寫法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("歡迎關注公眾號:程式新視界");
}
}).start();
// lambda表示式寫法
new Thread(() -> System.out.println("歡迎關注公眾號:程式新視界")).start();
// lambda表示式 如果方法體內有多行程式碼需要帶大括號
new Thread(() -> {
System.out.println("歡迎關注公眾號");
System.out.println("程式新視界");
}).start();
通常都會把lambda表示式內部變數的名字起得短一些,這樣能使程式碼更簡短。
事件處理示例
Swing API程式設計中經常會用到的事件監聽。
// 匿名函類寫法
JButton follow = new JButton("關注");
follow.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("已關注公眾號:程式新視界");
}
});
// lambda表示式寫法
follow.addActionListener((e) -> System.out.println("已關注公眾號:程式新視界"));
// lambda表示式寫法
follow.addActionListener((e) -> {
System.out.println("已關注公眾號");
System.out.println("程式新視界");
});
列表遍歷輸出示例
傳統遍歷一個List,基本上都使用for迴圈來遍歷,Java8之後List擁有了forEach方法,可配合lambda表示式寫出更加簡潔的方法。
List<String> list = Arrays.asList("歡迎","關注","程式新視界");
// 傳統遍歷
for(String str : list){
System.out.println(str);
}
// lambda表示式寫法
list.forEach(str -> System.out.println(str));
// lambda表示式寫法
list.forEach(System.out::println);
函式式介面示例
在上面的例子中已經看到函式式介面java.util.function.Function的使用,在java.util.function包下中還有其他的類,用來支援Java的函數語言程式設計。比如通過Predicate函式式介面以及lambda表示式,可以向API方法新增邏輯,用更少的程式碼支援更多的動態行為。
@Test
public void testPredicate() {
List<String> list = Arrays.asList("歡迎", "關注", "程式新視界");
filter(list, (str) -> ("程式新視界".equals(str)));
filter(list, (str) -> (((String) str).length() == 5));
}
public static void filter(List<String> list, Predicate condition) {
for (String content : list) {
if (condition.test(content)) {
System.out.println("符合條件的內容:" content);
}
}
}
其中filter方法中的寫法還可以進一步簡化:
list.stream().filter((content) -> condition.test(content)).forEach((content) ->System.out.println("符合條件的內容:" content));
list.stream().filter(condition::test).forEach((content) ->System.out.println("符合條件的內容:" content));
list.stream().filter(condition).forEach((content) ->System.out.println("符合條件的內容:" content));
如果不需要“符合條件的內容:”字串的拼接,還能夠進一步簡化:
list.stream().filter(condition).forEach(System.out::println);
如果將呼叫filter方法的判斷條件也寫在一起,test方法中的內容可以通過一行程式碼來實現:
list.stream().filter((str) -> ("程式新視界".equals(str))).forEach(System.out::println);
如果需要同時滿足兩個條件或滿足其中一個即可,Predicate可以將這樣的多個條件合併成一個。
Predicate start = (str) -> (((String) str).startsWith("程式"));
Predicate len = (str) -> (((String) str).length() == 5);
list.stream().filter(start.and(len)).forEach(System.out::println);
Stream相關示例
在《JAVA8 STREAM新特性詳解及實戰》一文中已經講解了Stream的使用。你是否發現Stream的使用都離不開Lambda表示式。是的,所有Stream的操作必須以Lambda表示式為引數。
以Stream的map方法為例:
Stream.of("a","b","c").map(item -> item.toUpperCase()).forEach(System.out::println);
Stream.of("a","b","c").map(String::toUpperCase).forEach(System.out::println);
更多的使用例項可參看Stream的《JAVA8 STREAM新特性詳解及實戰》一文。
Lambda表示式與匿名類的區別
- 關鍵詞的區別:對於匿名類,關鍵詞this指向匿名類,而對於Lambda表示式,關鍵詞this指向包圍Lambda表示式的類的外部類,也就是說跟表示式外面使用this表達的意思是一樣。
- 編譯方式:Java編譯器編譯Lambda表示式時,會將其轉換為類的私有方法,再進行動態繫結,通過invokedynamic指令進行呼叫。而匿名內部類仍然是一個類,編譯時編譯器會自動為該類取名並生成class檔案。
其中第一條,以Spring Boot中ServletWebServerApplicationContext類的一段原始碼作為示例:
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(),servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
其中,這裡的this指向的就是getSelfInitializer方法所在的類。
小結
至此,Java8 Lambda表示式的基本使用已經講解完畢,最關鍵的還是要勤加練習,達到熟能生巧的使用。當然,剛開始可能需要一個適應期,在此期間可以把本篇文章收藏當做一個手冊拿來參考。
原文連結:《Java8 Lambda表示式詳解手冊及例項》
本文參與騰訊雲自媒體分享計劃,歡迎正在閱讀的你也加入,一起分享。
java中,某方法中含有介面做引數,為什麼建立一個介面的實現類可以代替該介面作為該方法的引數
將類當成引數傳入方法,其實就是將類的物件傳入方法,如果是抽象類,其實就是將抽象類的子類的物件傳入方法,如果是介面,其實就是將介面實現類的物件傳入方法。
因為抽象類和介面是不能例項化成物件的,所以必須找它們的子類或實現類
JAVA:將類、抽象類、介面當成方法的引數傳入
將類當成引數傳入方法,其實就是將類的物件傳入方法,如果是抽象類,其實就是將抽象類的子類的物件傳入方法,如果是介面,其實就是將介面實現類的物件傳入方法。
因為抽象類和介面是不能例項化成物件的,所以必須找它們的子類或實現類
1. 普通類物件當成方法引數傳入
public class Person{ public void eat(){ System.out.println("吃飯"); } } //方法 public static void operatePerson(Person p){ p.eat(); } //main呼叫方法,將person物件傳入方法中 operatePerson(new person());
2. 抽象類作為方法引數傳入
//抽象類 public abstract class Animal{ public abstratc void eat(); } //Animal的子類 public class Cat extends Animal{ //重寫方法 public void eat(){ System.out.println("貓吃魚"); } } //main中的方法,這裡要求傳入Animal類,但Animal的類是一個抽象類,不能例項物件,所以只能傳其子類。 function static void operateAnimal(Animal a){ a.eat(); } //方法的呼叫 Animal a = new Cat(); operateAnimal(new cat());或operateAnimal(a); 當然,還要以通過內部類一次性傳參 operateAnimal( new Animal(){ //重寫animal抽象方法 public void eat(){System.out.println("貓還吃飯");} } ); 要求傳入父類物件,但可以傳入任意子類物件,這樣就使得擴充套件性得到提高 operateAnimal(new Cat()); operateAnimal(new Dog()); operateAnimal(new Bird()); ...... 傳入什麼類,就呼叫什麼類的功能,這就是多型的實現。
3. 介面實現類的物件當成方法引數傳入
public interface Smoking{ public abstract void smoking(); } //實現類 public class Student implements Smoking{ //實現介面方法 public void smoking(){ System.out.println("抽菸中...."); } } //main中定義方法,將介面實現類當方法引數傳入 public static void operateSmoking(Smoking s){ s.smoking(); } //方法的呼叫 Smoking s = new Student(); operateSmoking(s);或operateSmoking(new Student()); 或用內部類傳入 operateSmoking( new Smoking(){ //重寫介面方法 public void smoking(){ System.out.println("不準吸菸"); } } );
總結:類當成方法引數傳入,其實就是傳物件。抽象類和介面其實就是傳其子類或實現類的物件。
來源 Java8 Lambda表示式詳解手冊及例項 - 雲+社群 - 騰訊雲 (tencent.com) JAVA:將類、抽象類、介面當成方法的引數傳入 - adamal - 部落格園 (cnblogs.com)