簡介
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);
更多關於 :: 的詳解,請看你竟然不知道Java中可以用 :: 嗎?
更多關於Optional的詳解,請看使用Java8中的Optional類來消除程式碼中的null檢查
函式式介面
如果你的好奇心使你翻看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中可以用 :: 嗎?
推薦閱讀:使用Java8中的Optional類來消除程式碼中的null檢查
本文參考連結:部落格【一書生VOID】https://lw900925.github.io/java/java8-lambda-expression.html