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表達式詳解手冊及實例

程式新視界:精彩和成長都不容錯過

csdn-微信公眾號