1. 程式人生 > 實用技巧 >1-06-2 Lambda表示式

1-06-2 Lambda表示式

last modified:2020/10/31

1-06-3-Lambda表示式

6.3.1 為什麼引入lambda表示式

  • lambda表示式是一個可傳遞的程式碼塊,可以在以後執行一次或多次。
    • 將一個程式碼塊傳遞到某個物件,這個程式碼塊會在將來某個時間呼叫。

6.3.2 lambda表示式的語法

  • 帶引數變數的表示式被稱為lambda表示式。

  • 你已經見過Java中的一種lambda表示式形式:引數,箭頭(->)以及一個表示式。

    • 如果程式碼要完成的計算無法放在一個表示式中,就可以像寫方法一樣,把這些程式碼放在中,幷包含顯式的return語句。例如:
    (String first,String second)->
    {
        if (first.1ength() < second.length()) return -1;
    	else if (first.length() > second.length()) return 1;
    	else return 0;
    }
    
    • 即使lambda表示式沒有引數,仍然要提供空括號,就像無引數方法一樣:
    () -> { for (int i = 100; i >= 0; i--) System.out.prinln(i); }
    
    • 如果可以推匯出一個lambda表示式的引數型別,則可以忽略其型別。例如:
    Comparator<String> comp
    (first,second) // Same as (String first,String second)
    -> first.length() - second.length();
    
    • 在這裡,編譯器可以推匯出first和second必然是字串,因為這個lambda表示式將賦給一個字串比較器。(下一節會更詳細地分析這個賦值。)
    • 如果方法只有一個引數,而且這個引數的型別可以推導得出,那麼甚至還可以省略小括號:
    ActionListener listener = event ->
    System.out.println("The time is " + new Date()");
    // Instead of (event) -> .. . or (ActionEvent event) -> ..·
    
    • 無需指定lambda表示式的返回型別。lambda表示式的返回型別總是會由上下文推導得出。例如,下面的表示式
    (String first, String second)-> first.length() - second.length();
    
    • 可以在需要int型別結果的上下文中使用。
    • 如果一個lambda表示式只在某些分支返回一個值,而在另外一些分支不返回值,這是不合法的。例如,(int x)->{ if(x>= 0) return1; }就不合法。
//程式顯示瞭如何在一個比較器和一個動作監聽器中使用lambda表示式
public class LambdaTest
{
    public static void main(String[]args)
    {
    	String[] planets = new String[]{ "Mercury","Venus","Earth”,							
    	"Mars","Jupiter","Saturn","Uranus","Neptune"};
    System.out.println(Arrays.toString(planets));
    system.out.println("Sorted in dictionary order:");
    Arrays.sort(planets);
    System.out.println(Arrays.toString(planets));
    system.out.println("Sorted by length:");
    Arrays.sort(planets,(first,second)->first.length()-second.length());
    System.out.print1n(Arrays.toString(planets));
    Timer t = new Timer(1000, event->
    System.out.println("The time is" +new Date());
    t.start();
    // keep program running until user selects "ok"
    optionPane.showMessageDialog(null,"Quit program?");
    System.exit(O);
    }
}

6.3.3 函式式介面

  • Java中已經又很多封裝程式碼塊的介面,lambda表示式與這些介面是相容的。

  • 對於只有一個抽象方法的介面,需要這種介面的物件時,就可以提供一個lambda表示式。這種介面稱為函式式介面(functional interface)

    註釋:你可能想知道為什麼函式式介面必須有一個抽象方法。不是介面中的所有方法都是抽象的嗎?實際上,介面完全有可能重新宣告Object類的方法,如toString 或clone,這些宣告有可能會讓方法不再是抽象的。(Java API中的一些介面會重新宣告Object方法來附加javadoc註釋。Comparator API就是這樣一個例子。)更重要的是,正如6.1.5節所述,在Java SE 8中,介面可以宣告非抽象方法

  • 為了展示如何轉換為函式式介面,下面考慮Arrays.sort方法。

    • 它的第二個引數需要一個Comparator例項,Comparator就是隻有一個方法的介面,所以可以提供一個lambda表示式:

      Arrays.sort(words,
      		(first, second)->first.length()- second.length();
      

      在底層,Arrays.sort方法會接收實現了Comparator<String>的某個類的物件。在這個物件上呼叫compare方法會執行這個lambda表示式的體。這些物件和類的管理完全取決於具體實現,與使用傳統的內聯類相比,這樣可能要高效得多。最好把lambda表示式看作是一個函式,而不是一個物件,另外要接受lambda表示式可以傳遞到函式式介面。

  • 實際上,在java中,對lambda表示式所能做的也只是能轉換為函式式介面。

  • Java API在java.util.function包中定義了很多非常通用的函式式介面

    • 其中一個介面BiFunction<T,U,R>描述了引數型別為T和U而且返回型別為R的函式。可以把我們的字串比較lambda表示式儲存在這個型別的變數中:

      BiFunction<String,String,Integer> comp
      = (first,second)-> first.length() - second.length();
      

      不過,這對於排序並沒有幫助。沒有哪個Arrays.sort方法想要接收一個BiFunction。如果你之前用過某種函式式程式設計語言,可能會發現這很奇怪。不過,對於Java程式設計師而言,這非常自然。類似Comparator的介面往往有一個特定的用途,而不只是提供一個有指定引數和返回型別的方法。Java SE8沿襲了這種思路。想要用lambda表示式做某些處理,還是要謹記表示式的用途,為它建立一個特定的函式式介面。

    • java.util.function包中有一個尤其有用的介面Predicate:

      public interface Predicate<T>
      {	
      	boolean test(T t);
      	//Additional default and static methods
      }
      

      ArrayList類有一個removelf方法,它的引數就是一個Predicate。這個介面專門用來傳遞lambda表示式。例如,下面的語句將從一個數組列表刪除所有null值:

      list.removeIf(e -> e == null);
      

6.3.4 方法引用

  • 有時,可能已經有現成的方法可以完成你想要傳遞到其他程式碼的某個動作。

    • 例如,假設你希望只要出現一個定時器事件就列印這個事件物件。當然,為此也可以呼叫:

      Timer t = new Timer(1000,event -> System.out.println(event));
      

      但是,如果直接把printIn方法傳遞到Timer構造器就更好了。具體做法如下:

      Timer t = new Timer(1000,System.out::println);
      

      表示式System.out::printIn是一個方法引用(method reference),它等價於lambda表示式

      x->System.out.println(x)
      
    • 再來看一個例子,假設你想對字串排序,而不考慮字母的大小寫。可以傳遞以下方法表示式:

      Arrays.sort(strings,String::compareToIgnoreCase)
      

      從這些例子可以看出,要用::操作符分隔方法名與物件或類名。主要有3種情況:

      • object::instanceMethod
      • Class::staticMethod
      • Class::instanceMethod

      在前2種情況中,方法引用等價於提供方法引數的lambda表示式。前面已經提到,
      System.out::println等價於x->System.out.printIn(x)。類似地,Math:pow等價於(x,y)->Math.pow(x,y)。
      對於第3種情況,第1個引數會成為方法的目標。例如String::compareTolgnoreCase等同於(x,y)-> x.compareTolgnoreCase(y)。

  • 如果有多個同名的過載方法,編譯器就會嘗試從上下文中找出你指的那一個方法

    • 例如,Math.max方法有兩個版本,一個用於整數,另一個用於double值。選擇哪一個版本取決於Math::max轉換為哪個函式式介面的方法引數。類似於lambda表示式,方法引用不能獨立存在,總是會轉換為函式式介面的例項。
  • 可以在方法引用中使用this引數。

    • 例如,this::equals 等同於x -> this.equals(x)。使用super也是合法的。下面的方法表示式super::instanceMethod使用this 作為目標,會呼叫給定方法的超類版本。

6.3.5 構造器引用

  • 構造器引用與方法引用很類似,只不過方法名為new

    • 例如,Person::new 是Person構造器的一個引用。哪個構造器呢?這取決於上下文

    • 假設你有一個字串列表。可以把它轉換為一個Person物件陣列,為此要在各個字串上呼叫構造器,呼叫如下:

      Arraylist<String> names= ... ;
      // map方法會為各個列表元素呼叫Person(String)構造器
      Strean<Person> strean = names.stream().map(Person::new) ;
      List<Person> people = strean.collect(Collectors . tolist());
      

      如果有多個Person構造器,編譯器會選擇有一個String引數的構造器,因為它從上下文推匯出這是在對一個字串呼叫構造器。

    • 可以用陣列型別建立構造器引用

      • 例如,int[]::new 是一個構造器引用,它有一個引數:即陣列的長度。這等價於lambda表示式x -> new int[x]。
    • Java有一個限制,無法構造泛型型別T的陣列

      • 陣列構造器引用對於克服這個限制很有用。表示式new T[n]會產生錯誤,因為這會改為new Object[n]。對於開發類庫的人來說,這是一個問題。例如,假設我們需要一個Person物件陣列。Stream介面有一個toArray方法可以返回Object陣列:

        0bject[] people = stream.toArray();
        

        不過,這並不讓人滿意。使用者希望得到一個Person引用陣列,而不是Object引用陣列。流庫利用構造器引用解決了這個問題。可以把Person[]:new傳入toArray方法:

        Person[] people = stream.toArray(Person[]::new);
        

        toArray方法呼叫這個構造器來得到一個正確型別的陣列。然後填充這個陣列並返回。

6.3.6 變數作用域

  • lambda 表示式有3個部分:

    • 1)一個程式碼塊;
    • 2)引數;
    • 3)自由變數的值,這是指非引數而且不在程式碼中定義的變數。
  • public static void repeatMessage(String text, int delay)
    {
        Actionlistener listener = event ->
    	{
    		System.out.println(text);
    		Toolkit.getDefaultToolkit().beep();
        };
    	new Timer(delay,listener).start();
    }
    //來看這樣一個呼叫:
    repeatMessage("Hel1o",1000); // Prints Hel1o every 1,000 milliseconds
    
  • 在我們的例子中,這個lambda表示式有1個自由變數text。表示lambda表示式的資料結構必須儲存自由變數的值,在這裡就是字串"Hello"。我們說它被lambda表示式捕獲
    ( captured)
    。(下面來看具體的實現細節。例如,可以把一個lambda表示式轉換為包含一個方法的物件,這樣自由變數的值就會複製到這個物件的例項變數中。)

    註釋:關於程式碼塊以及自由變數值有一個術語: 閉包( closure)。 如果有人吹噓他們的語言有閉包,現在你也可以自信地說Java也有閉包。在Java中,lambda表示式就是閉包。

  • 可以看到,lambda表示式可以捕獲外圍作用域中變數的值

  • 在Java中,要確保所捕獲的值是明確定義的,這裡有一個重要的限制。

    • 在lambda表示式中,只能引用值不會改變的變數。例如,下面的做法是不合法的:

      public static void countDown(int start,int delay)
      {
          ActionListener listener = event ->{
          	 start--; // Error: Can't mutate captured variable
         		 System.out.println(start);
          };
          new Timer(delay,listener) .start();
      }
      
    • 另外如果在lambda表示式中引用變數,而這個變數可能在外部改變,這也是不合法的。
      例如,下面就是不合法的:

      public static void repeat(String text,int count)
      {
      	for (int i = 1; i <= count; i++)
      	{
      	ActionListener listener = event -> {
      		System.out.println(i + "; " + text);
      			// Error: Cannot refer to changing i
      		};
      		new Timer(1000 ,listener).start();
      	}
      }
      

      這裡有一條規則: lambda 表示式中捕獲的變數必須實際上是最終變數( effectively final)。實際上的最終變數是指,這個變數初始化之後就不會再為它賦新值。在這裡,text 總是指示同一個String 物件,所以捕獲這個變數是合法的。不過, i的值會改變,因此不能捕獲i。

  • lambda表示式的體與巢狀塊有相同的作用域。這裡同樣適用命名衝突和遮蔽的有關規則。

    • 在lambda表示式中宣告與一個區域性變數同名的引數或區域性變數是不合法的。
    Path first = Paths.get("/usr/bin");
    Comparator<String> comp =
    (first, second) -> first.length() - second.length();
    // Error: Variable first already defined
    
    • 在方法中,不能有兩個同名的區域性變數,因此,lambda表示式中同樣也不能有同名的區域性變數。

    • 在一個lambda表示式中使用this關鍵字時,是指建立這個lambda表示式的方法的this引數。例如,考慮下面的程式碼:

      public class Application()
      {
      	public void init()
      	{
      		ActionListener listener = event ->
      		{
                  //表示式this.toString()會呼叫Application物件的					//toString方法,而不是ActionListener例項的方法。
      			System.out.print1n(this. toString());
      			...
      		}
      		...
      	}
      }
      

      在lambda表示式中,this 的使用並沒有任何特殊之處lambda 表示式的作用域巢狀在init方法中,與出現在這個方法中的其他位置一樣,lambda表示式中this的含義並沒有變化。

6.3.7 處理lambda表示式

  • 使用lambda表示式的重點是延遲執行( deferred execution)。

  • 之所以希望以後再執行程式碼,這有很多原因,如:

    • 在一個單獨的執行緒中執行程式碼;
    • 多次執行程式碼;
    • 在演算法的適當位置執行程式碼(例如,排序中的比較操作);
    • 發生某種情況時執行程式碼(如,點選了一個按鈕,資料到達,等等);
    • 只在必要時才執行程式碼。
  • 下面來看一個簡單的例子。假設你想要重複一個動作n次。將這個動作和重複次數傳遞
    到一個repeat方法:

    repeat(10, () -> System.out.println("Hello, World!"));
    

    要接受這個lambda表示式,需要選擇(偶爾可能需要提供)一個函式式介面

    表6-1列出了Java API中提供的最重要的函式式介面。

    在這裡,我們可以使用Runnable介面:

    public static void repeat(int n, Runnable action)
    {
    	for (int i =0; i < n; i++) action.run();
    }
    

    需要說明,呼叫action.run()時會執行這個lambda表示式的主體。

  • 現在讓這個例子更復雜一些。我們希望告訴這個動作它出現在哪一次迭代中。 為此,需
    要選擇一個合適的函式式介面,其中要包含一個方法,這個方法有一個int引數而且返回類
    型為void。處理int值的標準介面如下:

    public interface IntConsumer{
    	void accept(int value);
    }
    

    下面給出repeat方法的改進版本:

    public static void repeat(int n, IntConsumer action){
    	for (int i =0; i < n; i++) action.accept(i);
    }
    

    可以如下呼叫它:

    repeat(10, i -> Sysem.out.pritnln("Countdown: "+ (9 - i)));
    

    表6-2列出了基本型別int、long和double的34個可能的規範。最好使用這些特殊
    化規範來減少自動裝箱。出於這個原因,上一個例子中使用了IntConsumer 而不是
    Consumer<Integer>。

    • 最好使用6-1或6-2中的介面
    • 大多數標準函式式介面都提供了非抽象方法來生成或合併函式。
      • 如,Predicate.isEqual(a)等同於a::equal,不過如果a為null也能正常工作。
    • 如果設計你自己的介面,其中只有一個抽象方法,可以用@FunctionalInterface注
      **解來標記這個介面。這樣做有兩個優點。
      • 如果你無意中增加了另一個非抽象方法,編譯器會產生一個錯誤訊息。
      • 另外javadoc頁裡會指出你的介面是一個函式式介面。
    • 並不是必須使用註解。根據定義,任何有一個抽象方法的介面都是函式式介面。不
      過使用@FunctionalInterface 註解確實是一個很好的做法。

6.3.8 再談Comparator(!!!)

  • Comparator介面包含很多方便的靜態方法來建立比較器。這些方法可以用於lambda表示式或方法引用。

  • 靜態comparing方法取一個“鍵提取器”函式,它將型別T對映為一個可比較的型別
    (如String)。對要比較的物件應用這個函式,然後對返回的鍵完成比較。

    例如,假設有一個Person物件陣列,可以如下按名字對這些物件排序:

    Arrays.sort(people,Comparator.comparing(Person:getName));
    

    與手動實現一個Comparator相比,這當然要容易得多。另外,程式碼也更為清晰,因為顯然我們都希望按人名來進行比較。
    可以把比較器與thenComparing方法串起來。例如,

    Arrays.sort(people,Comparator.comparing(Person::getLastName)
    .thenComparing(Person::getFirstNane));
    

    如果兩個人的姓相同,就會使用第二個比較器。
    這些方法有很多變體形式。可以為comparing和thenComparing方法提取的鍵指定一個比較器。例如,可以如下根據人名長度完成排序:

    Arrays.sort(people, Comparator.comparing(Person:getName,
    (s, t) -> Integer.compare(s.length(), t.length())));
    

    另外,comparing 和thenComparing方法都有變體形式,可以避免int、long或double值的裝箱。要完成前一個操作,還有一種更容易的做法:

    Arrays. sort(people, Comparator . comparingInt(p -> p.getName(). length));
    

    如果鍵函式可以返回null,可能就要用到nullFirst和nullsLast介面卡這些靜態方
    法會修改現有的比較器,從而在遇到null值時不會丟擲異常,而是將這個值標記為小於或
    大於正常值。例如,假設一個人沒有中名時getMiddleName會返回一個nul,就可以使用

    Comparator.comparing(Person:.getMiddleName(),        Comparator.nullsFirst(...))
    

    nullsFirst方法需要一個比較器, 在這裡就是比較兩個字串的比較器。

    naturalOrder 方法可以為任何實現了Comparable的類建立一個比較器。在這裡,Comparator.<String> naturalOrder()正是我們需要的。下面是一個完整的呼叫,可以按可能為null 的中名進行排序。這裡使用了一個靜態匯入java.util.Comparator.*,以便理解這個表示式。注意naturalOrder的型別可以推導得出。

    Arrays.sort(people,comparing(Person::getMiddleName,
    	nullsFirst(naturalOrder())));
    

    靜態reverseOrder方法會提供自然順序的逆序。要讓比較器逆序比較,可以使用reversed例項方法。例如naturalOrder.(reversed)等同於reverseOrder()。