Java 8新特性(一):Lambda表示式

一書生VOID發表於2019-03-04
Java 8新特性(一):Lambda表示式

本文首發於一書生VOID的部落格
原文連結:Java 8新特性(一):Lambda表示式


2014年3月釋出的Java 8,有可能是Java版本更新中變化最大的一次。新的Java 8為開發者帶來了許多重量級的新特性,包括Lambda表示式,流式資料處理,新的Optional類,新的日期和時間API等。這些新特性給Java開發者帶來了福音,特別是Lambda表示式的支援,使程式設計更加簡化。本篇文章將討論行為引數化,Lambda表示式,函式式介面等特性。

行為引數化

在軟體開發的過程中,開發人員可能會遇到頻繁的需求變更,使他們不斷地修改程式以應對這些變化的需求,導致專案進度緩慢甚至專案延期。行為引數化就是一種可以幫助你應對頻繁需求變更的開發模式,簡單的說,就是預先定義一個程式碼塊而不去執行它,把它當做引數傳遞給另一個方法,這樣,這個方法的行為就被這段程式碼塊引數化了。

為了方便理解,我們通過一個例子來講解行為引數化的使用。假設我們正在開發一個圖書管理系統,需求是要對圖書的作者進行過濾,篩選出指定作者的書籍。比較常見的做法就是編寫一個方法,把作者當成方法的引數:

public List<Book> filterByAuthor(List<Book> books, String author) {
	List<Book> result = new ArrayList<>();
	for (Book book : books) {
		if (author.equals(book.getAuthor())) {
			result.add(book);
		}
	}
	return result;
}
複製程式碼

現在客戶需要變更需求,新增過濾條件,按照出版社過濾,於是我們不得不再次編寫一個方法:

public List<Book> filterByPublisher(List<Book> books, String publisher) {
	List<Book> result = new ArrayList<>();
	for (Book book : books) {
		if (publisher.equals(book.getPublisher())) {
			result.add(book);
		}
	}
	return result;
}
複製程式碼

兩個方法除了名稱之外,內部的實現邏輯幾乎一模一樣,唯一的區別就是if判斷條件,前者判斷的是作者,後者判斷的是出版社。如果現在客戶又要增加需求,需要按照圖書的售價過濾,是不是需要再次將上面的方法複製一遍,將if判斷條件改為售價? No! 這種做法違背了DRY(Don’t Repeat Yourself,不要重複自己)原則,而且不利於後期維護,如果需要改變方法內部遍歷方式來提高效能,意味著每個filterByXxx()方法都需要修改,工作量太大。

一種可行的辦法是對過濾的條件做更高層的抽象,過濾的條件無非就是圖書的某些屬性(比如價格、出版社、出版日期、作者等),可以宣告一個介面用於對過濾條件建模:

public interface BookPredicate {
    public boolean test(Book book);
}
複製程式碼

BookPredicate介面只有一個抽象方法test(),該方法接受一個Book型別引數,返回一個boolean值,可以用它來表示圖書的不同過濾條件。

接下來我們對之前的過濾方法進行重構,將filterByXxx()方法的第二個引數換成上面定義的介面:

public List<Book> filter(List<Book> books, BookPredicate bookPredicate) {
    List<Book> result = new ArrayList<>();
	for (Book book : books) {
		if (bookPredicate.test(book)) {
			result.add(book);
		}
	}
	return result;
}
複製程式碼

將過濾的條件換成BookPredicate的實現類,這裡採用了內部類:

// 根據作者過濾
final String author = "張三";
List<Book> result = filter(books, new BookPredicate() {
    @Override
    public boolean test(Book book) {
        return author.equals(book.getAuthor());
    }
});

// 根據圖書價格過濾
final double price = 100.00D;
List<Book> result = filter(books, new BookPredicate() {
    @Override
    public boolean test(Book book) {
        return price > book.getPrice();
    }
});
複製程式碼

重構前後有什麼區別?我們將方法中的if判斷條件換成了BookPredicate介面定義的test()方法,用於判斷是否滿足過濾條件,將圖書過濾的邏輯交給了BookPredicate介面的實現類,而不是在filter()方法內部實現過濾,而BookPredicate介面又是filter()方法的引數。以上的步驟,就是將行為引數化,也就是將圖書過濾的行為(BookPredicate介面的實現類)當做filter()方法的引數。現在,可以刪掉所有filterByXxx()的方法,只保留filter()方法,就算後期資料規模很龐大,需要改變集合的遍歷方式來提高效能,只需要在filter()方法內部做出相應的修改,而不用去修改其他業務程式碼。

不過,BookPredicate介面只是針對圖書的過濾,如果需要對其他物件集合排序(如:使用者),又得重新申明一個介面。有一個辦法就是可以用Java的泛型對它做進一步的抽象:

public interface Predicate<T> {
    public boolean test(T t);
}
複製程式碼

現在你可以把filter()方法用在任何物件的過濾中。

Lambda表示式

雖然我們對filter()方法進行重構,並抽象了Predicate介面作為過濾的條件,但實際上還需要編寫很多內部類來實現Predicate介面。使用內部類的方式實現Predicate介面有很多缺點:首先是程式碼顯得臃腫不堪,可讀性差;其次,如果某個區域性變數被內部類使用,這個變數必須使用final關鍵字修飾。在Java 8中,使用Lambda表示式可以對內部類進一步簡化:

// 根據作者過濾
List<Book> result = filter(books, book -> "張三".equals(book.getAuthor()));

// 根據圖書價格過濾
List<Book> result = filter(books, book -> 100 > book.getPrice());
複製程式碼

使用Lambda僅僅用一行程式碼就對內部類進行了轉化,而且程式碼變得更加清晰可讀。其中book -> "張三".equals(book.getAuthor())book -> 100 > book.getPrice()就是我們接下來要研究的Lambda表示式。

Lambda表示式是什麼

Lambda表示式(lambda expression)是一個匿名函式,由數學中的λ演算而得名。在Java 8中可以把Lambda表示式理解為匿名函式,它沒有名稱,但是有引數列表、函式主體、返回型別等。

Lambda表示式的語法如下:

(parameters) -> { statements; }
複製程式碼

為什麼要使用Lambda表示式?前面你也看到了,在Java中使用內部類顯得十分冗長,要編寫很多樣板程式碼,Lambda表示式正是為了簡化這些步驟出現的,它使程式碼變得清晰易懂。

如何使用Lambda表示式

Lambda表示式是為了簡化內部類的,你可以把它當成是內部類的一種簡寫方式,只要是有內部類的程式碼塊,都可以轉化成Lambda表示式:

// Comparator排序
List<Integer> list = Arrays.asList(3, 1, 4, 5, 2);
list.sort(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});

// 使用Lambda表示式簡化
list.sort((o1, o2) -> o1.compareTo(o2));
複製程式碼
// Runnable程式碼塊
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello Man!");
    }
});

// 使用Lambda表示式簡化
Thread thread = new Thread(() -> System.out.println("Hello Man!"));
複製程式碼

可以看出,只要是內部類的程式碼塊,就可以使用Lambda表示式簡化,並且簡化後的程式碼清晰易懂。甚至,Comparator排序的Lambda表示式還可以進一步簡化:

list.sort(Integer::compareTo);
複製程式碼

這種寫法被稱為 方法引用,方法引用是Lambda表示式的簡便寫法。如果你的Lambda表示式只是呼叫這個方法,最好使用名稱呼叫,而不是描述如何呼叫,這樣可以提高程式碼的可讀性。

方法引用使用::分隔符,分隔符的前半部分表示引用型別,後面半部分表示引用的方法名稱。例如:Integer::compareTo表示引用型別為Integer,引用名稱為compareTo的方法。

類似使用方法引用的例子還有列印集合中的元素到控制檯中:

list.forEach(System.out::println);
複製程式碼

函式式介面

如果你的好奇心使你翻看Runnable介面原始碼,你會發現該介面被一個@FunctionalInterface的註解修飾,這是Java 8中新增的新註解,用於表示 函式式介面

函式式介面又是什麼鬼?在Java 8中,把那些僅有一個抽象方法的介面稱為函式式介面。如果一個介面被@FunctionalInterface註解標註,表示這個介面被設計成函式式介面,只能有一個抽象方法,如果你新增多個抽象方法,編譯時會提示“Multiple non-overriding abstract methods found in interface XXX”之類的錯誤。

函式式方法又能做什麼?Java8允許你以Lambda表示式的方式為函式式介面提供實現,通俗的說,你可以將整個Lambda表示式作為介面的實現類。

除了Runnable之外,Java 8中內建了許多函式式介面供開發者使用,這些介面位於java.util.function包中,我們之前使用的Predicate介面,已經被包含在這個包內,他們分別為PredicateConsumerFunction,由於我們已經在之前的圖書過濾的例子中介紹了Predicate的用法,所以接下來主要介紹ConsumerFunction的用法。

Consumer

java.util.function.Consumer<T>定義了一個名叫accept()的抽象方法,它接受泛型T的物件,沒有返回(void)。如果你需要訪問型別T的物件,並對其執行某些操作,就可以使用這個介面。比如,你可以用它來建立一個forEach()方法,接受一個集合,並對集合中每個元素執行操作:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

public static <T> void forEach(List<T> list, Consumer<T> consumer) {
    for(T t: list){
        consumer.accept(t);
    }
}

public static void main(String[] args) {
    List<String> list = Arrays.asList("A", "B", "C", "D");
    forEach(list, str -> System.out.println(str));
    // 也可以寫成
    forEach(list, System.out::println);
}
複製程式碼

Function

java.util.function.Function<T, R>介面定義了一個叫作apply()的方法,它接受一個泛型T的物件,並返回一個泛型R的物件。如果你需要定義一個Lambda,將輸入物件的資訊對映到輸出,就可以使用這個介面。比如,我們需要計算一個圖書集合中每本書的作者名稱有幾個漢字(假設這些書的作者都是中國人):

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for(T s: list){
        result.add(f.apply(s));
    }
    return result;
}

public static void main(String[] args) {
    List<Book> books = Arrays.asList(
        new Book("張三", 99.00D),
        new Book("李四", 59.00D),
        new Book("王老五", 59.00D)
    );
    List<Integer> results = map(books, book -> book.getAuthor().length());
}
複製程式碼

現在,你應該對Lambda表示式有一個初步的瞭解了,並且,你可以使用Lambda表示式來重構你的程式碼,提高程式碼可讀性;使用行為引數化來設計你的程式,讓程式更靈活。在下一篇文章將會介紹Java 8的另一個特性——流式資料處理。

相關文章