一、Lambda 表達式基礎

1.0 前言

lambda 表達式是 java 8 中最引人注目的新特性,可是它自身的概念並很差理解,爲了更好的掌握它,咱們必須先了解一下函數式編程。網上關於這方面的介紹有不少,可是大多說的含糊不清。這裏提供一篇我認爲說的最明白的文章,是一篇譯文,若是有疑問還能夠直接對比原文。 傻瓜函數式編程java

1.1 lambda 表達式是什麼?

一個lambda 是一段帶有參數的代碼塊,它能夠被傳遞,所以,它能夠執行一次或屢次。先從一個簡單、經典的比較器例子入手,來感覺一下 lambda 表達式git

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort(new Comparator<String>() {
    @Override public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
});

它改寫成 lambda 表達式的樣子是:github

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort((o1, o2) -> Integer.compare(o1.length(), o2.length()));

1.2 爲何要使用 lambda 表達式?

仔細對比一下上面的例子的兩段代碼,咱們思考幾個問題:編程

  1. 咱們爲何要 new 一個比較器? 由於咱們須要 compara方法的實現,即 Integer.compare(o1.length(), o2.length())。
  2. 爲何不直接把 compara 的實現代碼直接給 sort 方法用? 由於 Java 在以前沒有這種傳遞代碼的機制。咱們只能把複用的代碼封裝到一個類裏。在面向對象的 Java 裏,你不可能將一段代碼傳來傳去,想要向一段代碼中傳遞另外一段代碼你不得不構造一個屬於某個類的對象,由它的某個方法包含所須要的代碼。咱們一直以這種方式達到咱們傳代碼的目的,可是這樣作並很差。

1.3 lambda 表達式語法

簡單抽象一下,基本語法的格式是這個樣子:安全

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

可是上面的例子 (o1, o2) -> Integer.compare(o1.length(), o2.length())) 顯然和語法格式不一致,咱們繼續以這段代碼爲例,詳細說一下 lambda 表達式的語法。 o1, o2 是什麼呢?它們都是字符串,Java 是強類型語言,咱們必須爲每一個變量或參數指明類型,一個正經 lambda 的寫法以下: 若是一個表達式的參數類型是能夠被推導的,那麼能夠忽略它們的類型ide

(o1, o2) -> {
    return Integer.compare(o1.length(), o2.length());
}

若是一個表達式的返回類型是能夠被推導的,那麼它將會從上下文自動推導函數式編程

(o1, o2) -> Integer.compare(o1.length(), o2.length());

也就變成了例子中的樣子。 其它簡化寫法:若是某個表達式不含有參數,你可使用一對空的小括號,跟無參方法同樣;若是隻含有一個參數,且能夠被推導,能夠不寫小括號。函數

() -> System.out.println("無參的 lambda 表達式");
event -> System.out.println("按鈕點擊");

1.4 函數式接口

這部分是理解 Java lambda 的重點 對於只包含一個抽象 方法的接口,你能夠經過 lambda 表達式建立該接口的對象。這種接口被稱爲函數式接口線程

上面已經提到,咱們有傳遞一段代碼的需求。一樣,Java 中不少已有的接口也須要封裝代碼塊,好比 Runnable、Comparator。使用 lambda 表達式實現本應由這些接口實例實現的功能的行爲,即lambda 表達式轉換成一個接口的實例叫函數式接口轉換(ps:我的理解,非標準定義)。 實際上完成函數式接口的轉換是 lambda 表達式在Java 中惟一能作的事。咱們以API 中的幾段代碼來講明什麼是函數式接口轉換:code

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

上面一段代碼是 Arraylist 類中的 sort 方法,從方法簽名中的參數列表看,這個方法須要一個參數:比較器實現。回顧一下咱們最開始的例子,參數傳的是什麼? lambda 表達式對不對?

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort((o1, o2) -> Integer.compare(o1.length(), o2.length()));

即咱們以 lambda 表達式實現了接口實例的功能。是怎麼實現的呢?list.sort 方法會接收一個實現了 Comparator 接口的類的實例,調用該對象的 compare 方法會執行 lambda 表達式中的代碼。

Java API 在 java.util.function 包中定義了一些通用的函數式接口,好比 BiFunction<T, U, R>意思是T,U類型的參數以及R類型的返回值。

BiFunction<String, String, Integer> c = (o1, o2) -> Integer.compare(o1.length(), o2.length());

雖然你可使用 BiFunction 聲明、引用一個表達式,可是並無什麼卵用,由於沒有任何一個方法能夠接收函數式接口做爲參數。

1.5 方法引用

把一個已有的方法傳遞給其餘代碼。 好比說上面一直用的排序例子,咱們不按長度排序了,如今按字符串大小排序,而且忽略大小寫,代碼應該是這樣子的:

List<String> list = new ArrayList<>();
list.sort((String x, String y) -> x.compareToIgnoreCase(y));

其實咱們傳遞給sort 方法的代碼片斷就是 String 類的 compareToIgnoreCase 方法。爲了更優雅的寫代碼,API 簡化這種寫法,即

List<String> list = new ArrayList<>();
list.sort(String::compareToIgnoreCase);

::操做符分隔對象/類名 和 方法名,主要有三種狀況

  • 對象::實例方法
  • 類::靜態方法
  • 類::實例方法 對於前兩種狀況,方法引用就是對參數執行該方法。好比打印 list 元素,
list.forEach(System.out::print);  // 至關於 list.forEach(x -> System.out.println(x));

對於第三種狀況,第一個參數會成爲執行方法的對象,第2個是方法的參數。好比上面的字符串排序 demo。

1.6 構造器引用

構造器引用與方法引用相似,不一樣的是構造器引用引用的方法名是 new。貌似用處不大,瞭解便可。

1.7 變量做用域

簡單地說,lambda 表達式的變量做用域和內部類的變量做用域相似。

static int b = 10;
public static void main(String[] args) {
    int a = 100;
    Runnable r = () -> {
        a++;       // 錯誤,不能更改局部變量的值
        b++;         // 正確,但有線程安全問題
        int a = 1;  // 錯誤,不能與局部變量重名
        int b = 1;  // 正確
    };
    new Thread(r).run();
}

1.8 默認方法

接口能夠有實現方法,並且不須要實現類去實現其方法。只需在方法名前面加個default關鍵字便可。

爲何要定義默認方法? Java 的接口是不可擴展的,一旦你爲某個父接口增長了一個方法,你就必須爲全部的實現類增長這個方法的實現。 由於新增了對 lambda 表達式的支持,如今 Java 有了傳遞代碼塊的能力,因此對集合的處理有了更簡潔、高效的方式,好比,輸出集合元素:

// old
for(String s : list) {
   System.out.println(s);
}
// now
list.forEach(System.out::println);

可是,若是給Collection 或 Iterator 增長forEach 接口,會要求全部的實現類增長 forEach 的實現,顯然這是沒法接受的。因此,爲了解決接口的修改與現有的實現不兼容的問題,引入了默認方法。 在JVM中,默認方法的實現是很是高效的,而且經過字節碼指令爲方法調用提供了支持。 使用規則 若是接口接口定義了一個默認方法,而另外一個父類/接口有定義了一個同名方法,該選擇哪一個? 1.選擇父類中的方法。若是父類中提供了實現,忽略接口中的方法。 2.接口衝突。若是多個父接口中都提供了對同一方法的實現,接口中的默認方法都忽略,實現類必須本身定義實現。

1.9 接口中的靜態方法

Java 8 中,接口中容許添加靜態方法。好比咱們在 章節「5.3 比較器」 中使用到的 Comparator.comparing 方法。使用靜態方法,代碼會更簡潔。咱們再以開頭的那段代碼做爲例子:

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort(new Comparator<String>() {
    @Override public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
});

能夠寫成

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort(Comparator.comparing(String::length));
相關文章
相關標籤/搜索