Java8 Lambda表達式詳解手冊及實例
- 2019 年 10 月 13 日
- 筆記
先販賣一下焦慮,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表達式詳解手冊及實例》