一篇文章學會 Java 8 新特性 —— Lambda 表達式

Lambda 表達式因其簡潔易讀直觀易理解的特點,顯然已經成爲各大編程語言的開發者最喜愛的語法之一。

首先,我們先來了解一下 Lambda 表達式在幾種主流語言中的寫法,研究一下其共同的特點。

Python

arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
print filter(lambda x: x % 3 == 0, arr)

# 輸出: [3, 6, 9]

C#

// C# 中的 Lambda 表達式常用於 Func 和 Action 委託
Func<int, int, int> add = (c1, c2) => c1 + c2;
int sum = add(2, 5);

// sum: 7

JavaScript

//ES6 爲 JS 也新增了 Lambda 表達式寫法,也稱之爲箭頭函數
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
var result = arr.map(i => i * 10)

// result: [10, 20, 30, 40, 50, 60, 70, 80, 90, 0]

Java8

public interface Add{
    void add(int t1, int t2);
}

public class Test{
	public static  void main(String[] args){
	    Add s = (t1, t2) -> System.out.println(t1 + t2);
	    s.add(1, 2);
	    // 輸出: 3
	}
}

分析,綜合以上四種語言的 Lambda 表達式寫法,可以看出,不同語言的 Lambda 表達式大同小異,主要由三部分構成:參數、特殊代表符號、函數/表達式


我們主要來研究 Java 8 中的 Lambda 表達式

Lamda 表達式的組成

Java 8 中, Lambda 表達式由三部分組成:參數、箭頭、主體

例如:

(int i, int j) -> { return i + j; }

  • 參數
    • 參數含參數類型、參數名稱,由小括號包裹,逗號隔開。
      • (int i, int j) -> { return i + j; }
    • 參數類型可以省略。
      • (i, j) -> { return i + j; }
    • 參數只有一個時,小括號可以省略。
      • i -> { return i * 10; }
    • 無參數時,小括號中爲空。
      • () -> System.out.print("Hello")
  • 特殊符號
    • 箭頭符號,由短橫線和大於號組成.
      • ->
  • 函數語句/表達式
    • 主體爲語句時,需要使用花括號。
      • i -> { return i * 10; }
    • 主體爲表達式時,不能使用花括號。
      • i -> i * 10

函數式接口

剛開始我們舉例了 4 種開發語言的 Lambda 表達式語法,不知有沒有細心的小夥伴發現一個問題:

PythonJavaScript 的例子中,我們調用了原有庫中自帶的 filtermap 方法演示;

C# 和 Java 的例子中,我們是創建了自定義的 add 方法來演示的。

C# 中,我們把幫助定義 add 方法的 Func 稱之爲委託;

那麼,Java 中,名爲 AddInterface 又是什麼呢?它又有什麼作用呢?

這個 Add 接口,在Java 中,稱之爲 函數式接口

Java 中, 函數式接口是使用 Lambda 表達式的必要條件,二者是不可分割的。

簡單來說,函數式接口就是只定義一個抽象方法的接口

Lambda 表達式允許以內聯的形式爲函數式接口的抽象方法直接提供實現,並將整個表達式作爲函數式接口的實例。

舉個例子: 我們查看一下熟悉的 java.lang.Runnable 接口的源碼

public interface Runnable {
    void run();
}

發現,其內部有且僅有一個返回值爲 void 的抽象方法 run(),滿足函數式接口的定義,所以,Runnable 接口就是一個函數式接口。

Java 8 之前,我們使用 Runnable 接口創建多線程:

public class MyRunnable implements Runnable{
	@Override
	public void run(){
		System.out.print("Hello World");
	}
}

public class Test{
	public static void main(String[] args){
		MyRunnable r1 = new MyRunnable();
		new Thread(r1).start();
	}
}

或者,可以使用匿名方法簡化代碼:

public class Test{
	public static void main(String[] args){
		new Thread(new Runnable(){
			@Override
			public void run(){
				System.out.print("Hello World");
			}
		}).start();
	}
}

而在 Java 8 中,我們可以使用 Lambda 表達式:

public class Test{
	public static void main(String[] args){
		/* 使用 Lambda 表達式, 一行代碼創建多線程*/
		new Thread(() -> System.out.print("Hello World")).start();
	}
}

看到這裏,你應該也明白爲什麼 Lambda 表達式一定要與函數式接口相依存了。

因爲,Lambda 表達式是以內聯形式直接爲接口的抽象方法提供實現的。如果接口中有多個抽象方法,Lambda 表達式就無法確定傳遞的是哪個抽象方法的實現了。

那如何來區分某個接口是需要被定義爲函數式接口,還是僅僅臨時只有一個抽象方法,以後還有可能追加其他方法的普通接口呢?

又或者說,如何來保證某個接口是函數式接口,避免後期被誤改呢?

@FunctionalInterface 註解,就是專門用來檢測某個接口是否爲函數式接口的。

當標註該註解時,如果標註的接口不是函數式接口,編譯器將返回一個錯誤提示,如:Multiple non-overriding abstract methods found in interface foo

註解不是必須的,但是必要的。

四大常用函數式接口

上面提到,Lambda 表達式必須有函數式接口的支持,那是否意味着我們每次寫 Lambda 表達式都需要自定義函數式接口呢?

其實大多數時候,我們並不需要去自定義函數式接口,因爲 Java 8API 中,已經爲我們封裝好了一些實用的函數式接口。

這些接口封裝在 java.util.function 包中,主要分爲以下四類:

  • Predicate 類型

    Predicate 類型接口又稱之爲斷言型接口或者謂詞接口

    其抽象方法爲 test(), 接收泛型 T 對象, 返回 boolean 值。

    因其返回 boolean 值,且擁有特有的謂詞方法(點此直達,所以經常被用來做判斷、過濾條件等。

    例如,可以使用它擴展一個 filter 方法來按照指定條件過濾集合:

    // 利用 Predicate 定義 filter 方法按照指定條件過濾集合
    public static <T> List<T> filter(List<T> list, Predicate<T> pre){
        List<T> result = new ArrayList<>();
        for(T t: list){			// 遍歷傳入的泛型集合
            if(pre.test(t)){		// 如果集合中元素滿足 test 方法
                result.add(t);	// 將元素追加到新集合
            }
        }
        return result;
    }
    
    // 使用 filter 方法過濾集合,只返回 能被 3 整除的數字
    List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
    List<Integer> result = filter(nums, i -> i % 3 == 0);
    // result: [3, 6, 9, 0]
    
  • Consumer 類型

    Consumer 類型接口又稱之爲消費型接口

    其抽象方法爲 accept(), 接收泛型 T 對象,無返回值。

    因其無返回值,所以經常被用來執行某個操作。

    例如,可以使用它擴展一個 foreach 方法遍歷集合:

    //利用 Consumer 定義 foreach 方法遍歷集合並執行指定操作
    public static <T> void foreach(List<T> list, Consumer<T> con) {
        for (T t : list) {
            con.accept(t);
        }
    }
    
    // 使用 foreach 方法輸出集合
    List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
    foreach(nums, i -> System.out.println("當前遍歷到的數字: " + i));
    
    // 當前遍歷到的數字: 1
    // 當前遍歷到的數字: 2
    // ...
    
  • Function 類型

    Function 類型接口又稱之爲方法型接口或者函數型接口

    其抽象方法爲 apply(),接收泛型 T 對象,返回 R 泛型對象。

    因其返回 R 泛型對象,且擁有特有的函數方法(點此直達,應用相對比較廣泛。

    例如, 可以使用它擴展一個 map 方法對集合進行指定操作並返回。

    // 利用 Function 定義 map 方法對集合元素進行指定的操作並返回
    public static <T, R> List<R> map(List<T> list, Function<T, R> fun) {
        List<R> result = new ArrayList<>();
        for (T t : list) {
            result.add(fun.apply(t));
        }
        return result;
    }
    
    // 使用 map 方法對集合的每個元素計算平方並返回
    List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
    List<Integer> result = map(nums, i -> i * i);
    
    // result: [1, 4, 9, 16, 25, 36, 49, 64, 81, 0]
    
  • Supplier 類型

    Supplier 類型接口又稱之爲供應型接口

    其抽象方法爲 get(),不接受參數,返回 R 泛型對象。

    因其無參數,所以一般被用在不依賴參數的處理場景中。

    例如, 可以使用它擴展一個 randomList 方法生成一個一定數量的隨機數集合。

    // 利用 Supplier 定義 randomList 方法返回一定數量的隨機數集合
    List<Double> result = randomList(() -> Math.random(), 3);
    
    // result: [0.5441047654548076, 0.055580576644587886, 0.6309881540663]
    
    

其他常用函數式接口

除了以上四類最常用的函數式接口之外,Java 8 中還有一些比較常用的函數式接口,但方法基本都大同小異,便不多做介紹。 如下:

  • UnaryOperator
    • T -> T
  • BinaryOperator
    • (T, T) -> T
  • BiPredicate<L, R>
    • (L, R) -> boolean
  • BiConsumer<T, U>
    • (T, U) -> void
  • BiFunction(T, U, R)
    • (T, U) -> R

除此之外,爲了避免裝箱拆箱操作(在性能方面消耗較大),大多數函數式接口還擁有其對應的原始類型接口。如下:

  • DoublePredicate
  • IntConsumer
  • LongBinaryOperator
  • IntFunction

捕獲 Lambda

一般來說,Lambda表達式中都只會使用其主體中的參數和變量。但是 Lambda表達式是允許使用自由變量(即外層變量)的。

我們把 Lambda表達式使用主體外的變量的這種行爲稱爲捕獲變量,這種Lambda表達式稱之爲捕獲Lambda

Lambda雖然可以無限制的捕獲其他實例變量和靜態變量,但是,局部變量必須只賦值一次,或者顯示聲明爲 final,換句話說,Lambda表達式捕獲的變量只能賦值一次。如下:

正確寫法:

int i = 1;
Runnable r = () -> System.out.println(i);

或者:

final int i = 1;
Runnable r = () -> System.out.println(i);

錯誤寫法:

int i = 1;
Runnable r = () -> System.out.println(i);	//Variable used in lambda expression should be final or effectively final
i = 2;

爲什麼會有這樣的限制呢?

因爲實例變量存儲在中,局部變量存儲在中,如果允許Lambda直接訪問局部變量,並且Lambda在一個線程中使用,,則Lambda線程有可能會在分配該變量的線程已經將該變量回收之後纔去訪問。因此,Java 在訪問自由的局部變量時,實際上是在訪問這個變量的副本,而並非原始變量。只有當該變量只允許賦值一次的情況下,Lambda表達式訪問原始變量還是副本變量纔不會造成影響。

另外,我們並不建議在Lambda表達式中去捕獲局部變量。準確來說,我們不建議使用改變外部變量的這種命令式編程模式。

方法引用

看了這麼多例子,Lambda表達式簡潔、易讀的特點已經可以說是顯而易見了。

那麼,Lambda表達式還可以更簡潔一點嗎?

當然,當Lambda表達式的主體爲一個已有方法時,Lambda表達式還可以進一步簡寫爲方法引用

方法引用主要分三類:

  1. 靜態方法引用

    例如:

    class User{
    	public static String getName(String name){
    		return "My name is " + name;
    	}
    }
    
    public class Test{
    	@Test
    	public void test(){
    		/* 常規Lambda寫法 */
    		Function<String, String> myName1 = (args) -> User.getName(args);
    
    		/* 方法引用寫法 */
    		Function<String, String> myName2 = User::getName;
    	}
    }
    
  2. 其他實例方法引用

    例如:

    class User{
    	public String sayHello(String words){
            return "Hello everyone, Nice to meet you !" + words;
        }
    }
    
    public class Test{
    	@Test
    	public void test(){
    		/* 常規Lambda寫法 */
            BiFunction<User, String, String> sayHello1 = (user, words) -> user.sayHello(words);
    
            /* 方法引用寫法 */
            BiFunction<User, String, String> sayHello12 = User::sayHello;
    	}
    }
    
  3. 當前對象實例方法引用

    例如:

    class User{
    	public String sayHello(String words){
            return "Hello everyone, Nice to meet you !" + words;
        }
    }
    
    public class Test{
    	@Test
    	public void test(){
    		/* 創建實例 */
            User user = new User();
            /* 常規Lambda寫法 */
            Function<String, String> sayH1 = (words) -> user.sayHello(words);
            /* 方法引用寫法 */
            Function<String, String> sayH2 = user::sayHello;
    	}
    }
    

構造函數引用

與方法引用類似,也可以使用 類名+new 關鍵字創建一個對象

例如:

class User{
    public User(){

    }
    public User(String name){

    }
    public User(String name, int age){

    }
}

空參構造:

Supplier<User> user = User::new;

一參構造:

Function<String, User> user2 = User::new;

二參構造:

BiFunction<String, Integer, User> user3 = User::new;

複合使用

實際上,Java 8 所提供的四大函數式接口中,並不是所有的都只包含一個接口的,比如斷言接口(謂詞接口)中,還包含了一些謂詞(如 orand 等)來協助 Lambda 複合實現更加強大的邏輯。

不是說函數式接口只可以包含一個抽象方法嗎?

沒錯。的確只能包含一個抽象方法,查看源碼可以很容易發現,這裏使用了另一種 Java 8 的新特性 —— 默認方法。也就是說,orand 等函數式接口中的方法,其實並不是抽象方法。

謂詞複合

  • and
  • or
  • negate

看一個例子:

class User {
    
    private String firstName;
    private String lastName;
    private String gender;
    private int age;

	/* Constructor */

    /* Getter & Setter */
}

public class Test {

    @Test
    public void test() {
		Predicate<User> men = user -> "male".equals(user.getGender());		// 所有男性
        Predicate<User> women = men.negate();	//所有女性(非男性) ———— 這裏不考慮不男不女的情況 ~
        Predicate<User> womenXu = women.and(user -> "Xu".equals(user.getFirstName()));  // Xu 姓的女性
        Predicate<User> womenXuOrMenWang = women.and(user -> "Xu".equals(user.getFirstName()))
                .or(men.and(user -> "Wang".equals(user.getFirstName())));   // Xu 姓的女性或者 Wang 姓的男性
    }
}

函數複合

  • andThen
  • compose

看一個例子:

//定義兩個函數
Function<Integer, Integer> f = x -> x * 2;		// f(x) = x * 2
Function<Integer, Integer> g = x -> x * x;		// g(x) = x * x

Function<Integer, Integer> t1 = f.andThen(g);	// t1(x) = g(f(x))
Function<Integer, Integer> t2 = f.compose(g);	// t2(x) = f(g(x))

/* 調用查看結果 */
int r1 = t1.apply(10);		//	400		t1(10) = (10 * 2) * (10 * 2)
int r2 = t2.apply(10);		//  200		t2(10) = (10 * 10) * 2

比較器複合

  • comparing
  • thenComparing
  • sort
  • reversed

看一個例子:

List<User> users = new ArrayList<>();
users.add(new User("FirstName", "LastName", "Male", 18));
/* 構造數據 */

Comparator<User> userOrderByAge = Comparator.comparing(User::getAge);   //按年齡排序
Comparator<User> userOrderByAgeReversed = userOrderByAge.reversed();    //按年齡逆序

Comparator<User> userOrderByAgeReversedThenOrderByFirstName =
        Comparator.comparing(User::getAge)
                .reversed()
                .thenComparing(User::getFirstName);                     //按年齡逆序,相同年齡按姓名首字母排序

users.sort(userOrderByAge);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章