Java8 Lambda表達式詳解手冊及實例

先販賣一下焦慮,Java8發於2014年3月18日,距離如今已經快6年了,若是你對Java8的新特性尚未應用,甚至還一無所知,那你真得關注公衆號「程序新視界」,好好系列的學習一下Java8的新特性。Lambda表達式已經在新框架中普通使用了,若是你對Lambda還一無所知,真得認真學習一下本篇文章了。java

如今進入正題Java8的Lambda,首先看一下發音 ([ˈlæmdə])表達式。注意該詞的發音,b是不發音的,da發[də]音。web

爲何要引入Lambda表達式

簡單的來講,引入Lambda就是爲了簡化代碼,容許把函數做爲一個方法的參數傳遞進方法中。若是有JavaScript的編程經驗,立刻會想到這不就是閉包嗎。是的,Lambda表達式也能夠稱做Java中的閉包。spring

先回顧一下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表達式的出現解決了這個問題。app

Lambda表達式使得Java擁有了函數式編程的能力,但在Java中Lambda表達式是對象,它必須依附於一類特別的對象類型——函數式接口(functional interface),後面詳細講解。框架

Lambda表達式簡介

Lambda表達式是一種匿名函數(對Java而言這並不徹底準確),通俗的說,它是沒有聲明的方法,即沒有訪問修飾符、返回值聲明和名字的方法。使用Lambda表達式的好處很明顯就是可使代碼變的更加簡潔緊湊。編程語言

Lambda表達式的使用場景與匿名類的使用場景幾乎一致,都是在某個功能(方法)只使用一次的時候。ide

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

<center><b>程序新視界</b>:精彩和成長都不容錯過</center>

csdn-微信公衆號

相關文章
相關標籤/搜索