java8之lambda介紹

Java是一門面向對象編程語言。面向對象編程語言和函數式編程語言中的基本元素(Basic Values)均可以動態封裝程序行爲:面向對象編程語言使用帶有方法的對象封裝行爲,函數式編程語言使用函數封裝行爲。但這個相同點並不明顯,由於Java的對象每每比較「重量級」:實例化一個類型每每會涉及不一樣的類,並須要初始化類裏的字段和方法。因而對於有些Java對象,只是對單個函數的封裝。 下面這個典型用例:Java API中定義了一個接口(通常被稱爲回調接口),用戶經過提供這個接口的實例來傳入指定行爲,例如:java

public interface ActionListener {
  void actionPerformed(ActionEvent e);
}

這裏並不須要專門定義一個類來實現ActionListener接口,由於它只會在調用處被使用一次。用戶通常會使用匿名類型把行爲內聯(inline):express

button.addActionListener(new ActionListener) {
  public void actionPerformed(ActionEvent e) {
    ui.dazzle(e.getModifiers());
  }
}

顯然匿名內部類並非一個好的選擇,其語法過於冗餘,匿名類中的this和變量名容易令人產生誤解,類型載入和實例建立語義不夠靈活,沒法捕獲非final的局部變量,沒法對控制流進行抽象等等等等。編程

函數式接口(Functional interfaces)

因而java8引入了一個函數式接口的概念。理解Functional Interface(函數式接口,如下簡稱FI)是學習Java8 Lambda表達式的關鍵所在,因此放在最開始討論。FI的定義其實很簡單:任何接口,若是隻包含惟一一個抽象方法,那麼它就是一個FI。數據結構

Java8提供了@FunctionalInterface註解。舉個簡單的例子,Runnable接口就是一個FI,下面是它的源代碼:app

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

咱們並不須要額外的工做來聲明一個接口是函數式接口:編譯器會根據接口的結構自行判斷(判斷過程並不是簡單的對接口方法計數:一個接口可能冗餘的定義了一個Object已經提供的方法,好比toString(),或者定義了靜態方法或默認方法,這些都不屬於函數式接口方法的範疇)。不過API做者們能夠經過@FunctionalInterface註解來顯式指定一個接口是函數式接口(以免無心聲明瞭一個符合函數式標準的接口),加上這個註解以後,編譯器就會驗證該接口是否知足函數式接口的要求。編程語言

下面是Java SE 7中已經存在的函數式接口:ide

java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener

Java8除了給Runnable,Comparator等接口打上了@FunctionalInterface註解以外,還預約義了一大批新的FI。這些接口都在java.util.function包裏,例如:函數式編程

Predicate<T>——接收T對象並返回boolean
Consumer<T>——接收T對象,不返回值
Function<T, R>——接收T對象,返回R對象
Supplier<T>——提供T對象(例如工廠),不接收值
UnaryOperator<T>——接收T對象,返回T對象
BinaryOperator<T>——接收兩個T對象,返回T對象

下面簡單介紹幾個:函數

@FunctionalInterface
//Predicate用來判斷一個對象是否知足某種條件,好比,單詞是否由六個以上字母組成:
public interface Predicate<T> {
    boolean test(T t);
}
words.stream().filter(word -> word.length() > 6).count();
@FunctionalInterface
//Function表示接收一個參數,併產生一個結果的函數:
public interface Function<T, R> {
    R apply(T t);
}
//下面的例子將集合裏的每個整數都乘以2:
ints.stream().map(x -> x * 2);
@FunctionalInterface
//Consumer表示對單個參數進行的操做,前面例子中的forEach()方法接收的參數就是這種操做:
public interface Consumer<T> {
    void accept(T t);
}

Lambda表達式(lambda expressions)

爲了可以方便、快捷、幽雅的建立出FI的實例,Java8提供了Lambda表達式這顆語法糖。下面我用一個例子來介紹Lambda語法。假設咱們想對一個List<String>按字符串長度進行排序,那麼在Java8以前,能夠藉助匿名內部類來實現:性能

List<String> words = Arrays.asList("apple", "banana", "pear");
  words.sort(new Comparator<String>() {
    @Override
    public int compare(String w1, String w2) {
        return Integer.compare(w1.length(), w2.length());
    }
  });

上面的匿名內部類簡直能夠用醜陋來形容,惟一的一行邏輯被五行垃圾代碼淹沒。根據前面的定義(並查看Java源代碼)可知,Comparator是個FI,因此,能夠用Lambda表達式來實現:

List<String> words = Arrays.asList("apple", "banana", "pear");
  words.sort((String w1, String w2) -> {
      return Integer.compare(w1.length(), w2.length());
  });

lambda表達式的語法由參數列表、箭頭符號->和函數體組成。函數體既能夠是一個表達式,也能夠是一個語句塊:

表達式:表達式會被執行而後返回執行結果。

語句塊:語句塊中的語句會被依次執行,就像方法中的語句同樣——

return語句會把控制權交給匿名方法的調用者

break和continue只能在循環中使用

若是函數體有返回值,那麼函數體內部的每一條路徑都必須返回值

表達式函數體適合小型lambda表達式,它消除了return關鍵字,使得語法更加簡潔。

下面是一些出如今語句中的lambda表達式:

FileFilter java = (File f) -> f.getName().endsWith("*.java");
String user = doPrivileged(() -> System.getProperty("user.name"));
new Thread(() -> {
  connectToService();
  sendNotification();
}).start();

目標類型(Target typing)

對於給定的lambda表達式,其類型都由上下文推導而出,例如:

ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());  //ActionListener的實例
Callable<String> c = () -> "done";                  //Callable的實例
PrivilegedAction<String> a = () -> "done";              //PrivilegedAction的實例

lambda表達式對目標類型也是有要求的。編譯器會檢查lambda表達式的類型和目標類型的方法簽名(method signature)是否一致。當且僅當下面全部條件均知足時,lambda表達式才能夠被賦給目標類型T:

  • T是一個函數式接口
  • lambda表達式的參數和T的方法參數在數量和類型上一一對應
  • lambda表達式的返回值和T的方法返回值相兼容(Compatible)
  • lambda表達式內所拋出的異常和T的方法throws類型相兼容

而且lambda表達式的參數類型能夠從目標類型中得出

Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

在上面的例子裏,編譯器能夠推導出s1和s2的類型是String。此外,當lambda的參數只有一個並且它的類型能夠被推導得知時,該參數列表外面的括號能夠被省略:

FileFilter java = f -> f.getName().endsWith(".java");

方法引用(Method References)

有時候Lambda表達式的代碼就只是一個簡單的方法調用而已,遇到這種狀況,Lambda表達式還能夠進一步簡化爲 方法引用(Method References) 。一共有四種形式的方法引用,第一種引用 靜態方法 ,例如:

List<Integer> ints = Arrays.asList(1, 2, 3);
ints.sort(Integer::compare);

第二種引用 某個特定對象的實例方法 ,例如前面那個遍歷並打印每個word的例子能夠寫成這樣:

words.forEach(System.out::println);

第三種引用 某個類的實例方法,例如:

words.stream().map(word -> word.length()); // lambda
words.stream().map(String::length); // method reference

第四種引用類的 構造函數 ,例如:

// lambda
words.stream().map(word -> {
    return new StringBuilder(word);
});
// constructor reference
words.stream().map(StringBuilder::new);

總結

咱們在設計lambda時的一個重要目標就是新增的語言特性和庫特性可以無縫結合(designed to work together)。接下來,咱們經過一個實際例子(按照姓對名字列表進行排序)來演示這一點: 好比說下面的代碼:

List<Person> people = ...
Collections.sort(people, new Comparator<Person>() {
  public int compare(Person x, Person y) {
    return x.getLastName().compareTo(y.getLastName());
  }
})

冗餘代碼實在太多了!有了lambda表達式,咱們能夠去掉冗餘的匿名類:

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

儘管代碼簡潔了不少,但它的抽象程度依然不好:開發者仍然須要進行實際的比較操做(並且若是比較的值是原始類型那麼狀況會更糟),因此咱們要藉助Comparator裏的comparing方法實現比較操做:

Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));

在類型推導和靜態導入的幫助下,咱們能夠進一步簡化上面的代碼:

Collections.sort(people, comparing(p -> p.getLastName()));

咱們注意到這裏的lambda表達式其實是getLastName的代理(forwarder),因而咱們能夠用方法引用代替它:

Collections.sort(people, comparing(Person::getLastName));

最後,使用Collections.sort這樣的輔助方法並非一個好主意:它不但使代碼變的冗餘,也沒法爲實現List接口的數據結構提供特定(specialized)的高效實現,並且因爲Collections.sort方法不屬於List接口,用戶在閱讀List接口的文檔時不會察覺在另外的Collections類中還有一個針對List接口的排序(sort())方法。 默認方法能夠有效的解決這個問題,咱們爲List增長默認方法sort(),而後就能夠這樣調用:

people.sort(comparing(Person::getLastName));;

此外,若是咱們爲Comparator接口增長一個默認方法reversed()(產生一個逆序比較器),咱們就能夠很是容易的在前面代碼的基礎上實現降序排序。

people.sort(comparing(Person::getLastName).reversed());;
相關文章
相關標籤/搜索