在 Eclipse IDE 中試用 Lambda 表達式

在 Eclipse IDE 中試用 Lambda 表達式

作者:Deepak Vohra

學習如何充分利用 lambda 和虛擬擴展方法。

2013 年 8 月發佈

Lambda 表達式也稱爲閉包,是匿名類的簡短形式。Lambda 表達式簡化了單一抽象方法聲明接口的使用,因此 lambda 表達式也稱爲功能接口。在 Java SE 7 中,單一方法接口可使用下列選項之一實現。

  • 創建接口實現類。
  • 創建匿名類。

可以使用 lambda 表達式實現功能接口,無需創建類或匿名類。Lambda 表達式只能用於單一方法聲明接口。

Lambda 表達式旨在支持多核處理器架構,這種架構依賴於提供並行機制的軟件,而該機制可以提高性能、減少完成時間。

Lambda 表達式具有以下優點:

  • 簡明的語法
  • 方法引用和構造函數引用
  • 相比於匿名類,減少了運行時開銷

前提條件

要跟隨本文中的示例,請下載並安裝以下軟件:

Lambda 表達式的語法

Lambda 表達式的語法如下所示。

(formal parameter list) ->{ expression or statements }

參數列表是一個逗號分隔的形式參數列表,這些參數與功能接口中單一方法的形式參數相對應。指定參數類型是可選項;如果未指定參數類型,將從上下文推斷。

參數列表必須用括號括起來,但當指定的單一參數不帶參數類型時除外;指定單一形式參數時可以不帶括號。如果功能接口方法不指定任何形式參數,則必須指定空括號。

參數列表後面是 -> 運算符,然後是 lambda 主體,即單一表達式或語句塊。Lambda 主體的結果必須是下列值之一:

  • void,如果功能接口方法的結果是 void
  • Java 類型、基元類型或引用類型,與功能接口方法的返回類型相同

Lambda 主體根據以下選項之一返回結果:

  • 如果 lambda 主體是單一表達式,則返回表達式的值。
  • 如果該方法具有返回類型,且 lambda 主體不是單一表達式,則 lambda 主體必須使用 return 語句返回值。
  • 如果功能接口方法的結果是 void,可以提供一個 return 語句,但這不是必需的。

語句塊必須包含在大括號 ({}) 內,除非語句塊是一個方法調用語句,而其調用的方法的結果是 void。Lambda 主體的結果必須與功能接口中單一方法的結果相同。例如,如果功能接口方法的結果是 void,則 lambda 表達式主體不能返回值。如果功能接口方法具有返回類型 String,則 lambda 表達式主體必須返回 String。如果 lambda 主體是一條語句,並且該方法具有一個返回類型,則該語句必須是 return 語句。調用 lambda 表達式時,將運行 lambda 主體中的代碼。

功能接口

Lambda 表達式與功能接口一起使用,功能接口實際上是一種只有一個抽象方法的接口;功能接口可以包含一個同時也存在於 Object 類中的方法。功能接口的示例有 java.util.concurrent.Callable(具有單一方法 call())和 java.lang.Runnable(具有單一方法 run())。

區別在於,匿名接口類需要指定一個實例創建表達式,以便接口和編譯器用來創建接口實現類的實例。與指定接口類型(或類類型)的匿名類不同,lambda 表達式不指定接口類型。從上下文推斷爲其調用 lambda 表達式的功能接口,也稱爲 lambda 表達式的目標類型

Lambda 表達式的目標類型

Lambda 表達式有一個隱式的目標類型與之關聯,因爲未明確指定接口類型。在 lambda 表達式中,lambda 轉換的目標類型必須是一個功能接口。從上下文推斷目標類型。因此,lambda 表達式只能用在可以推斷目標類型的上下文中。此類上下文包括

  • 變量聲明
  • 賦值
  • return 語句
  • 數組初始值設定項
  • 方法或構造函數的參數
  • Lambda 表達式主體
  • 三元條件表達式
  • 轉換表達式

使用支持 Java SE 8 的 Eclipse IDE

要在 Eclipse IDE 中使用 Java 8,您需要下載一個支持 JDK 8 的 Eclipse 版本。

  1. 在 Eclipse 中,選擇 Windows > Preferences,然後選擇 Java > Installed JREs。使用在前提條件部分下載的 JDK 8 安裝適用於 JDK 8 的 JRE。
  2. 選擇 Java > Compiler,然後將 Compiler compliance level 設爲 1.8,如圖 1 所示。

    圖 1

    圖 1

  3. 單擊 Apply,然後單擊 OK
  4. 在 Eclipse 中創建一個 Java 項目時,請選擇 JDK 1.8 JRE。

接下來,我們將通過一些示例討論如何使用 lambda 表達式。

用 Lambda 表達式創建 Hello 應用程序

我們都很熟悉 Hello 應用程序,當我們提供一個姓名時,它會輸出一條消息。Hello 類聲明瞭兩個字段、兩個構造函數和一個 hello() 方法來輸出消息,如下所示。

public class Hello {
   String firstname;
   String lastname;
   public Hello() {}
   public Hello(String firstname, String lastname) {
      this.firstname = firstname;
      this.lastname = lastname;}
   public void hello() {
      System.out.println("Hello " + firstname + " " + lastname);}
   public static void main(String[] args) {
      Hello hello = new Hello(args[0], args[1]);
      hello.hello();
   }
}

現在,我們來看看 lambda 表達式如何簡化 Hello 示例中的語法。首先,我們需要創建一個功能接口,該接口包含一個返回“Hello”消息的方法。

interface HelloService {String hello(String firstname, String lastname);
    }

創建一個 lambda 表達式,它包含兩個參數,與接口方法的參數相匹配。在 lambda 表達式的主體中,使用 return 語句創建並返回根據 firstname 和 lastname 構造的“Hello”消息。返回值的類型必須與接口方法的返回類型相同,並且 lambda 表達式的目標必須是功能接口 HelloService。參見清單 1。

public class Hello {
   interface HelloService {
      String hello(String firstname, String lastname);
   }

   public static void main(String[] args) {
      
HelloService helloService=(String firstname, String lastname) -> 
{ String hello="Hello " + firstname + " " + lastname; return hello; };
System.out.println(helloService.hello(args[0], args[1]));
        

    }
}

清單 1

我們需要先爲 hello() 的方法參數提供一些程序參數,然後才能運行 Hello 應用程序。在 Package Explorer 中右鍵單擊 Hello.java,然後選擇 Run As > Run Configurations。在 Run Configurations 中,選擇 Arguments 選項卡,在 Program arguments 字段中指定參數,然後單擊 Apply。然後單擊 Close

要運行 Hello.java 應用程序,在 Package Explorer 中右鍵單擊 Hello.java,然後選擇 Run As > Java Application。應用程序的輸出將顯示在 Eclipse 控制檯中,如圖 2 所示。

圖 2

圖 2

Lambda 表達式中的局部變量

Lambda 表達式不會定義新的作用域;lambda 表達式的作用域與封閉作用域相同。例如,如果 Lambda 主體聲明的局部變量與封閉作用域內的變量重名,將產生編譯器錯誤 Lambda expression's local variable i cannot re-declare another local variable defined in an enclosing scope,如圖 3 所示。

圖 3

圖 3

局部變量無論是在 lambda 表達式主體中聲明,還是在封閉作用域中聲明,使用之前都必須先初始化。要證明這一點,請在封閉方法中聲明一個局部變量:

int i;

在 lambda 表達式中使用該局部變量。將產生編譯器錯誤 The local variable i may not have been initialized,如圖 4 所示。

圖 4

圖 4

lambda 表達式中使用的變量必須處於終態或等效終態。要證明這一點,請聲明並初始化局部變量:

int i=5;

給 lambda 表達式主體中的變量賦值。將產生編譯器錯誤 Variable i is required to be final or effectively final,如圖 5 所示。

圖 5

圖 5

可按如下方式將變量 i 聲明爲終態。

final int i=5;

否則,該變量必須爲等效終態,即不能在 lambda 表達式中對該變量賦值。封閉上下文中的方法參數變量和異常參數變量也必須處於終態或等效終態。

Lambda 主體中的 this 和 super 引用與封閉上下文中一樣,因爲 lambda 表達式不會引入新的作用域,這與匿名類不同。

Lambda 表達式是一種匿名方法

Lambda 表達式實際上是一種匿名方法實現;指定形式參數,並使用 return 語句返回值。匿名方法必須按照以下規則所規定的與其實現的功能接口方法兼容。

  • Lambda 表達式返回的結果必須與功能接口方法的結果兼容。如果結果是 void,則 lambda 主體必須與 void 兼容。如果返回一個值,則 lambda 主體必須與值兼容。返回值的類型可以是功能接口方法聲明中返回類型的子類型。
  • Lambda 表達式簽名必須與功能接口方法的簽名相同。Lambda 表達式簽名不能是功能接口方法簽名的子簽名。
  • Lambda 表達式只能拋出那些在功能接口方法的 throws 子句中聲明瞭異常類型或異常超類型的異常。

要證明如果功能接口方法返回一個結果,lambda 表達式也必須返回一個結果,請在目標類型爲 HelloService 的 lambda 表達式中註釋掉 return 語句。因爲功能接口 HelloService 中的 hello() 方法具有一個 String 返回類型,因此產生編譯器錯誤,如圖 6 所示。

圖 6

圖 6

如果功能接口方法將結果聲明爲 void,而 lambda 表達式返回了一個值,那麼將產生編譯器錯誤,如圖 7 所示。

圖 7

圖 7

如果 lambda 表達式簽名與功能接口方法簽名不完全相同,將產生編譯器錯誤。要證明這一點,請使 lambda 表達式參數列表爲空,同時功能接口方法聲明兩個形式參數。將產生編譯器錯誤 Lambda expression's signature does not match the signature of the functional interface method,如圖 8 所示。

圖 8

圖 8

可變參數與數組參數之間不作區分。例如,功能接口方法按如下方式聲明數組類型參數:

interface Int {
      void setInt(int[] i);

   }  

Lambda 表達式的參數列表可以聲明可變參數:

Int int1  =(int... i)->{};

異常處理

Lambda 表達式主體拋出的異常不能超出功能接口方法的 throws 子句中指定的異常數。如果 lambda 表達式主體拋出異常,功能接口方法的 throws 子句必須聲明相同的異常類型或其超類型。

要證明這一點,在 HelloService 接口的 hello 方法中不聲明 throws 子句,從 lambda 表達式主體拋出異常。將產生編譯器錯誤 Unhandled exception type Exception,如圖 9 所示。

圖 9

圖 9

如果在功能接口方法中添加與所拋出異常相同的異常類型,編譯器錯誤將得以解決,如圖 10 所示。但如果使用以 lambda 表達式結果賦值的引用變量來調用 hello 方法,將產生編譯器錯誤,因爲 main 方法中未對異常進行處理,如圖 10 所示。

圖 10

圖 10

Lambda 表達式是一種多態表達式

Lambda 表達式的類型是從目標類型推導出來的類型。相同的 lambda 表達式在不同的上下文中可以有不同的類型。此類表達式稱爲多態表達式。要證明這一點,請定義與 HelloService 具有相同抽象方法簽名的另一功能接口,例如:

interface HelloService2 {
		String hello(String firstname, String lastname);

	}

例如,相同的 lambda 表達式(下面的表達式)可用於所聲明的方法具有相同簽名、返回類型以及 throws 子句的兩個功能接口:

(String firstname, String lastname) -> {
         String hello = "Hello " + firstname + " " + lastname;
         return hello;
      }

沒有上下文,前面的 lambda 表達式沒有類型,因爲它沒有目標類型。但是,如果在具有目標類型的上下文中使用,則 lambda 表達式可以根據目標類型具有不同的類型。在以下兩種情況下,前面的 lambda 表達式具有不同的類型,因爲目標類型不同:HelloService 和 HelloService2

HelloService helloService =(String firstname, String lastname) -> {
         String hello = "Hello " + firstname + " " + lastname;
         return hello;
      };

HelloService2 helloService2 =(String firstname, String lastname) -> {
         String hello = "Hello " + firstname + " " + lastname;
         return hello;
      };

不支持泛型 Lambda。Lambda 表達式不能引入類型變量。

GUI 應用程序中的 Lambda 表達式

java.awt 軟件包中的 GUI 組件使用 java.awt.event.ActionListener 接口註冊組件的操作事件。java.awt.event.ActionListener 接口是一個只有一個方法的功能接口:actionPerformed(ActionEvent e)

使用 addActionListener(ActionListener l) 方法將 java.awt.event.ActionListener 註冊到組件。例如,可以使用應用程序中的匿名內部類按如下方式將 java.awt.event.ActionListener 註冊到 java.awt.Button 組件,用以計算 Button 對象(稱爲 b)被單擊的次數。(更多詳細信息,請參見“如何編寫操作監聽程序”)

b.addActionListener (new ActionListener() {
          int numClicks = 0;
          public void actionPerformed(ActionEvent e) {
             numClicks++;
                text.setText("Button Clicked " + numClicks + " times");
          }
       });

可以用 Lambda 表達式代替匿名內部類,使語法更加簡潔。以下是使用 lambda 表達式將 ActionListener 註冊到 ActionListener 組件的一個示例:

b.addActionListener(e -> {
         numClicks++;
         text.setText("Button Clicked " + numClicks + " times");
      });

   }

對於只有一個參數的 lambda 表達式,用於指定參數的括號可以省略。Lambda 表達式的目標類型(功能接口 ActionListener)是從上下文(即一個方法調用)推斷出來的。

將 lambda 表達式與常見的功能接口結合使用

在本節中,我們將討論一些常見的功能接口如何與 lambda 表達式結合使用。

FileFilter 接口

FileFilter 接口具有單一方法 accept(),用於篩選文件。在 Java 教程 ImageFilter 示例中,ImageFilter 類實現了 FileFilter 接口,並提供了 accept() 方法的實現。accept() 方法用來使用 Utils 類只接受圖像文件(和目錄)。

我們可以使用一個返回 boolean 的 lambda 表達式提供 FileFilter 接口的實現,如清單 2 所示。

import java.io.FileFilter;
import java.io.File;

public class ImageFilter {

   public static void main(String[] args) {
      FileFilter fileFilter = (f) -> {
         String extension = null;
         String s = f.getName();
         int i = s.lastIndexOf('.');

         if (i > 0 && i << s.length() - 1) {
            extension = s.substring(i + 1).toLowerCase();
         }
         if (extension != null) {
            if (extension.equals("tiff") || extension.equals("tif")
               || extension.equals("gif") || extension.equals("jpeg")
               || extension.equals("jpg") || extension.equals("png")
               || extension.equals("bmp")) {
            return true;
         } else {
            return false;
         }
         }
         return false;
      };

      File file = new File("C:/JDK8/Figure10.bmp");
      System.out.println("File is an image file: " + fileFilter.accept(file));

   }
}

清單 2

Eclipse 控制檯顯示了 ImageFilter 類的輸出,如圖 11 所示。

圖 11

圖 11

Runnable 接口

在 Java 教程“定義和啓動線程”一節中,HelloRunnable 類實現了 Runnable 接口,並使用 HelloRunnable 類的實例創建了 Thread。可以使用 lambda 表達式創建 Thread 的 Runnable,如清單 3 所示。Lambda 表達式沒有 return 語句,因爲 Runnable 中 run() 方法的結果是 void

import java.lang.Runnable;

public class HelloRunnable {

   public static void main(String args[]) {
      (new Thread(() -> {
         System.out.println("Hello from a thread");
      })).start();
   }
}

清單 3:

HelloRunnable 類的輸出如圖 12 所示。

圖 12

圖 12

Callable 接口

如果我們創建了一個實現 java.util.concurrent.Callable<V> 泛型功能接口的類,該類需要實現 call() 方法。在清單 4 中,HelloCallable 類實現了參數化類型 Callable<String>

package lambda;

import java.util.concurrent.Callable;

public class HelloCallable implements Callable<String> {
   @Override
   public String call() throws Exception {

      return "Hello from Callable";
   }

   public static void main(String[] args) {
      try {
         HelloCallable helloCallable = new HelloCallable();
         System.out.println(helloCallable.call());
      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
}

清單 4

我們可以使用 lambda 表達式提供 call() 泛型方法的實現。由於 call() 方法不需要任何參數,因此 lambda 表達式中的括號爲空;由於該方法爲參數化類型 Callable<String> 返回 String,所以 lambda 表達式必須返回 String

import java.util.concurrent.Callable;

public class HelloCallable {

   public static void main(String[] args) {
      try {

         Callable<String> c = () -> "Hello from Callable";
         System.out.println(c.call());
      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
}

清單 5

HelloCallable 的輸出如圖 13 所示。

圖 13

圖 13

PathMatcher 接口

java.nio.file.PathMatcher 接口用於匹配路徑。該功能接口具有單一方法 matches(Path path),用於匹配 Path。我們可以使用 lambda 表達式提供 matches() 方法的實現,如清單 6 所示。Lambda 表達式返回 boolean,目標類型是功能接口 PathMatcher

import java.nio.file.PathMatcher;
import java.nio.file.Path;
import java.nio.file.FileSystems;

public class FileMatcher {

   public static void main(String[] args) {

      PathMatcher matcher = (f) -> {
         boolean fileMatch = false;
         String path = f.toString();
         if (path.endsWith("HelloCallable.java"))
            fileMatch = true;
         return fileMatch;
      };
      Path filename = FileSystems.getDefault().getPath(
            "C:/JDK8/HelloCallable.java");
      System.out.println("Path matches: " + matcher.matches(filename));

   }
}

清單 6

FileMatcher 類的輸出如圖 14 所示。

圖 14

圖 14

Comparator 接口

功能接口 Comparator 具有單一方法:compares()。雖然該接口還有 equals() 方法,但 equals() 方法也存在於 Object 類中。除了另一個方法外,功能接口還可以有 Object 類方法。如果我們使用 Comparator 比較 Employee 實體的實例,首先需要定義 Employee POJO,它具有 empIdfirstName 和 lastName 屬性和針對這些屬性的 getter/setter 方法,如清單 7 所示。

import java.util.*;

public class Employee {

   private int empId;
   private String lastName;
   private String firstName;
    

   public Employee() {
   }

   public Employee(int empId, String lastName, String firstName) {
      this.empId = empId;
      this.firstName = firstName;
      this.lastName = lastName;

   }

      // setters and getters
   public int getEmpId() {
      return empId;
   }

   public void setEmpId(int empId) {
      this.empId = empId;
   }

   public String getLastName() {
      return lastName;
   }

   public void setLastName(String lastName) {
      this.lastName = lastName;
   }

   public String getFirstName() {
      return firstName;
   }

   public void setFirstName(String firstName) {
      this.firstName = firstName;
   }

   public  int compareByLastName(Employee x, Employee y) 
   { 
      return x.getLastName().compareTo(y.getLastName()); 
   }

   /**
    * 
    * public static int compareByLastName(Employee x, Employee y) 
   { 
      return x.getLastName().compareTo(y.getLastName()); 
   }
    */
}

清單 7

如清單 8 所示,創建一個名爲 EmployeeSort 的類,根據 lastName 對 Employee 實體的 List 進行排序。在 EmployeeSort 類中,創建 List 對象並向其添加 Employee 對象。使用 Collections.sort 方法對 List 進行排序,並使用匿名內部類爲 sort() 方法創建 Comparator 對象。

package lambda;

import java.util.*;

public class EmployeeSort {

   public static void main(String[] args) {

      Employee e1 = new Employee(1, "Smith", "John");
      Employee e2 = new Employee(2, "Bloggs", "Joe");
      List<Employee> list = new ArrayList<Employee>();
      list.add(e1);
      list.add(e2);

      Collections.sort(list, new Comparator<Employee>() {
         public int compare(Employee x, Employee y) {
            return x.getLastName().compareTo(y.getLastName());
         }
      });
      ListIterator<Employee> litr = list.listIterator();

      while (litr.hasNext()) {
         Employee element = litr.next();
         System.out.print(element.getLastName() + " ");
      }

   }
}

清單 8

可以用 lambda 表達式替換匿名內部類,該表達式有兩個 Employee 類型參數,根據 lastName 比較返回 int 值,如清單 9 所示。

import java.util.*;

public class EmployeeSort {

   public static void main(String[] args) {

      Employee e1 = new Employee(1, "Smith", "John");
      Employee e2 = new Employee(2, "Bloggs", "Joe");
      List<Employee> list = new ArrayList<Employee>();
      list.add(e1);
      list.add(e2);

      Collections.sort(list,
            (x, y) -> x.getLastName().compareTo(y.getLastName()));

      ListIterator<Employee> litr = list.listIterator();

       while (litr.hasNext()) {
         Employee element = litr.next();
         System.out.print(element.getLastName() + " ");
      }

   }
}

清單 9

EmployeeSort 的輸出如圖 15 所示。

圖 15

圖 15

如何推斷目標類型和 Lambda 參數類型?

對於 lambda 表達式,從上下文推斷目標類型。因此,lambda 表達式只能用在可以推斷目標類型的上下文中。這類上下文包括:變量聲明、賦值語句、return 語句、數組初始值設定項、方法或構造函數的參數、lambda 表達式主體、條件表達式和轉換表達式。

Lambda 的形式參數類型也從上下文推斷。除了清單 1 中的 Hello 示例,在前面的所有示例中,參數類型都是從上下文推斷出來的。在後續章節中,我們將討論可以使用 lambda 表達式的上下文。

return 語句中的 Lambda 表達式

Lambda 表達式可以用在 return 語句中。return 語句中使用 lambda 表達式的方法的返回類型必須是一個功能接口。例如,返回 Runnable 的方法的 return 語句中包含一個 lambda 表達式,如清單 10 所示。

import java.lang.Runnable;

public class HelloRunnable2 {

   public static Runnable getRunnable() {
      return () -> {
         System.out.println("Hello from a thread");
      };
   }

   public static void main(String args[]) {

      new Thread(getRunnable()).start();
   }

}

清單 10

Lambda 表達式未聲明任何參數,因爲 Runnable 接口的 run() 方法沒有聲明任何形式參數。Lambda 表達式不返回值,因爲 run() 方法的結果是 void。清單 10 的輸出如圖 16 所示。

圖 16

圖 16

Lambda 表達式作爲目標類型

Lambda 表達式本身可以用作內部 lambda 表達式的目標類型。Callable 中的 call() 方法返回一個 Object,但是 Runnable 中的 run() 方法沒有返回類型。在 HelloCallable2 類中,內部 lambda 表達式的目標類型是 Runnable,外部 lambda 表達式的目標類型是 Callable。目標類型是從上下文(對 Callable<Runnable> 類型的引用變量進行賦值)推斷出來的。

在清單 11 中,內部 lambda 表達式 () -> {System.out.println("Hello from Callable");} 的類型被推斷爲 Runnable,因爲參數列表爲空,並且結果是 void;匿名方法簽名和結果與 Runnable 接口中的 run() 方法相同。外部 lambda 表達式 () -> Runnable 的類型被推斷爲 Callable<Runnable>,因爲 Callable<V> 中的 call() 方法沒有聲明任何形式參數,並且結果類型是類型參數 V

import java.util.concurrent.Callable;

public class HelloCallable2 {

   public static void main(String[] args) {
      try {

         Callable<Runnable> c = () -> () -> {
            System.out.println("Hello from Callable");
         };
          c.call().run();

      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
}

清單 11

HelloCallable2 的輸出如圖 17 所示。

圖 17

圖 17

數組初始值設定項中的 Lambda 表達式

Lambda 表達式可以用在數組初始值設定項中,但不能使用泛型數組初始值設定項。例如,以下泛型數組初始值設定項中的 lambda 表達式將產生編譯器錯誤:

Callable<String>[] c=new Callable<String>[]{ ()->"a", ()->"b", ()->"c" };

將產生編譯器錯誤 Cannot create a generic array of Callable<String>,如圖 18 所示。

圖 18

圖 18

要在數組初始值設定項中使用 lambda 表達式,請指定一個非泛型數組初始值設定項,如清單 12 所示的 CallableArray 類。

import java.util.concurrent.Callable;

public class CallableArray  {

public static void main(String[] args) {
try{


Callable<String>[] c=new Callable[]{ ()->"Hello from Callable a", 
()->"Hello from Callable b", ()->"Hello from Callable c" };

System.out.println(c[1].call());
}catch(Exception e){System.err.println(e.getMessage());}
}
}

清單 12:

CallableArray 中的每個數組初始值設定項變量都是一個 Callable<String> 類型的 lambda 表達式。Lambda 表達式的參數列表爲空,並且 lambda 表達式的結果是一個 String 類型的表達式。每個 lambda 表達式的目標類型從上下文推斷爲 Callable<String> 類型。CallableArray 的輸出如圖 19 所示。

圖 19

圖 19

轉換 Lambda 表達式

Lambda 表達式的目標類型有時可能並不明確。例如,在下面的賦值語句中,lambda 表達式用作 AccessController.doPrivileged 方法的方法參數。Lambda 表達式的目標類型不明確,因爲多個功能接口(PrivilegedAction 和 PrivilegedExceptionAction)都可以是 lambda 表達式的目標類型。

String user = AccessController.doPrivileged(() -> System.getProperty("user.name"));

將產生編譯器錯誤 The method doPrivileged(PrivilegedAction<String>) is ambiguous for the type AccessController,如圖 20 所示。

圖 20

圖 20

我們可以使用 lambda 表達式的轉換將目標類型指定爲 PrivilegedAction<String>,如清單 13 所示的 UserPermissions 類。

import java.security.AccessController;
import java.security.PrivilegedAction;

public class UserPermissions {

   public static void main(String[] args) {

      String user = AccessController
            .doPrivileged((PrivilegedAction<String>) () -> System
               .getProperty("user.name"));
      System.out.println(user);

   }
}

清單 13

Lambda 表達式中使用轉換後的 UserPermissions 輸出如圖 21 所示。

圖 21

圖 21

條件表達式中的 Lambda 表達式

Lambda 表達式可以用在三元條件表達式中,後者的值是這兩個操作數中的任何一個,具體取決於 boolean 條件爲 true 還是 false

在清單 14 所示的 HelloCallableConditional 類中,lambda 表達式 () -> "Hello from Callable:flag true") 和 () -> "Hello from Callable:flag false") 構成了用於賦值的這兩個可供選擇的表達式。Lambda 表達式的目標類型是從上下文(對 Callable<String> 引用變量進行賦值)推斷出來的。隨後,使用該引用變量調用 call() 方法。

import java.util.concurrent.Callable

public class HelloCallableConditional {

   public static void main(String[] args) {
      try {

         boolean flag = true;
         Callable<String> c = flag ? (() -> "Hello from Callable: flag true")
               : (() -> "Hello from Callable: flag false");

         System.out.println(c.call());
      } catch (Exception e) {
         System.err.println(e.getMessage());
      }
   }
}

清單 14

HelloCallableConditional 的輸出如圖 22 所示。

圖 22

圖 22

推斷重載方法中的目標類型

調用重載方法時,將使用與 lambda 表達式最匹配的方法。我們將使用目標類型和方法參數類型選擇最佳方法。

在清單 15 的 HelloRunnableOrCallable 類中,指定了兩個返回類型爲 String 的 hello() 方法(hello() 方法被重載):它們的參數類型分別是 Callable 和 Runnable

將調用 hello() 方法,其中 lambda 表達式作爲方法參數。由於 lambda 表達式 () -> "Hello Lambda" 返回 String,因此會調用 hello(Callable) 方法並輸出 Hello from Callable,因爲 Callable 的 call() 方法具有返回類型,而 Runnable 的 run() 方法沒有。

import java.util.concurrent.Callable;

public class HelloRunnableOrCallable {

   static String hello(Runnable r) {
      return "Hello from Runnable";
   }

   static String hello(Callable c) {
      return "Hello from Callable";
   }

   public static void main(String[] args) {

      String hello = hello(() -> "Hello Lambda");
      System.out.println(hello);

   }
}

清單 15

HelloCallableConditional 的輸出如圖 23 所示。

圖 23

圖 23

Lambda 表達式中的 this

在 lambda 表達式外面,this 引用當前對象。在 lambda 表達式裏面,this 引用封閉的當前對象。不引用封閉實例成員的 lambda 表達式不會存儲對該成員的強引用,這解決了內部類實例保留對封閉類的強引用時經常出現的內存泄漏問題。

在清單 16 所示的示例中,Runnable 是 lambda 表達式的目標類型。在 lambda 表達式主體中,指定了對 this 的引用。創建 Runnable r 實例並調用 run() 方法後,this 引用將調用封閉實例,並從 toString() 方法獲得封閉實例的 String 值。將輸出 Hello from Class HelloLambda 消息。

public class HelloLambda {
     Runnable r = () -> { System.out.println(this); };
  

     public String toString() { return "Hello from Class HelloLambda"; }

     public static void main(String args[]) {
       new HelloLambda().r.run();
       
     }
   }

清單 16

HelloLambda 的輸出如圖 24 所示。

圖 24

圖 24

Lambda 表達式的參數名稱

爲 lambda 表達式的形式參數創建新名稱。如果用作 lambda 表達式參數名稱的名稱與封閉上下文中局部變量名稱相同,將產生編譯器錯誤。在以下示例中,lambda 表達式的參數名稱被指定爲 e1 和 e2,它們同時還用於局部變量 Employee e1 和 Employee e2

Employee e1 = new Employee(1,"A", "C");   
  Employee e2 = new Employee(2,"B","D" );   
  List<Employee> list = new ArrayList<Employee>();   
  list.add(e1);list.add(e2);

    Collections.sort(list, (Employee e1, Employee e2) -> e1.getLastName().compareTo(e2.getLastName()));

Lambda 表達式參數 e1 將導致編譯器錯誤,如圖 25 所示。

圖 25

圖 25

Lambda 表達式參數 e2 也將導致編譯器錯誤,如圖 26 所示。

圖 26

圖 26

對局部變量的引用

在提供匿名內部類的替代方案時,局部變量必須處於終態才能在 lambda 表達式中訪問的要求被取消。JDK 8 也取消了局部變量必須處於終態才能從內部類訪問的要求。在 JDK 7 中,必須將從內部類訪問的局部變量聲明爲終態。

在內部類或 lambda 表達式中使用局部變量的要求已從“終態”修改爲“終態或等效終態”。

方法引用

Lambda 表達式定義了一個匿名方法,其中功能接口作爲目標類型。可以使用方法引用來調用具有名稱的現有方法,而不是定義匿名方法。在清單 9 所示的 EmployeeSort 示例中,以下方法調用將 lambda 表達式作爲方法參數。

Collections.sort(list, (x, y) -> x.getLastName().compareTo(y.getLastName()));

可以按以下方式將 lambda 表達式替換爲方法引用:

Collections.sort(list, Employee::compareByLastName);

分隔符 (::) 可用於方法引用。compareByLastName 方法是 Employee 類中的靜態方法。

public static int compareByLastName(Employee x, Employee y) 
{ return x.getLastName().compareTo(y.getLastName()); 

對於非靜態方法,方法引用可與特定對象的實例一起使用。通過將 compareByLastName 方法非靜態化,可按如下方式將方法引用與與 Employee 實例結合使用:

Employee employee=new Employee();
Collections.sort(list, employee::compareByLastName); 

方法引用甚至不必是所屬對象的實例方法。方法引用可以是任何任意類對象的實例方法。例如,通過方法引用,可以使用 String 類的 compareTo 方法對 String List 進行排序。

String e1 = new String("A");   
  String e2 = new String("B");    
  List<String> list = new ArrayList<String>();   
  list.add(e1);list.add(e2);
Collections.sort(list, String::compareTo);

方法引用是 lambda 表達式的進一步簡化。

構造函數引用

方法引用的作用是方法調用,而構造函數引用的作用是構造函數調用。方法引用和構造函數引用是 lambda 轉換,方法引用和構造函數引用的目標類型必須是功能接口。

在本節中,我們將以 Multimap 爲例討論構造函數引用。Multimap 是一個 Google Collections 實用程序。圖像類型的 Multimap 按如下方式創建:

Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps
        .newListMultimap(
              Maps.<ImageTypeEnum, Collection<String>> newHashMap(),
              new Supplier<List<String>>() { public List<String> get() { 
        return new ArrayList<String>(); 
            } 
        });

在 Multimap 示例中,使用構造函數按如下方式創建 Supplier<List<String>>

new Supplier<List<String>>() { 
            public List<String> get() { 
                return new ArrayList<String>(); 
            } 
        }

構造函數返回 ArrayList<String>。通過構造函數引用,可以使用簡化的語法按如下方式創建 Multimap

Multimap<ImageTypeEnum, String> imageTypeMultiMap = 
Multimaps.newListMultimap(Maps.<ImageTypeEnum, Collection<String>> newHashMap(),ArrayList<String>::new); 

清單 17 顯示了使用構造函數引用的 Multimap 示例,即 ImageTypeMultiMap 類。

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.google.common.base.Supplier;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;

public class ImageTypeMultiMap {
   enum ImageTypeEnum {
      tiff, tif, gif, jpeg, jpg, png, bmp
   }

   public static void main(String[] args) {
      Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps
            .newListMultimap(
                  Maps.<ImageTypeEnum, Collection<String>> newHashMap(),
               ArrayList<String>::new);

      imageTypeMultiMap.put(ImageTypeEnum.tiff, "tiff");
      imageTypeMultiMap.put(ImageTypeEnum.tif, "tif");
      imageTypeMultiMap.put(ImageTypeEnum.gif, "gif");
      imageTypeMultiMap.put(ImageTypeEnum.jpeg, "jpeg");
      imageTypeMultiMap.put(ImageTypeEnum.jpg, "jpg");
      imageTypeMultiMap.put(ImageTypeEnum.png, "png");
      imageTypeMultiMap.put(ImageTypeEnum.bmp, "bmp");

      System.out.println("Result: " + imageTypeMultiMap);
   }
}

清單 17

要測試 ImageTypeMultiMap,我們需要從 https://code.google.com/p/guava-libraries/ 下載 Guava 庫 guava-14.0.1.jar,並將 guava-14.0.1.jar 添加到 Java 構建路徑。ImageTypeMultiMap 的輸出如圖 27 所示。

圖 27

圖 27

虛擬擴展方法

接口的封裝和可重用性是接口的主要優點。但接口的缺點是實現接口的類必須實現所有接口方法。有時只需要接口的部分方法,但在實現接口時必須提供所有接口方法的方法實現。虛擬擴展方法解決了這個問題。

虛擬擴展方法是接口中具有默認實現的方法。如果實現類不提供方法的實現,則使用默認的實現。實現類可以重寫默認實現,或提供新的默認實現。

虛擬擴展方法添加配置來擴展接口的功能,而不會破壞已實現接口較早版本的類的向後兼容性。虛擬擴展方法中的默認實現是用 default 關鍵字提供的。由於虛擬擴展方法提供默認實現,因此不能是抽象方法。

JDK 8 中的 java.util.Map<K,V> 類提供了幾個具有默認實現的方法:

  • default V getOrDefault(Object key,V defaultValue)
  • default void forEach(BiConsumer<? super K,? super V> action)
  • default void replaceAll(BiFunction<? super K,? super V,? extends V> function)
  • default V putIfAbsent(K key,V value)
  • default boolean remove(Object key,Object value)
  • default boolean replace(K key,V oldValue,V newValue)
  • default V replace(K key,V value)
  • default V computeIfAbsent(K key,Function<? super K,? extends V> mappingFunction)
  • default V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
  • default V compute(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
  • default V merge(K key,V value,BiFunction<? super V,? super V,? extends V> remappingFunction)

要證明類在實現接口時無需實現具有默認實現的方法,請創建實現 Map<K,V> 接口的 MapImpl 類:

public class MapImpl<K,V> implements Map<K, V> {

}

清單 18 顯示了完整的 MapImpl 類,實現了不提供默認實現的方法。

import java.util.Collection;
import java.util.Map;
import java.util.Set;

public class MapImpl<K,V> implements Map<K, V> {

   public static void main(String[] args) {

   }

   @Override
   public int size() {
 
      return 0;
   }

   @Override
   public boolean isEmpty() {
 
      return false;
   }

   @Override
   public boolean containsKey(Object key) {

      return false;
   }

   @Override
   public boolean containsValue(Object value) {
      
      return false;
   }

   @Override
   public V get(Object key) {

      return null;
   }

   @Override
   public V put(K key, V value) {

      return null;
   }

   @Override
   public V remove(Object key) {

      return null;
   }

   @Override
   public void putAll(Map<? extends K, ? extends V> m) {

   }

   @Override
   public void clear() {	

   }

   @Override
   public Set<K> keySet() {

      return null;
   }

   @Override
   public Collection<V> values() {

      return null;
   }

   @Override
   public Set<java.util.Map.Entry<K, V>> entrySet() {

      return null;
   }

}

清單 18

雖然 Map<K,V> 接口是一個預定義的接口,但也可以使用虛擬擴展方法定義一個新接口。使用 default 關鍵字創建爲所有方法提供默認實現的 EmployeeDefault 接口,如清單 19 所示。

public interface EmployeeDefault {

   String name = "John Smith";
   String title = "PHP Developer";
   String dept = "PHP";

   default  void setName(String name) {
      System.out.println(name);
   }

   default String getName() {
      return name;
   }

   default void setTitle(String title) {
      System.out.println(title);
   }

   default String getTitle() {
      return title;
   }

   default void setDept(String dept) {
      System.out.println(dept);
   }

   default String getDept() {
      return dept;
   }
}

清單 19

如果使用 default 關鍵字聲明接口方法,則該方法必須按編譯器錯誤指出的方式提供實現,如圖 28 所示。

圖 28

圖 28

默認情況下,接口的字段處於終態,不能在默認方法的默認實現中賦值,如圖 29 中的編譯器錯誤所示。

圖 29

圖 29

沒有實現 EmployeeDefault 接口的類,也可以提供虛擬擴展方法的實現。EmployeeDefaultImpl 類實現了 EmployeeDefault 接口,沒有爲從 EmployeeDefault 繼承的任何虛擬擴展方法提供實現。EmployeeDefaultImpl 類使用方法調用表達式調用虛擬擴展方法,如清單 20 所示。

public class EmployeeDefaultImpl implements EmployeeDefault {

   public static void main(String[] args) {
 
      EmployeeDefaultImpl employeeDefaultImpl=new EmployeeDefaultImpl();
      System.out.println(employeeDefaultImpl.getName());
      System.out.println(employeeDefaultImpl.getTitle());
      System.out.println(employeeDefaultImpl.getDept());

   }

}

清單 20

總結

本文介紹了 JDK 8 的新特性 — lambda 表達式,其語法簡潔,是匿名類的簡短形式。此外,還介紹了虛擬擴展方法,它非常有用,因爲它提供了具有默認方法實現的接口;如果實現類不提供方法的實現,則會使用該默認的方法實現。

另請參見

Lambda 表達式

關於作者

Deepak Vohra 是一名 NuBean 顧問、Web 開發人員、Sun 認證的 Java 1.4 程序員和 Sun 認證的 Java EE Web 組件開發人員。

分享交流

請在 FacebookTwitter 和 Oracle Java 博客上加入 Java 社區對話!

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