1. 程式人生 > >在 Eclipse IDE 中試用 Lambda 表示式 Java

在 Eclipse IDE 中試用 Lambda 表示式 Java

學習如何充分利用 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 語句建立並返回根據firstnamelastname 構造的“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 主體中的 thissuper 引用與封閉上下文中一樣,因為 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 表示式具有不同的型別,因為目標型別不同:HelloServiceHelloService2

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 表示式建立ThreadRunnable,如清單 3 所示。Lambda 表示式沒有 return 語句,因為Runnablerun() 方法的結果是 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,它具有empIdfirstNamelastName 屬性和針對這些屬性的 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 表示式的目標型別不明確,因為多個功能介面(PrivilegedActionPrivilegedExceptionAction)都可以是 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 類中,指定了兩個返回型別為 Stringhello() 方法(hello() 方法被過載):它們的引數型別分別是 CallableRunnable

將呼叫 hello() 方法,其中 lambda 表示式作為方法引數。由於 lambda 表示式 () -> "Hello Lambda" 返回String,因此會呼叫 hello(Callable) 方法並輸出 Hello from Callable,因為Callablecall() 方法具有返回型別,而 Runnablerun() 方法沒有。

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 表示式的引數名稱被指定為e1e2,它們同時還用於區域性變數 Employee e1Employee 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 表示式,其語法簡潔,是匿名類的簡短形式。此外,還介紹了虛擬擴充套件方法,它非常有用,因為它提供了具有預設方法實現的介面;如果實現類不提供方法的實現,則會使用該預設的方法實現。