Ummm... Java8和lambda

Java8引入了與此前徹底不一樣的函數式編程方法,經過Lambda表達式和StreamAPI來爲Java下的函數式編程提供動力。本文是Java8新特性的第一篇,旨在闡釋函數式編程的本義,更在展現Java是如何經過新特性實現函數式編程的。java

最近在讀這本圖靈的新書:Java 8 in Action ,本書做者的目的在於向Java程序員介紹Java8帶來的新特性,鼓勵使用新特性來完成更簡潔有力的Java編程。本系列的文章的主要思路也來源於本書。程序員

到底什麼是函數式編程呢?

函數式編程並非一個新概念,諸如Haskell這樣的學院派編程語言就是以函數式編程爲根基的,JVM平臺上更完全的採用函數式編程思惟的更是以Scala爲表明,所以函數式編程確實不是什麼新概念。
下面來談談命令式編程和函數式編程。
什麼是命令式編程呢?很容易理解,就是一條條的命令明明白白地告訴計算機,計算機依照這些這些明確的命令一步步地執行下去就行了,從彙編到C,這樣的命令式編程語言無非都是在模仿計算機的機器指令的下達,明確地在每一句命令裏面告訴計算機每一步須要怎麼申請內存(對象變量)、怎麼跳轉到下一句命令(流轉),即使後來的爲面向對象編程思惟而生的編程語言,好比Java,也仍然未走出這個範式,在每一個類的對象執行具體的方法時也是按照這種「對象變量-流轉」的模式在運行的。在這個模式下,咱們會常常發現程序編寫可能會常常限於冗長的「非關鍵」語句,大量的無用命令只是爲了照顧語言自己的規則:好比所謂的面向接口編程最終變成了定義了一組一組的interface、interfaceImpl。
函數式編程則試圖從編程範式的高度提升代碼的抽象表達能力。命令式編程語言把「對象變量」和「流轉」看成一等公民,而函數式編程在此基礎上加入了「策略變量」這一新的一等公民。策略是什麼呢?策略就是函數,函數自己是能夠做爲變量進行傳遞的。在以往的編程範式裏,策略要被使用時一般是被調用,因此策略的使用必須經過承載策略的類或對象這樣的對象變量,而函數式編程裏面,咱們能夠直接使用策略對象來隨意傳遞,省去了這些沒必要要的無用命令。
Java8做爲一個新特性版本,在保留原有的Java純面向對象特性以外,在容易理解的範圍內引入了函數式編程方式。編程

引入策略:策略何以做爲變量?

咱們有這樣的一個引入的例子:咱們有一堆顏色和重量不定的蘋果,這些蘋果須要通過咱們的一道程序,這道程序能夠把這堆蘋果中的紅蘋果取出來。怎樣編寫程序來選出紅蘋果呢?app

首先咱們定義蘋果Apple類:dom

public class Apple{
    private String color;
    private Integer weight;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }

    public Apple(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }
}

添加咱們的一堆顏色和重量隨機的蘋果:編程語言

public static void main(String[] args){
        ArrayList<Apple> apples = new ArrayList<>();
        Random weightRandom = new Random();
        Random colorRandom = new Random();
        String[] colors = {"red","green","yellow"};
        for (int i = 0; i < 100; i++) {
            apples.add(new Apple(colors[colorRandom.nextInt(3)],weightRandom.nextInt(200)));
        }
}

純命令式的思路:

若是咱們使用傳統的命令式的編程方法,這個從蘋果堆中篩選紅蘋果的方法會這樣:ide

public static List<Apple> redAppleFilter(List<Apple> apples){
        List<Apple> redApples = new ArrayList<>();
        for (Apple apple:
             apples) {
            if("red".equals(apple.getColor())){
                redApples.add(apple);
            }
        }
        return redApples;
    }
List<Apple> redApples = redAppleFilter(apples);

若是這個時候咱們變動需求了,好比咱們不篩選紅蘋果了,要綠蘋果了,怎麼辦呢?就得再定義一個從蘋果堆中篩選綠蘋果的方法:函數式編程

public static List<Apple> greenAppleFilter(List<Apple> apples){
        List<Apple> greenApples = new ArrayList<>();
        for (Apple apple:
             apples) {
            if("green".equals(apple.getColor())){
                greenApples.add(apple);
            }
        }
        return greenApples;
    }
List<Apple> greenApples = greenAppleFilter(apples);

面向對象的編程方法:

使用爲抽象操做而生的接口:接口只定義抽象的方法,具體的方法實現能夠有不一樣的類來實現。若是把這些操做放到繼承了通常篩選器的不一樣篩選方法的篩選器中去就會有一個典型的面向對象式的解決方案了:函數

interface AppleFilter {
    public List<Apple> filterByRules(List<Apple> apples);
}

class RedAppleFilter implements AppleFilter{

    @Override
    public List<Apple> filterByRules(List<Apple> apples) {
        List<Apple> redApples = new ArrayList<>();
        for (Apple apple:
                apples) {
            if("red".equals(apple.getColor())){
                redApples.add(apple);
            }
        }
        return redApples;
    }
}

class GreenAppleFilter implements AppleFilter{

    @Override
    public List<Apple> filterByRules(List<Apple> apples) {
        List<Apple> greenApples = new ArrayList<>();
        for (Apple apple:
                apples) {
            if("green".equals(apple.getColor())){
                greenApples.add(apple);
            }
        }
        return greenApples;
    }
}

面向對象的抽象層級問題

咱們發現雖然使用了面向對象的編程方法雖然可使得邏輯結構更爲清晰:子類蘋果篩選器實現了通常蘋果篩選器的抽象方法,但仍然會有大量的代碼是出現屢次的。
這就是典型的壞代碼的味道,重複編寫了兩個基本同樣的代碼,因此咱們要怎麼修改才能使得代碼應對變化的需求呢,好比能夠應對篩選其餘顏色的蘋果,不要某些顏色的蘋果,能夠篩選某些重量範圍的蘋果等等,而不是每一個肯定的篩選都須要編寫獨立且基本邏輯相同的代碼呢?工具

咱們來看一下重複的代碼到底是哪些:

List<Apple> greenApples = new ArrayList<>();
   for (Apple apple:
           apples) {
       ... ...
   }
   return greenApples;

不重複的代碼有哪些:

if("green".equals(apple.getColor())){
    
}

其實對於循環列表這部分是對篩選這一邏輯的公用代碼,而真正不一樣的是篩選的具體邏輯:根據紅色篩選、綠色篩選等等。

而形成如今局面的緣由就在於僅僅對大的篩選方法的實現的抽象層級過低了,因此就會編寫太多的代碼,若是篩選的抽象層級定位到篩選策略這一級就會大大提高代碼的抽象能力。

傳遞策略對象

所謂策略的範圍就是咱們上面找到的這個「不重複的代碼」:在這個問題裏面就是什麼樣的蘋果是能夠通過篩選的。因此咱們須要的這個策略就是用於肯定什麼樣的蘋果是能夠被選出來的。咱們定義一個這樣的接口:給一個蘋果用於判斷,在test方法裏對這個蘋果進行檢測,而後給出是否被選出的結果。

interface AppleTester{
    public Boolean test(Apple apple);
}

好比咱們能夠經過實現上述接口,重寫這個test方法使之成爲選擇紅蘋果的方法,而後咱們就能夠獲得一個紅蘋果選擇器:

class RedAppleTester implements AppleTester{
    @Override
    public Boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    }
}

再好比咱們能夠經過實現上述接口,重寫這個test方法使之成爲選擇大蘋果的方法,而後咱們就能夠獲得一個大蘋果選擇器:

class BigAppleTester implements AppleTester{
    @Override
    public Boolean test(Apple apple) {
        return apple.getWeight()>150;
    }
}

有了這個選擇器,咱們就能夠把這個選擇器,亦即咱們上面提到的篩選策略,傳給咱們的篩選器,以此進行相應需求的篩選,只要改變選擇器,就能夠更換篩選策略:

public static List<Apple> filterSomeApple(List<Apple> apples,AppleTester tester){
        ArrayList<Apple> resList = new ArrayList<>();
        for (Apple apple
                : apples) {
            if(tester.test(apple))
                resList.add(apple);
        }
        return resList;
    }
List<Apple> redApples = filterSomeApple(apples,new RedAppleTester());
List<Apple> bigApples = filterSomeApple(apples,new BigAppleTester());

經過使用Java的匿名類來實現選擇器接口,咱們能夠不顯式地定義RedAppleTester,BigAppleTester,而進一步簡潔代碼:

List<Apple> redApples = filterSomeApple(apples, new AppleTester() {
            @Override
            public Boolean test(Apple apple) {
                return "red".equals(apple.getColor());
            }
        });
List<Apple> bigApples = filterSomeApple(apples, new AppleTester() {
            @Override
            public Boolean test(Apple apple) {
                return apple.getWeight()>150;            
            }
        });

因此咱們已經從上面的說明中看到,咱們定義的策略是:一個實現了通常蘋果選擇器接口的抽象方法的特殊蘋果選擇器類的對象,由於是對象,因此固然是能夠在代碼裏做爲參數來傳遞的。這也就是咱們反覆提到的在函數式編程裏的策略傳遞,在原書中叫作「行爲參數化的目的是傳遞代碼」

說到這裏,其實這種函數式編程的解決思路並未出現什麼Java8的新特性,在低版本的Java上便可實現這個過程,由於思路雖然很繞,可是說到底使用的就是簡單的接口實現和方法重寫。實際上呢,藉助Java 8新的特性,咱們能夠更方便地使用語法糖來編寫更簡潔、更易懂的代碼。

Java 8 Lambda簡潔化函數式編程

咱們上面定義的這種單方法接口叫作函數式接口

interface AppleTester{
    public Boolean test(Apple apple);
}

函數式接口的這個方法就是這個函數式接口的函數,這個函數的「參數-返回值」類型描述叫作函數描述符,test函數的描述符是 Apple->Boolean
而lambda表達式實際上是一種語法糖現象,它是對函數實現的簡單表述,好比咱們上文的一個函數實現,即實現了AppleTester接口的RedAppleTester:

class RedAppleTester implements AppleTester{
    @Override
    public Boolean test(Apple apple) {
            return "red".equals(apple.getColor());
    }
}

這個實現類能夠用lambda表達式
(Apple a) -> "red".equals(a.getColor())
或者
(Apple a) -> {return "red".equals(a.getColor());}
來代替。->前是參數列表,後面是表達式或命令。

在有上下文的狀況下,甚至有更簡潔的寫法:
AppleTester tester = a -> "red".equals(a.getColor());
能夠這樣寫的緣由在於編譯器能夠根據上下文來推斷參數類型:AppleTester做爲函數式接口只定義了單一抽象方法:public Boolean test(Apple apple),因此能夠很容易地推斷出其抽象方法實現的參數類型。

若是AppleUtils工具類直接定義了斷定紅蘋果的方法:

class AppleUtils {
    public static Boolean isRedApple(Apple apple) {
        return "red".equals(apple.getColor());
    }
}

咱們會發現isRedApple方法的方法描述符和函數式接口AppleTester定義的單一抽象方法的函數描述符是同樣的:Apple->Boolean,所以咱們能夠採用一種叫作方法引用的語法糖來進一步化簡這個lambda表達式,不須要在lambda表達式中重複寫已經定義過的方法:

AppleTester tester = AppleUtils::isRedApple

方法引用之因此能夠起做用,就是由於這個被引用的方法具備和引用它的函數式接口的函數描述符相同的方法描述符。在實際建立那個實現了抽象方法的匿名類對象時會將被引用的方法體嵌入到這個實現方法中去:

雖然寫起來簡潔了,可是在本質上編譯器會將lambda表達式編譯成一個這樣的實現了接口抽象方法的匿名類的對象。
基於lambda表達式簡潔而強大的表達能力,能夠很容易把上面的這段代碼:

List<Apple> redApples = filterSomeApple(apples, new AppleTester() {
            @Override
            public Boolean test(Apple apple) {
                return "red".equals(apple.getColor());
            }
        });

改寫爲Java8版本的:

List<Apple> redApples = filterSomeApple(apples, AppleUtils::isRedApple);

如你所見,這樣的寫法瞬間將代碼改到Java8前沒法企及的簡潔程度。

Java 8泛型函數式接口

咱們在上文介紹的這個函數式接口:

interface AppleTester{
    public Boolean test(Apple apple);
}

它的做用僅僅是對蘋果進行選擇,經過實現test抽象方法來做出具體的選擇器。
可是其實在咱們的應用環境中,不少需求是泛化的,好比上文中的給一個對象(文中是蘋果)以判斷其是否能知足某些需求,這個場景一經泛化便可被許多場景所使用,可使用泛型來對接口進行泛化:

interface ChooseStrategy<T>{
    public Boolean test(T t);
}

public Boolean test(T t)的函數描述符是T->Boolean,因此只要說知足這個描述符的方法均可以做爲方法引用。

同時咱們須要一個泛化的filter方法:

public static <T> List<T> filter(List<T> ts, ChooseStrategy<T> strategy){
        ArrayList<T> resList = new ArrayList<>();
        for (T t
                : ts) {
            if(strategy.test(t))
                resList.add(t);
        }
        return resList;
    }
List<Apple> redApples = filter(apples,AppleUtils::isRedApple);

除了這種在類型上的泛型來泛化使用定義的函數式接口外,甚至有一些公用的場景Java8 爲咱們定義了一整套的函數式接口API來涵蓋這些使用場景中須要的函數式接口。咱們的編程中甚至不須要本身定義這些函數式接口:

  1. java.util.function.Predicate<T>
    函數描述符:T->boolean

  2. java.util.function.Consumer<T>
    函數描述符:T->void

  3. java.util.function.Function<T,R>
    函數描述符:T->R

  4. java.util.function.Supplier<T>
    函數描述符:()->T

  5. java.util.function.UnaryOperator<T>
    函數描述符:T->T

  6. java.util.function.BinaryOperator<T>
    函數描述符:(T,T)->T

  7. java.util.function.BiPredicate<L,R>
    函數描述符:(L,R)->boolean

  8. java.util.function.BiConsumer<T,U>
    函數描述符:(T,U)->void

  9. java.util.function.BiFunction<T,U,R>
    函數描述符:(T,U)->R

Java8經過接口抽象方法實現、lambda表達式來實現了策略對象的傳遞,使得函數成爲了第一公民,並以此來將函數式編程帶入了Java世界中。有了策略傳遞後,使用具體的策略來完成任務,好比本文中篩選蘋果的filter過程,Java8則依靠StreamAPI來實現,一系列泛化的任務過程定義在這些API中,這也將是本系列文章的後續的關注。

相關文章
相關標籤/搜索