Java 8 習慣用語(7):函數接口

Java 8 習慣用語(7):函數接口

lambda 表達式的類型是什麼?一些語言使用函數值函數對象來表示 lambda 表達式,但 Java™ 語言沒有這麼作。Java 使用函數接口來表示 lambda 表達式類型。乍一看彷佛有點奇怪,但事實上這是一種確保對 Java 語言舊版本的向後兼容性的有效途徑。html

您應該很是熟悉下面這段代碼:java

Thread thread = new Thread(new Runnable() {
 public void run() {
 System.out.println("In another thread");
 }});
 thread.start();
 System.out.println("In main");

Thread 類和它的構造函數是在 Java 1.0 中引入的,距今已有超過 20 年的時間。從那時起,構造函數從未改變過。將 Runnable的匿名實例傳遞給構造函數已成爲一種傳統。可是從 Java 8 開始,能夠選擇傳遞 lambda 表達式:程序員

Thread thread = new Thread(() -> System.out.println("In another thread"));
關於本系列Java 8 是自 Java 語言誕生以來進行的一次最重大更新—包含了很是豐富的新功能,您可能想知道從何處開始着手瞭解它。在本系列中,做家兼教師 Venkat Subramaniam 提供了一種慣用的 Java 8 編程方法:這些簡短的探索會激發您反思您認爲理所固然的 Java 約定,同時逐步將新技術和語法集成到您的程序中。

Thread 類的構造函數想要一個實現 Runnable 的實例。在本例中,咱們傳遞了一個 lambda 表達式,而不是傳遞一個對象。咱們能夠選擇向各類各樣的方法和構造函數傳遞 lambda 表達式,包括在 Java 8 以前建立的一些方法和構造函數。這頗有效,由於 lambda 表達式在 Java 中表示爲函數接口。編程

函數接口有 3 條重要法則:安全

  1. 一個函數接口只有一個抽象方法。
  2. Object 類中屬於公共方法的抽象方法不會被視爲單一抽象方法。
  3. 函數接口能夠有默認方法和靜態方法。

任何知足單一抽象方法法則的接口,都會被自動視爲函數接口。這包括 RunnableCallable 等傳統接口,以及您本身構建的自定義接口。app

內置函數接口

除了已經提到的單一抽象方法以外,JDK 8 還包含多個新函數接口。最經常使用的接口包括 Function<T, R>Predicate<T>Consumer<T>,它們是在 java.util.function 包中定義的。Streammap 方法接受 Function<T, R> 做爲參數。相似地,filter 使用 Predicate<T>forEach 使用 Consumer<T>。該包還有其餘函數接口,好比 Supplier<T>BiConsumer<T, U>BiFunction<T, U, R>ide

能夠將內置函數接口用做咱們本身的方法的參數。例如,假設咱們有一個 Device 類,它包含方法 checkoutcheckin 來指示是否正在使用某個設備。當用戶請求一個新設備時,方法 getFromAvailable 從可用設備池中返回一個設備,或在必要時建立一個新設備。函數

咱們能夠實現一個函數來借用設備,就象這樣:學習

public void borrowDevice(Consumer<Device> use) {
 Device device = getFromAvailable();
  
 device.checkout();
  
 try {
 use.accept(device);      
 } finally {
 device.checkin();
 }}

borrowDevice 方法:測試

  • 接受 Consumer<Device> 做爲參數。
  • 從池中獲取一個設備(咱們在這個示例中不關心線程安全問題)。
  • 調用 checkout 方法將設備狀態設置爲 checked out
  • 將設備交付給用戶。

在完成設備調用後返回到 Consumeraccept 方法時,經過調用 checkin 方法將設備狀態更改成 checked in

下面給出了一種使用 borrowDevice 方法的方式:

new Sample().borrowDevice(device -> System.out.println("using " + device));

由於該方法接收一個函數接口做爲參數,因此傳入一個 lambda 表達式做爲參數是能夠接受的。

自定義函數接口

儘管最好儘可能使用內置函數接口,但有時須要自定義函數接口。

要建立本身的函數接口,須要作兩件事:

  1. 使用 @FunctionalInterface 註釋該接口,這是 Java 8 對自定義函數接口的約定。
  2. 確保該接口只有一個抽象方法。

該約定清楚地代表該接口應接收 lambda 表達式。當編譯器看到該註釋時,它會驗證該接口是否只有一個抽象方法。

使用 @FunctionalInterface 註釋能夠確保,若是在將來更改該接口時意外違反抽象方法數量規則,您會得到錯誤消息。這頗有用,由於您會當即發現問題,而不是留給另外一位開發人員在之後處理它。沒有人但願在將 lambda 表達式傳遞給其餘人的自定義接口時得到錯誤消息。

建立自定義函數接口

做爲一個示例,咱們將建立一個 Order 類,它有一系列 OrderItem 以及一個轉換並輸出它們的方法。咱們首先建立一個接口。

下面的代碼將建立一個 Transformer 函數接口。

@FunctionalInterfacepublic interface Transformer<T> {
 T transform(T input);}

該接口用 @FunctionalInterface 註釋作了標記,代表它是一個函數接口。由於該註釋包含在 java.lang 包中,因此沒有必要導入。該接口有一個名爲 transform 的方法,後者接受一個參數化爲 T 類型的對象,並返回一個相同類型的轉換後對象。轉換的語義將由該接口的實現來決定。

這是 OrderItem 類:

public class OrderItem {
 private final int id;
 private final int price;
  
 public OrderItem(int theId, int thePrice) {
 id = theId;
 price = thePrice;
 }
  
 public int getId() { return id; }
 public int getPrice() { return price; }
  
 public String toString() { return String.format("id: %d price: %d", id, price); }}

OrderItem 是一個簡單的類,它有兩個屬性:idprice,以及一個 toString 方法。

如今來看看 Order 類。

import java.util.*;import java.util.stream.Stream;
 public class Order {
 List<OrderItem> items;
  
 public Order(List<OrderItem> orderItems) {
 items = orderItems;
 }
  
 public void transformAndPrint(
 Transformer<Stream<OrderItem>> transformOrderItems) {
  
 transformOrderItems.transform(items.stream())
 .forEach(System.out::println);
 }}

transformAndPrint 方法接受 Transform<Stream<OrderItem> 做爲參數,調用 transform 方法來轉換屬於 Order 實例的訂單項,而後按轉換後的順序輸出這些訂單項。

這是一個使用該方法的樣本:

import java.util.*;import static java.util.Comparator.comparing;import java.util.stream.Stream;import java.util.function.*;
 class Sample {     
 public static void main(String[] args) {
 Order order = new Order(Arrays.asList(
 new OrderItem(1, 1225),
 new OrderItem(2, 983),
 new OrderItem(3, 1554)
 ));
  
  
 order.transformAndPrint(new Transformer<Stream<OrderItem>>() {
 public Stream<OrderItem> transform(Stream<OrderItem> orderItems) {
 return orderItems.sorted(comparing(OrderItem::getPrice));
 }
 });
 }}

咱們傳遞一個匿名內部類做爲 transformAndPrint 方法的參數。在 transform 方法內,調用給定流的 sorted 方法,這會對訂單項進行排序。這是咱們的代碼的輸出,其中顯示了按價格升序排列的訂單項:

id: 2 price: 983id: 1 price: 1225id: 3 price: 1554

lambda 表達式的強大功能

在任何須要函數接口的地方,咱們都有 3 種選擇:

  1. 傳遞一個匿名內部類。
  2. 傳遞一個 lambda 表達式。
  3. 在某些狀況下傳遞一個方法引用而不是 lambda 表達式。

傳遞匿名內部類的過程很複雜,咱們只能傳遞方法引用來替代直通 lambda 表達式。考慮若是咱們重寫對 transformAndPrint函數的調用,以使用 lambda 表達式來代替匿名內部類,將會發生什麼:

order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));

與咱們最初提供的匿名內部類相比,這簡潔得多且更容易閱讀。

自定義函數接口與內置函數接口

咱們的自定義函數接口演示了建立自定義接口的優點和不足。首先考慮優點:

  • 您能夠爲自定義接口提供一個描述性名稱,幫助其餘開發人員修改或重用它。像 TransformerValidatorApplicationEvaluator 這樣的名稱是特定於領域的,能夠幫助讀取接口方法的人推斷對參數的預期是什麼。
  • 只要您高興,能夠爲抽象方法提供任何具備有效語法的名稱。只有接口的接收者會得到此優點,並且僅在傳遞抽象方法時纔會體現出來。傳遞 lambda 表達式或方法引用的調用方不會得到此優點。
  • 您能夠在本身的接口中使用參數化的類型,或者讓它保持簡單並特定於某些類型。在本例中,能夠編寫 Transformer 接口來使用 OrderItems 而不是參數化類型 T
  • 您能夠編寫自定義的默認方法和靜態方法,它們可供該接口的其餘實現使用。

固然,使用自定義函數接口也存在不足之處:

  • 想象建立多個接口,全部接口都有具備相同簽名的抽象方法,好比接受 String 做爲參數並返回 Integer。儘管方法的名稱可能有所不一樣,但它們大部分都是多餘的,可替換爲一個具備通用名稱的接口。
  • 任何想要使用自定義接口的人,都必須投入額外的精力來學習、理解和記住它們。全部 Java 程序員都熟悉 java.lang 包中的 Runnable。咱們一次又一次地看到它,因此能夠輕鬆地記住它的用途。可是,若是我使用了一個自定義 Executor,您在使用該接口以前必須仔細瞭解它。在某些狀況下,投入一些精力是值得的,可是若是 ExecutorRunnable 很是類似,就會浪費精力。

哪一種接口最好?

瞭解自定義函數接口與內置函數接口的優缺點後,如何肯定採用哪一種接口?咱們回顧一下 Transformer 接口來尋找答案。

回想一下,Transformer 的存在是爲了傳達將一個對象轉換爲另外一個對象的語義。這裏,咱們按名稱來引用它:

public void transformAndPrint(Transformer<Stream<OrderItem>> transformOrderItems) {

方法 transformAndPrint 接收一個負責執行轉換的參數。該轉換可能對 OrderItems 集合中的元素進行從新排序。或者,它可能屏蔽每一個訂單項的部分細節。或者該轉換能夠決定什麼都不作,僅返回原始集合。將實現工做留給調用方。

重要的是,調用方知道它們能夠將轉換實現做爲參數提供給 transformAndPrint 方法。函數接口的名稱和它的文檔應該提供這些細節。在本例中,從參數名稱 (transformOrderItems) 也能夠清楚瞭解這些細節,並且它們應包含在 transformAndPrint 函數的文檔中。儘管函數接口的名稱頗有用,但它不是瞭解函數接口用途和用法的惟一途徑。

仔細查看 Transformer 接口,並將它的用途與 JDK 的內置函數接口進行比較,咱們看到 Function<T, R> 能夠取代 Transformer。要測試 Transformer 函數接口,能夠從代碼中刪除它並更改 transformAndPrint 函數,就像這樣:

public void transformAndPrint(Function<Stream<OrderItem>, Stream<OrderItem>> transformOrderItems) {
 transformOrderItems.apply(items.stream())
 .forEach(System.out::println);}

改動很小 —除了將 Transformer<Stream<OrderItem>> 更改成 Function<Stream<OrderItem>>Stream<OrderItem>>,咱們還將方法調用從 transform() 更改成 apply()

transformAndPrint 的調用使用了一個匿名內部類,咱們還須要更改這一點。可是,咱們已更改該調用來使用 lambda 表達式:

order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));

函數接口的名稱與 lambda 表達式無關—它僅與編譯器相關,編譯器將 lambda 表達式參數與方法參數聯繫起來。方法的名稱是transform 仍是 apply,一樣與調用方無關。

使用內置的函數接口讓咱們的接口減小了一個,調用該方法也具備一樣功效。咱們也沒有損害代碼的可讀性。這個練習告訴咱們,咱們能夠輕鬆地將自定義函數接口替換爲內置接口。咱們只需提供 transformAndPrint 的文檔(未顯示)並採用含義更明確的方式命名該參數。

結束語

將 lambda 表達式設置爲函數接口類型的設計決策,有助於在 Java 8 與早期 Java 版本之間實現向後兼容性。能夠將 lambda 表達式傳遞給任何一般接收單一抽象方法接口的舊函數。要接收 lambda 表達式,方法的參數類型應爲函數接口。

在某些狀況下,建立本身的函數接口是合情合理的,但在這麼作時應該當心謹慎。僅在應用程序須要高度專業化的方法時,或者現有接口沒法知足您的需求時,才考慮自定義函數接口。請始終檢查一個 JDK 的內置函數接口中是否存在該功能。儘可能使用內置函數接口。

原做者:Venkat Subramaniam  
原文連接: Java 8 習慣用語
原出處: IBM Developer

e3f6982e97de58e289858a0ec142affe.jpeg

相關文章
相關標籤/搜索