1. 程式人生 > 其它 >Java8 Lambda表示式詳解手冊及例項

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)