本文首發於一書生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
介面,已經被包含在這個包內,他們分別為Predicate
、Consumer
和Function
,由於我們已經在之前的圖書過濾的例子中介紹了Predicate
的用法,所以接下來主要介紹Consumer
和Function
的用法。
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的另一個特性——流式資料處理。