Lambda表達式你會用嗎?

函數式編程

在正式學習Lambda以前,咱們先來了解一下什麼是函數式編程java

咱們先看看什麼是函數。函數是一種最基本的任務,一個大型程序就是一個頂層函數調用若干底層函數,這些被調用的函數又能夠調用其餘函數,即大任務被一層層拆解並執行。因此函數就是面向過程的程序設計的基本單元。git

Java不支持單獨定義函數,但能夠把靜態方法視爲獨立的函數,把實例方法視爲自帶this參數的函數。而函數式編程(請注意多了一個「式」字)——Functional Programming,雖然也能夠歸結到面向過程的程序設計,但其思想更接近數學計算。程序員

咱們首先要搞明白計算機(Computer)和計算(Compute)的概念。在計算機的層次上,CPU執行的是加減乘除的指令代碼,以及各類條件判斷和跳轉指令,因此,彙編語言是最貼近計算機的語言。而計算則指數學意義上的計算,越是抽象的計算,離計算機硬件越遠。對應到編程語言,就是越低級的語言,越貼近計算機,抽象程度低,執行效率高,好比C語言;越高級的語言,越貼近計算,抽象程度高,執行效率低,好比Lisp語言。github

函數式編程就是一種抽象程度很高的編程範式,純粹的函數式編程語言編寫的函數沒有變量,所以,任意一個函數,只要輸入是肯定的,輸出就是肯定的,這種純函數咱們稱之爲沒有反作用。而容許使用變量的程序設計語言,因爲函數內部的變量狀態不肯定,一樣的輸入,可能獲得不一樣的輸出,所以,這種函數是有反作用的。express

函數式編程的一個特色就是,容許把函數自己做爲參數傳入另外一個函數,還容許返回一個函數!編程

函數式編程最先是數學家阿隆佐·邱奇研究的一套函數變換邏輯,又稱Lambda Calculus(λ-Calculus),因此也常常把函數式編程稱爲Lambda計算。markdown

Java平臺從Java 8開始,支持函數式編程。多線程

Lambda初體驗

先從一個例子開始,讓咱們來看一下Lambda能夠用在什麼地方。閉包

例一:建立線程

常見建立線程的方法(JDK1.8之前)app

//JDK1.7經過匿名內部類的方式建立線程
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() { //實現run方法
        System.out.println("Thread Run...");
    }
});

thread.start();

經過匿名內部類的方式建立線程,省去了取名字的煩惱,可是還能不能再簡化一些呢?

JDK1.8 Lambda表達式寫法

Thread thread = new Thread(() -> System.out.println("Thread Run")); //一行搞定

thread.start();

咱們能夠看到Lambda一行代碼就完成了線程的建立,簡直不要太方便。(至於Lambda表達式的語法,咱們下面章節再詳細介紹)

若是你的邏輯不止一行代碼,那麼你還能夠這麼寫

Thread thread = new Thread(() -> {
    System.out.println("Thread Run");
    System.out.println("Hello");
});

thread.start();

{}將代碼塊包裹起來

例二:自定義比較器

咱們先來看一下JDK1.7是如何實現自定義比較器的

List<String> list = Arrays.asList("Hi", "Life", "Hello~", "World");
Collections.sort(list, new Comparator<String>(){// 接口名
    @Override
    public int compare(String s1, String s2){// 方法名
        if(s1 == null)
            return -1;
        if(s2 == null)
            return 1;
        return s1.length()-s2.length();
    }
});

//輸出排序好的List
for (String s : list) {
    System.out.println(s);
}

這裏的sort方法傳入了兩個參數,一個是待排序的list,一個是比較器(排序規則),這裏也是經過匿名內部類的方式實現的比較器。

下面咱們來看一下Lambda表達式如何實現比較器?

List<String> list = Arrays.asList("Hi", "Life", "Hello~", "World");
Collections.sort(list, (s1, s2) ->{// 省略了參數的類型,編譯器會根據上下文信息自動推斷出類型
    if(s1 == null)
        return -1;
    if(s2 == null)
        return 1;
    return s1.length()-s2.length();
});

//輸出排序好的List
for (String s : list) {
    System.out.println(s);
}

咱們能夠看到,Lambda表達式和匿名內部類的做用相同,可是省略了不少代碼,能夠大大加快開發速度

Lambda表達式語法

Lambda 表達式,也可稱爲閉包,它是推進 Java 8 發佈的最重要新特性。Lambda 容許把函數做爲一個方法的參數(函數做爲參數傳遞進方法中)。

使用 Lambda 表達式可使代碼變的更加簡潔緊湊。上一章節咱們已經見識到了Lambda表達式的優勢,那麼Lambda表達式到底該怎麼寫呢?

語法

lambda 表達式的語法格式以下:

(parameters) -> expression   //一行代碼
  或
(parameters) ->{ statements; }  //多行代碼

lambda表達式的重要特徵:

  • 可選類型聲明:不須要聲明參數類型,編譯器能夠統一識別參數值。
  • 可選的參數圓括號:一個參數無需定義圓括號,但多個參數須要定義圓括號。
  • 可選的大括號:若是主體包含了一個語句,就不須要使用大括號。
  • 可選的返回關鍵字:若是主體只有一個表達式返回值則編譯器會自動返回值,大括號須要指定明表達式返回了一個數值。
// 1. 不須要參數,返回值爲 5  
() -> 5  
  
// 2. 接收一個參數(數字類型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2個參數(數字),並返回他們的差值  
(x, y) -> x – y  
  
// 4. 接收2個int型整數,返回他們的和  
(int x, int y) -> x + y  
  
// 5. 接受一個 string 對象,並在控制檯打印,不返回任何值(看起來像是返回void)  
(String s) -> System.out.print(s)

函數接口

上面幾個章節給你們介紹Lambda表達式的基本使用,那麼是否是在任意地方均可以使用Lambda表達式呢?

其實Lambda表達式使用是有限制的。也許你已經想到了,可以使用Lambda的依據是必須有相應的函數接口。(函數接口,是指內部只有一個抽象方法的接口)

自定義函數接口

自定義函數接口很容易,只須要編寫一個只有一個抽象方法的接口便可。

// 自定義函數接口
@FunctionalInterface
public interface PersonInterface<T>{
    void accept(T t);
}

上面代碼中的@FunctionalInterface是可選的,但加上該標註編譯器會幫你檢查接口是否符合函數接口規範。就像加入@Override標註會檢查是否重載了函數同樣。

那麼根據上面的自定義函數式接口,咱們就能夠寫出以下的Lambda表達式。

PersonInterface p = str -> System.out.println(str);

Lambda和匿名內部類

通過上面幾部分的介紹,相信你們對Lambda表達式已經有了初步認識,學會了如何使用。但想必你們心中始終有一個疑問,Lambda表達式彷佛只是爲了簡化匿名內部類的寫法,其餘也沒啥區別了。這看起來僅僅經過語法糖在編譯階段把全部的Lambda表達式替換成匿名內部類就能夠了,事實真的如此嗎?

public class Main {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous class");
            }
        }).start();
    }
}

匿名內部類也是一個類,只不過咱們不須要顯示爲他定義名稱,可是編譯器會自動爲匿名內部類命名。Main編輯後的文件以下圖

image-20201217164035157

咱們能夠看到共有兩個class文件,一個是Main.class,而另外一個則是編輯器爲咱們命名的內部類。

下面咱們來看一下Lambda表達式會產生幾個class文件

public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Lambda")).start();
    }
}

image-20201217164610350

Lambda表達式經過invokedynamic指令實現,書寫Lambda表達式不會產生新的類

Lambda在集合中的運用

既然Lambda表達式這麼方便,那麼哪些地方可使用Lambda表達式呢?

咱們先從最熟悉的Java集合框架(Java Collections Framework, JCF)開始提及。

爲引入Lambda表達式,Java8新增了java.util.funcion包,裏面包含經常使用的函數接口,這是Lambda表達式的基礎,Java集合框架也新增部分接口,以便與Lambda表達式對接。

首先回顧一下Java集合框架的接口繼承結構:

JCF_Collection_Interfaces

上圖中綠色標註的接口類,表示在Java8中加入了新的接口方法,固然因爲繼承關係,他們相應的子類也都會繼承這些新方法。下表詳細列舉了這些方法。

接口名 Java8新加入的方法
Collection removeIf() spliterator() stream() parallelStream() forEach()
List replaceAll() sort()
Map getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()

這些新加入的方法大部分要用到java.util.function包下的接口,這意味着這些方法大部分都跟Lambda表達式相關。咱們將逐一學習這些方法。

Collection中的新方法

如上所示,接口CollectionList新加入了一些方法,咱們以是List的子類ArrayList爲例來講明。瞭解Java7ArrayList實現原理,將有助於理解下文。

forEach()

該方法的簽名爲void forEach(Consumer<? super E> action),做用是對容器中的每一個元素執行action指定的動做,其中Consumer是個函數接口,裏面只有一個待實現方法void accept(T t)(後面咱們會看到,這個方法叫什麼根本不重要,你甚至不須要記憶它的名字)。

需求:假設有一個字符串列表,須要打印出其中全部長度大於3的字符串.

Java7及之前咱們能夠用加強的for循環實現:

// 使用曾強for循環迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list){
    if(str.length()>3)
        System.out.println(str);
}

如今使用forEach()方法結合匿名內部類,能夠這樣實現:

// 使用forEach()結合匿名內部類迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
    @Override
    public void accept(String str){
        if(str.length()>3)
            System.out.println(str);
    }
});

上述代碼調用forEach()方法,並使用匿名內部類實現Comsumer接口。到目前爲止咱們沒看到這種設計有什麼好處,可是不要忘記Lambda表達式,使用Lambda表達式實現以下:

// 使用forEach()結合Lambda表達式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
        if(str.length()>3)
            System.out.println(str);
    });

上述代碼給forEach()方法傳入一個Lambda表達式,咱們不須要知道accept()方法,也不須要知道Consumer接口,類型推導幫咱們作了一切。

removeIf()

該方法簽名爲boolean removeIf(Predicate<? super E> filter),做用是刪除容器中全部知足filter指定條件的元素,其中Predicate是一個函數接口,裏面只有一個待實現方法boolean test(T t),一樣的這個方法的名字根本不重要,由於用的時候不須要書寫這個名字。

需求:假設有一個字符串列表,須要刪除其中全部長度大於3的字符串。

咱們知道若是須要在迭代過程衝對容器進行刪除操做必須使用迭代器,不然會拋出ConcurrentModificationException,因此上述任務傳統的寫法是:

// 使用迭代器刪除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()){
    if(it.next().length()>3) // 刪除長度大於3的元素
        it.remove();
}

如今使用removeIf()方法結合匿名內部類,咱們但是這樣實現:

// 使用removeIf()結合匿名名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){ // 刪除長度大於3的元素
    @Override
    public boolean test(String str){
        return str.length()>3;
    }
});

上述代碼使用removeIf()方法,並使用匿名內部類實現Precicate接口。相信你已經想到用Lambda表達式該怎麼寫了:

// 使用removeIf()結合Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length()>3); // 刪除長度大於3的元素

使用Lambda表達式不須要記憶Predicate接口名,也不須要記憶test()方法名,只須要知道此處須要一個返回布爾類型的Lambda表達式就好了。

replaceAll()

該方法簽名爲void replaceAll(UnaryOperator<E> operator),做用是對每一個元素執行operator指定的操做,並用操做結果來替換原來的元素。其中UnaryOperator是一個函數接口,裏面只有一個待實現函數T apply(T t)

需求:假設有一個字符串列表,將其中全部長度大於3的元素轉換成大寫,其他元素不變。

Java7及以前彷佛沒有優雅的辦法:

// 使用下標實現元素替換
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(int i=0; i<list.size(); i++){
    String str = list.get(i);
    if(str.length()>3)
        list.set(i, str.toUpperCase());
}

使用replaceAll()方法結合匿名內部類能夠實現以下:

// 使用匿名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>(){
    @Override
    public String apply(String str){
        if(str.length()>3)
            return str.toUpperCase();
        return str;
    }
});

上述代碼調用replaceAll()方法,並使用匿名內部類實現UnaryOperator接口。咱們知道能夠用更爲簡潔的Lambda表達式實現:

// 使用Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
    if(str.length()>3)
        return str.toUpperCase();
    return str;
});

sort()

該方法定義在List接口中,方法簽名爲void sort(Comparator<? super E> c),該方法根據c指定的比較規則對容器元素進行排序Comparator接口咱們並不陌生,其中有一個方法int compare(T o1, T o2)須要實現,顯然該接口是個函數接口。

需求:假設有一個字符串列表,按照字符串長度增序對元素排序。

因爲Java7以及以前sort()方法在Collections工具類中,因此代碼要這樣寫:

// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>(){
    @Override
    public int compare(String str1, String str2){
        return str1.length()-str2.length();
    }
});

如今能夠直接使用List.sort()方法,結合Lambda表達式,能夠這樣寫:

// List.sort()方法結合Lambda表達式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());

spliterator()

方法簽名爲Spliterator<E> spliterator(),該方法返回容器的可拆分迭代器。從名字來看該方法跟iterator()方法有點像,咱們知道Iterator是用來迭代容器的,Spliterator也有相似做用,但兩者有以下不一樣:

  1. Spliterator既能夠像Iterator那樣逐個迭代,也能夠批量迭代。批量迭代能夠下降迭代的開銷。
  2. Spliterator是可拆分的,一個Spliterator能夠經過調用Spliterator<T> trySplit()方法來嘗試分紅兩個。一個是this,另外一個是新返回的那個,這兩個迭代器表明的元素沒有重疊。

可經過(屢次)調用Spliterator.trySplit()方法來分解負載,以便多線程處理。

stream()和parallelStream()

stream()parallelStream()分別返回該容器的Stream視圖表示,不一樣之處在於parallelStream()返回並行的StreamStream是Java函數式編程的核心類,咱們會在後面章節中學習。

Map中的新方法

相比CollectionMap中加入了更多的方法,咱們以HashMap爲例來逐一探祕。瞭解[Java7HashMap實現原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet and HashMap.md),將有助於理解下文。

forEach()

該方法簽名爲void forEach(BiConsumer<? super K,? super V> action),做用是Map中的每一個映射執行action指定的操做,其中BiConsumer是一個函數接口,裏面有一個待實現方法void accept(T t, U u)BinConsumer接口名字和accept()方法名字都不重要,請不要記憶他們。

需求:假設有一個數字到對應英文單詞的Map,請輸出Map中的全部映射關係.

Java7以及以前經典的代碼以下:

// Java7以及以前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

使用Map.forEach()方法,結合匿名內部類,代碼以下:

// 使用forEach()結合匿名內部類迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>(){
    @Override
    public void accept(Integer k, String v){
        System.out.println(k + "=" + v);
    }
});

上述代碼調用forEach()方法,並使用匿名內部類實現BiConsumer接口。固然,實際場景中沒人使用匿名內部類寫法,由於有Lambda表達式:

// 使用forEach()結合Lambda表達式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}

getOrDefault()

該方法跟Lambda表達式不要緊,可是頗有用。方法簽名爲V getOrDefault(Object key, V defaultValue),做用是按照給定的key查詢Map中對應的value,若是沒有找到則返回defaultValue。使用該方法程序員能夠省去查詢指定鍵值是否存在的麻煩.

需求;假設有一個數字到對應英文單詞的Map,輸出4對應的英文單詞,若是不存在則輸出NoValue

// 查詢Map中指定的值,不存在時使用默認值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及以前作法
if(map.containsKey(4)){ // 1
    System.out.println(map.get(4));
}else{
    System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2

putIfAbsent()

該方法跟Lambda表達式不要緊,可是頗有用。方法簽名爲V putIfAbsent(K key, V value),做用是隻有在不存在key值的映射或映射值爲null,纔將value指定的值放入到Map中,不然不對Map作更改.該方法將條件判斷和賦值合二爲一,使用起來更加方便.

remove()

咱們都知道Map中有一個remove(Object key)方法,來根據指定key值刪除Map中的映射關係;Java8新增了remove(Object key, Object value)方法,只有在當前Mapkey正好映射到value才刪除該映射,不然什麼也不作.

replace()

在Java7及之前,要想替換Map中的映射關係可經過put(K key, V value)方法實現,該方法老是會用新值替換原來的值.爲了更精確的控制替換行爲,Java8在Map中加入了兩個replace()方法,分別以下:

  • replace(K key, V value),只有在當前Mapkey的映射存在時才用value去替換原來的值,不然什麼也不作.
  • replace(K key, V oldValue, V newValue),只有在當前Mapkey的映射存在且等於oldValue才用newValue去替換原來的值,不然什麼也不作.

replaceAll()

該方法簽名爲replaceAll(BiFunction<? super K,? super V,? extends V> function),做用是對Map中的每一個映射執行function指定的操做,並用function的執行結果替換原來的value,其中BiFunction是一個函數接口,裏面有一個待實現方法R apply(T t, U u).不要被如此多的函數接口嚇到,由於使用的時候根本不須要知道他們的名字.

需求:假設有一個數字到對應英文單詞的Map,請將原來映射關係中的單詞都轉換成大寫.

Java7以及以前經典的代碼以下:

// Java7以及以前替換全部Map中全部映射關係
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    entry.setValue(entry.getValue().toUpperCase());
}

使用replaceAll()方法結合匿名內部類,實現以下:

// 使用replaceAll()結合匿名內部類實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
    @Override
    public String apply(Integer k, String v){
        return v.toUpperCase();
    }
});

上述代碼調用replaceAll()方法,並使用匿名內部類實現BiFunction接口。更進一步的,使用Lambda表達式實現以下:

// 使用replaceAll()結合Lambda表達式實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

簡潔到讓人難以置信.

merge()

該方法簽名爲merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),做用是:

  1. 若是Mapkey對應的映射不存在或者爲null,則將value(不能是null)關聯到key上;
  2. 不然執行remappingFunction,若是執行結果非null則用該結果跟key關聯,不然在Map中刪除key的映射.

參數中BiFunction函數接口前面已經介紹過,裏面有一個待實現方法R apply(T t, U u)

merge()方法雖然語義有些複雜,但該方法的用方式很明確,一個比較常見的場景是將新的錯誤信息拼接到原來的信息上,好比:

map.merge(key, newMsg, (v1, v2) -> v1+v2);

compute()

該方法簽名爲compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),做用是把remappingFunction的計算結果關聯到key上,若是計算結果爲null,則在Map中刪除key的映射.

要實現上述merge()方法中錯誤信息拼接的例子,使用compute()代碼以下:

map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));

computeIfAbsent()

該方法簽名爲V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction),做用是:只有在當前Map不存在key值的映射或映射值爲null,才調用mappingFunction,並在mappingFunction執行結果非null時,將結果跟key關聯.

Function是一個函數接口,裏面有一個待實現方法R apply(T t)

computeIfAbsent()經常使用來對Map的某個key值創建初始化映射.好比咱們要實現一個多值映射,Map的定義多是Map<K,Set<V>>,要向Map中放入新值,可經過以下代碼實現:

Map<Integer, Set<String>> map = new HashMap<>();
// Java7及之前的實現方式
if(map.containsKey(1)){
    map.get(1).add("one");
}else{
    Set<String> valueSet = new HashSet<String>();
    valueSet.add("one");
    map.put(1, valueSet);
}
// Java8的實現方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");

使用computeIfAbsent()將條件判斷和添加操做合二爲一,使代碼更加簡潔.

computeIfPresent()

該方法簽名爲V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),做用跟computeIfAbsent()相反,即,只有在當前Map存在key值的映射且非null,才調用remappingFunction,若是remappingFunction執行結果爲null,則刪除key的映射,不然使用該結果替換key原來的映射.

這個函數的功能跟以下代碼是等效的:

// Java7及之前跟computeIfPresent()等效的代碼
if (map.get(key) != null) {
    V oldValue = map.get(key);
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue != null)
        map.put(key, newValue);
    else
        map.remove(key);
    return newValue;
}
return null;
  1. Java8爲容器新增一些有用的方法,這些方法有些是爲完善原有功能,有些是爲引入函數式編程,學習和使用這些方法有助於咱們寫出更加簡潔有效的代碼.
  2. 函數接口雖然不少,但絕大多數時候咱們根本不須要知道它們的名字,書寫Lambda表達式時類型推斷幫咱們作了一切.

參考:https://github.com/CarpenterLee/JavaLambdaInternals

相關文章
相關標籤/搜索