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


先販賣一下焦慮,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-微信公眾號](https://img2018.cnblogs.com/blog/1742867/201910/1742867-20191013111755842-2090947098.png)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM