Java 8 Lambda 表達式

Lambda 是啥玩意

簡單來講,Lambda 就是一個匿名的方法,就這樣,沒啥特別的。它採用一種很是簡潔的方式來定義方法。當你想傳遞可複用的方法片斷時,匿名方法很是有用。例如,將一個方法傳遞給另一個方法。html

Tips
其實不少主流語言早已支持 lambda 表達式,例如,Scala,C#,Objective-C,Ruby,C++(11), Python等等。因此也不是啥新玩意兒。java

匿名方法 VS 匿名類

須要謹記一點,在 Java 裏,匿名方法和匿名類並非相同的。匿名類仍然須要實例化對象,匿名類雖然沒有明確的名字,但它只有是一個對象時纔可以使用。
而匿名方法並不須要給它分配實例,方法與做用的數據分離,而對象與它所做用的數據密切相關。數組

Java 中的 Lambda 表達式

在 Java 8以前,一個實現了只有一個抽象方法的接口的匿名類看起來更像Lambda 表達式。下面的代碼中,anonymousClass方法調用waitFor方法,參數是一個實現接口的Condition類,實現的功能爲,當知足某些條件,Server 就會關閉。
下面的代碼是典型的匿名類的使用。閉包

void anonymousClass() {
    final Server server = new HttpServer();
    waitFor(new Condition() {
        @Override
        public Boolean isSatisfied() {
            return !server.isRunning();
        }
    }

下面的代碼用 Lambda 表達式實現相同的功能:oracle

void closure() { 
     Server server = new HttpServer();
     waitFor(() -> !server.isRunning()); 
 }

其實,上面的waitFor方法,更接近於下面的代碼的描述:app

class WaitFor {
    static void waitFor(Condition condition) throws   
    InterruptedException {
        while (!condition.isSatisfied())
            Thread.sleep(250);
    }
}

一些理論上的區別

實際上,上面的兩種方法的實現都是閉包,後者的實現就是Lambda 表示式。這就意味着二者都須要持有運行時的環境。在 Java 8 以前,這就須要把匿名類所須要的一切複製給它。在上面的例子中,就須要把 server 屬性複製給匿名類。編輯器

由於是複製,變量必須聲明爲 final 類型,以保證在獲取和使用時不會被改變。Java 使用了優雅的方式保證了變量不會被更新,因此咱們不用顯式地把變量加上 final 修飾。ide

Lambda 表達式則不須要拷貝變量到它的運行環境中,從而 Lambda 表達式被當作是一個真正的方法來對待,而不是一個類的實例。函數

Lambda 表達式不須要每次都要被實例化,對於 Java 來講,帶來巨大的好處。不像實例化匿名類,對內存的影響能夠降到最小。測試

整體來講,匿名方法和匿名類存在如下區別:

  • 類必須實例化,而方法沒必要;
  • 當一個類被新建時,須要給對象分配內存;
  • 方法只須要分配一次內存,它被存儲在堆的永久區內;
  • 對象做用於它本身的數據,而方法不會;
  • 靜態類裏的方法相似於匿名方法的功能。

一些具體的區別

匿名方法和匿名類有一些具體的區別,主要包括獲取語義和覆蓋變量。

獲取語義

this 關鍵字是其中的一個語義上的區別。在匿名類中,this 指的是匿名類的實例,例若有了內部類爲Foo$InnerClass,當你引用內部類閉包的做用域時,像Foo.this.x的代碼看起來就有些奇怪。
在 Lambda 表達式中,this 指的就是閉包做用域,事實上,Lambda 表達式就是一個做用域,這就意味着你不須要從超類那裏繼承任何名字,或是引入做用域的層級。你能夠在做用域裏直接訪問屬性,方法和局部變量。
例如,下面的代碼中,Lambda 表達式能夠直接訪問firstName變量。

public class Example {
    private String firstName = "Tom";

    public void example() {
        Function<String, String> addSurname = surname -> {
            // equivalent to this.firstName
            return firstName + " " + surname;  // or even,   
        };
    }
}

這裏的firstName就是this.firstName的簡寫。
可是在匿名類中,你必須顯式地調用firstName

public class Example {
    private String firstName = "Jerry";

    public void anotherExample() {
        Function<String, String> addSurname = new Function<String,  
        String>() {
            @Override
            public String apply(String surname) {
                return Example.this.firstName + " " + surname;   
            }
        };
    }
}

覆蓋變量

在 Lambda 表達式中,

public class ShadowingExample {

    private String firstName = " Tim";

    public void shadowingExample(String firstName) {
        Function<String, String> addSurname = surname -> {
            return this.firstName + " " + surname;
        };
    }
}

由於 this 在Lambda 表達式中,它指向的是一個封閉的做用域,因此this.firstName對應的值是「Tim」,而不是跟它同名的參數的值。若是去掉this,那麼引用的則是方法的參數。

在上面的例子中,若是用匿名類來實現的話,firstName指的就是方法的參數;若是想訪問最外面的firstName,則使用Example.this.firstName

public class ShadowingExample {

    private String firstName = "King";

    public void anotherShadowingExample(String firstName) {
        Function<String, String> addSurname = new Function<String,  
        String>() {
            @Override
            public String apply(String surname) {
                return firstName + " " + surname;
            }
        };
    }
}

Lambda 表達式基本語法

Lambda 表達式基本上就是匿名函數塊。它更像是內部類的實例。例如,咱們想對一個數組進行排序,咱們可使用Arrays.sort方法,它的參數是Comparator接口,相似於下面的代碼。

Arrays.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first.compareTo(second);
    }
});

參數裏的Comparator實例就是一個抽象片斷,自己沒有別的。在這裏只有在 sort 方法中被使用。
若是咱們用新的語法來替換,用 Lambda 表達式的方式來實現:

Arrays.sort(numbers, (first, second) -> first.compareTo(second));

這種方式更加簡潔,實際上,Java 把它當作Comparator類的實例來對待。若是咱們把 sort的第二個參數從 Lambda 表達式中抽取出來,它的類型爲Comparator<Integer>

Comparator<Integer> ascending = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, ascending);

語法分解

你能夠把單一的抽象方法轉換成 Lambda 表達式。
舉例,若是咱們有一個接口名爲Example,裏面只有一個抽象方法apply,該抽象方法返回某一類型。

interface Example {
     R apply(A args);
}

咱們能夠匿名實現此接口裏的方法:

new Example() {
    @Override
    public R apply(A args) {
        body
    }
};

轉換成 Lambda 表達式的話,咱們去掉實例和聲明,去掉方法的細節,只保留方法的參數列表和方法體。

(args) {
    body
}

咱們引入新的符號(->)來表示 Lambda 表達式。

(args) -> {
    body
}

拿以前排序的方法爲例,首先咱們用匿名類來實現:

Arrays.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first.compareTo(second);
    }
});

下一步,去掉實例和方法簽名:

Arrays.sort(numbers, (Integer first, Integer second) {
    return first.compareTo(second);
});

引用 Lambda 表達式:

Arrays.sort(numbers, (Integer first, Integer second) -> {
    return first.compareTo(second);
});

完成!但有些地方能夠進一步優化。你能夠去掉參數的類型,編譯器已經足夠聰明知道參數的類型。

Arrays.sort(numbers, (first, second) -> {
    return first.compareTo(second);
});

若是是一個簡單的表達式的話,例如只有一行代碼,你能夠去掉方法體的大括號,若是有返回值的話,return 關鍵字也能夠去掉。

Arrays.sort(numbers, (first, second) -> first.compareTo(second));

若是Lambda 只有一個參數的話,參數外面的小括號也能夠去掉。

(x) -> x + 1

去掉小括號後,

x -> x + 1

下一步咱們作下總結,

(int x, int y) -> { return x + y; }
(x, y) -> { return x + y; }
(x, y) -> x + y; x -> x * 2
() -> System.out.println("Hello");
System.out::println;

第一個方式是完整的 Lambda 的聲明和使用的方式,不過有些冗餘,其實,參數的類型能夠省略;
第二個方式是去掉參數類型的 Lambda 表達式;
第三個方式是,若是你的方法體只有一行語句,你能夠直接省略掉大括號和 return 關鍵字;
第四個方式是沒有參數的 Lambda 表達式;
第五個方式是Lambda 表達式的變種:是Lambda 表達式的一種簡寫,稱爲方法引用。例如:

System.out::println;

實際上它是下面Lambda 表達式的一種簡寫:

(value -> System.out.prinltn(value)

深刻 Lambda表達式

函數式接口

Java 把 Lambda表達式看成是一個接口類型的實例。它把這種形式被稱之爲函數式接口。一個函數式接口就是一個只有單一方法的接口,Java把這種方法稱之爲「函數式方法」,但更經常使用的名字爲單一抽象方法(single abstract method" 或 SAM)。例如JDK中存在的接口例如RunnableCallable

@FunctionalInterface

Oracle 引入了一個新的註解爲@FunctionalInterface, 用來標識一個接口爲函數式接口。它基本上是用來傳達這一用途,除此而外,編輯器還會作一些額外的檢查。
好比,下面的接口:

public interface FunctionalInterfaceExample {
    // compiles ok
}

若是加上@FunctionalInterface註解,則會編譯錯誤:

@FunctionalInterface // <- error here
    public interface FunctionalInterfaceExample {
      // doesn't compile
}

編譯器就會報錯,錯誤的詳細信息爲「Invalid '@FunctionalInterface' annotation; FunctionalInterfaceExample is not a functional interface」。意思是沒有定義一個單一的抽象方法。
而若是咱們定義了兩個抽象方法會如何?

@FunctionalInterface
public interface FunctionalInterfaceExample {
    void apply();
    void illegal(); // <- error here
}

編譯器再次報錯,提示爲"multiple, non-overriding abstract methods were found"。因此,一旦使用了此註解,則在接口裏只能定義一個抽象方法。

而如今有這樣一種狀況,如歌一個接口繼承了另外一個接口,會怎麼辦?咱們建立一個新的函數式接口爲A,定義了另外一個接口B,B繼承A,則B仍然是一個函數式接口,它繼承了A的apply方法。

@FunctionalInterface
interface A {
    abstract void apply();
}

interface B extends A {

若是你想看起來更加清晰,能夠複寫父類的方法:

@FunctionalInterface
interface A {
    abstract void apply();
}

interface B extends A {
    @Override
    abstract void apply();
}

咱們能夠用下面的代碼來測試一下上面的兩個接口是否爲函數式接口:

@FunctionalInterface
public interface A {
    void apply();
}

public interface B extends A {
    @Override
    void apply();
}

public static void main(String... args) {
   A a = () -> System.out.println("A");
   B b = () -> System.out.println("B");
   a.apply(); // 打印:A
   b.apply(); // 打印:B
}

若是B接口繼承了A接口,那麼在B接口中就不能定義新的方法了,不然編譯器會報錯。

除了這些,在Java 8 中接口有了一些新的改進:

  • 能夠添加默認方法;
  • 能夠包含靜態接口方法;
  • java.util.function包中增長了一些新的接口,例如,FunctionPredicate

方法引用

簡單來講,方法引用就是 Lambda 表達式的一種簡寫。當你建立一個 Lambda 表達式時,你建立了一個匿名方法並提供方法體,但你使用方法引用時,你只須要提供已經存在的方法的名字,它自己已經包含方法體。
它的基本語法以下;

Class::method

或一個更加簡潔明瞭的例子:

String::valueOf

"::"符號前面表示的是目標引用,後面表示方法的名字。因此,在上面的例子,String 類做爲目標類,用來尋找它的方法valueOf,咱們指的就是 String 類上的靜態方法。

public static String valueOf(Object obj) { ... }

"::"稱之爲定界符,當咱們使用它的時候,只是用來引用要使用的方法,而不是調用方法,因此不能在方法後面加()。
String::valueOf(); // error
你不能直接調用方法引用,只是用來替代 Lambda 表達式,因此,哪裏使用 Lambda 表達式了,哪裏就可使用方法引用了。
因此,下面的代碼並不能運行:

public static void main(String... args) {
    String::valueOf;
}

這是由於該方法引用不能轉化爲Lambda 表達式,由於編譯器沒有上下文來推斷要建立哪一種類型的Lambda。
咱們知道這個引用實際上是等同於下面的代碼:

(x) -> String.valueOf(x)

但編譯器還不知道。雖然它能夠知道一些事情。它知道,做爲一個Lambda,返回值應該是字符串類型,由於valueOf方法的返回值爲字符串類型。但它不知道做爲論據須要提供什麼信息。咱們須要給它一點幫助,給它更多的上下文信息。
下面咱們建立一個函數式接口Conversion

@FunctionalInterface
interface Conversion {
    String convert(Integer number);
}

接下來咱們須要建立一個場景去使用這個接口做爲一個 Lambda,咱們定義了下面的方法:

public static String convert(Integer number, Conversion function) {
    return function.convert(number);
}

其實,咱們已經給編譯器提供了足夠多的信息,能夠把一個方法引用轉換成一個等同的 Lambda。當咱們調用convert方法時,咱們能夠把以下代碼傳遞給 Lambda。

convert(100, (number) -> String.valueOf(number));

咱們能夠用把上面的 Lambda 替換爲方法引用,

convert(100, String::valueOf);

另外一種方式是咱們告訴編譯器,把引用分配給一個類型:

Conversion b = (number) -> String.valueOf(number);

用方法引用來表示:

Conversion b = String::valueOf

方法引用的種類

在 Java 中,有四種方法引用的類型:

  • 構造方法引用;
  • 靜態方法引用:
  • 兩種實例方法引用。

最後兩個有點混亂。第一種是特定對象的方法引用,第二個是任意對象的方法引用,而是特定類型的方法引用。區別在於你想如何使用該方法,若是你事先並不知道有沒有實例。

構造方法引用

構造方法的基本引用以下:

String::new

它會建立一個 Lambda 表達式,而後調用String 無參的構造方法。
它實際上等同於:

() -> new String()

須要注意的是構造方法引用沒有括號,它只是引用,並非調用,上面的例子只是引用了 String類的構造方法,並無真正去實例化一個字符串對象。
接下來咱們看一個實際應用構造方法引用的例子。
看先的例子,循環十遍爲 list 增長對象。

public void usage() {
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(new Object());
  }
}

若是咱們想複用實例化的功能,咱們能夠抽取出一個新的方法initialisefactory建立對象。

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, ...);
}

private void initialise(List<Object> list, Factory<Object> factory){
    for (int i = 0; i < 10; i++) {
        list.add(factory.create());
    }
 }

Factory是一個函數式接口,包含一個create方法,此方法返回 Object 對象,咱們能夠用 Lambda 的方式向 list 中添加對象。

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, () -> new Object());
}

或者咱們用構造方法引用的方式來替換:

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, Object::new);
}

上面的方法其實還有待改進,上面只是建立 Object 類型的對象,咱們能夠增長泛型,實現能夠建立更多類型的方法。

public void usage() {
    List<String> list = new ArrayList<>();
    initialise(list, String::new);
}

private <T> void initialise(List<T> list, Factory<T> factory) {
    for (int i = 0; i < 10; i++) {
        list.add(factory.create());
    }
}

到如今爲知,咱們演示的都是無參的構造方法的引用,若是是帶有參數的構造方法的引用該如何處理呢?
當有多個構造函數時,使用相同的語法,但編譯器計算出哪一個構造函數是最佳匹配。它基於目標類型和推斷功能接口,它能夠用來建立該類型。
例如,咱們有個 Person 類,它有一個多個參數的構造方法。

class Person {
    public Person(String forename, String surname, LocalDate    
    birthday, Sex gender, String emailAddress, int age) {
      // ...
    }

回到上面的例子,咱們能夠以下使用:

initialise(people, () -> new Person(forename, surname, birthday,
                                    gender, email, age));

可是若是想使用這個構造方法引用,則須要 Lambda 表達式提供以下參數:

initialise(people, () -> new Person(forename, surname, birthday,
                                    gender, email, age));

特定對象的方法引用

下面是特定對象的方法引用的例子:

x::toString

x就是咱們想要獲得的對象。它等同於下面的Lambda 表達式。

() -> x.toString()

這種方法引用能夠爲咱們提供便利的方式在不一樣的函數式接口類型中進行切換。看例子:

Callable<String> c = () -> "Hello";

Callable的方法爲call,當被調用時返回「Hello」。
若是咱們有另一個函數式接口Factory,咱們可使用方法引用的方式來轉變Callable這個函數式接口。

Factory<String> f = c::call;

咱們能夠從新建立一個 Lambda表達式,可是這個技巧是重用預約義的Lambda的一個有用的方式。 將它們分配給變量並重用它們以免重複。
咱們有下面一個例子:

public void example() {
    String x = "hello";
    function(x::toString);
}

這個例子中方法引用使用了閉包。他建立了一個 Lambda用來調用x對象上的toString方法。
上面function方法的簽名和實現以下所示:

public static String function(Supplier<String> supplier) {
    return supplier.get();
}

函數式接口Supplier的定義以下:

@FunctionalInterface
public interface Supplier<T> {
  T get();
}

當使用此方法時,它經過get方法返回一個字符串,並且這是惟一的在咱們的結構中獲取字符串的方式。它等同於:

public void example() {
  String x = "";
  function(() -> x.toString());
}

須要注意的是,這裏的 Lambda 表達式沒有參數。這代表x變量在Lambda的局部做用域裏是不可用的,若是可用必需要放在它的做用域以外。咱們必需要掩蓋變量x
若是用匿名類來實現的話,應該是下面的樣子,這些須要主意,x變量是如何傳遞的。

public void example() {
    String x = "";
    function(new Supplier<String>() {
        @Override
        public String get() {
            return x.toString(); // <- closes over 'x'
        }
    });
}

任意對象的實例方法引用(實例隨後提供)

最後一種類型的實例方法引用的格式是這樣的:

Object::toString

儘管在「::」左邊指向的是一個類(有點相似於靜態方法引用),實際上它是指向一個對象,toString方法是Object類上的實例方法,不是靜態方法。您可能不使用常規實例方法語法的緣由是,尚未引用的實例。
在之前,當咱們調用x::toString時,咱們是知道x的類型,可是有些狀況咱們是不知道的,但你仍然能夠傳遞一個方法引用,可是在後面使用此語法時須要提供對應的類型。
例如,下面的表達式等同於x沒有限制的類型。

(x) -> x.toString()

有兩種不一樣的實例方法的引用基本是學術上的。有時候,你須要傳遞一些東西,其餘時候,Lambda 的用法會爲你提供。
這個例子相似於一個常規的方法引用;它此次調用String 對象的toString方法,該字符串提供給使用 Lambda 的函數,而不是從外部做用域傳遞的函數。

public void lambdaExample() {
    function("value", String::toString);
}

這個String看起來像是引用一個類,實際上是一個實例。是否是有些迷惑,爲了能清晰一些,咱們須要看一個使用 Lambda 表達式的方法,以下:

public static String function(String value, Function<String, String> function) {
    return function.apply(value);
}

因此,這個 String 實例直接傳遞給了方法,它看起來像一個徹底合格的Lambda。

public void lambdaExample() {
    function("value", x -> x.toString());
}

上面的代碼能夠簡寫成String::toString, 它是在說在運行時給我提供對象實例。
若是你想用匿名類展開加以理解,它是這個樣子的。參數x是可用的並無被遮蔽,因此它更像是Lambda 表達式而不是閉包。

public void lambdaExample() {
    function("value", new Function<String, String>() {
      @Override
      // takes the argument as a parameter, doesn't need to close 
      over it
      public String apply(String x) {
        return x.toString();
      }
    });
}

方法引用的總結

Oracle描述了四種類型的方法引用,以下所示。

種類 舉例
靜態方法引用 ContainingClass::staticMethodName
特定對象的實例方法引用 ContainingObject::instanceMethodName
特定類型的任意對象的實例方法引用 ContainingType::methodName
構造方法引用 ClassName::new

下面是方法引用的語法和具體的例子。

種類 語法 舉例
靜態方法引用 Class::staticMethodName String::valueOf
特定對象的實例方法引用 object::instanceMethodName x::toString
特定類型的任意對象的實例方法引用 Class::instanceMethodName String::toString
構造方法引用 ClassName::new String::new

最後,上面的方法引用等同於下面對應的 Lambda 表達式。

種類 語法 Lambda
靜態方法引用 Class::staticMethodName (s) -> String.valueOf(s)
特定對象的實例方法引用 object::instanceMethodName () -> "hello".toString()
特定類型的任意對象的實例方法引用 Class::instanceMethodName (s) -> s.toString()
構造方法引用 ClassName::new () -> new String()

目前爲止,Labmbda 的主要內容已經介紹完畢。

相關文章
相關標籤/搜索