Java 函數式編程

前些年 Scala 大肆流行,打出來 Java 顛覆者的旗號,究其底氣來源,無非是函數式和面向對象的“完美結合”,各式各樣的“語法糖”,但其過高的學習門檻,又給了新來者當頭一棒。

隨著 Java8 的發(fā)布,Lambda 特性的引入,之前的焦灼局面是否有所轉變,讓我們一起揭開 Java 函數式編程的面紗:

  1. 面向對象 VS 函數式
  2. FunctionalInterface 和 Lambda
  3. 類庫的升級改造(默認方法、靜態(tài)方法、Stream、Optional)
  4. Lambda 下模式的進化
  5. Lambda 下并發(fā)程序

1. 面向對象 VS 函數式編程

一句話總結兩種的關系:面向對象編程是對數據進行抽象;而函數式編程是對行為進行抽象。

在現(xiàn)實世界中,數據和行為并存,程序也應如此,可喜可賀的是在 Java 世界中,兩者也開啟了融合之旅。

首先思考一個問題, 在 Java 編程中,我們如何進行行為傳遞,例如我們需要打印線程名稱和當前時間,并將該任務提交到線程池中運行,會有哪些方法?

方法 1:新建 class Task 實現(xiàn) Runnable 接口

    public class Task implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");
        }
    }
    executorService.submit(new Task());

方法 2:匿名內部類實現(xiàn) Runnable 接口

    executorService.submit(new Runnable() {
            @Override
            public void run() {
              System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");
            }
        });

方法 3:使用 Lambda 表達式

    executorService.submit(()-> System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms"));

方法 4:使用方法引用

     private void print(){
        System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");
    }
    {
        executorService.submit(this::print);
    }

通過上面不同的行為傳遞方式,能夠比較直觀的體會到隨著函數式特性的引入,行為傳遞少了很多樣板代碼,增加了一絲靈活;可見Lambda表達式是一種緊湊的、傳遞行為的方式。

2. FunctionalInterface 和 Lambda

Java 函數式編程,只有兩個核心概念:

FunctionalInterface(函數接口)是只有一個抽象方法的接口,用作 Lambda 表達式的類型。

Lambda 表達式,及要傳遞的行為代碼,更像是一個匿名函數(當然 java 中并沒有這個概念),將行為像數據那樣進行傳遞。

換個好理解但是不正規(guī)的說法,F(xiàn)unctionalInterface 為類型,Lambda 表達式為值;我們可以將一個 Lambda 表達式賦予一個符合 FunctionalInterface 要求的接口變量(局部變量、方法參數)。

2.1. Lambda 表達式

先看幾個 Lambda 表達式的例子:

        // 不包含參數,用()表示沒有參數
        // 表達式主體只有一個語句,可以省略{}
        Runnable helloWord = () -> System.out.println("Hello World");

        // 表達式主體由多個語句組成,不能省略{}
        Runnable helloWords = () -> {
            System.out.println("Hello");
            System.out.println("Word");
            System.out.println("Word");
        };

        // 表達式中只有一個參數,可以省略()
        Consumer<String> infoConsumer = msg -> System.out.println("Hello " + msg);

        // 表達式由多個參數組成,不可省略()
        BinaryOperator<Integer> add1 = (Integer i ,Integer j) -> i + j;

        // 編譯器會進行類型推斷,在沒有歧義情況下可以省略類型聲明,但是不可省略()
        BinaryOperator<Integer> add2 = (i, j) -> i + j;

綜上可見,一個 Lambda 表達式主要由三部分組成:

  1. 參數列表
  2. 箭頭分隔符(->)
  3. 主體,單個表達式或語句塊

我們在使用匿名內部類時有一些限制:引用方法中的變量時,需要將變量聲明為 final,不能為其進行重新賦值,如下:

        final String msg = "World";
        Runnable print = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello"  + msg);
            }
        };

在 Java8 中放松了這個限制,可以引用非 final 變量,但是該變量在既成事實上必須是 final 的,雖然無需將變量聲明為 final,在 Lambda 表達式中,也無法用作非最終態(tài)變量,及只能給該變量賦值一次(與用 final 聲明變量效果相同)。

2.2 FunctionalInterface

FunctionalInterface,只有一個抽象方法的接口就是函數式接口,接口中單一方法命名并不重要,只要方法簽名與 Lambda 表達式的類型匹配即可。

Java 內置了常用函數接口如下:

1. Predicate<T>

參數類型:T
返回值:boolean
示例:Predicate<String> isAdmin = name -> "admin".equals(name);
2. Consumer<T>

參數:T
返回值:void
示例:Consumer<String> print = msg -> System.out.println(msg);
3. Function<T,R>

參數:T
返回值:R
示例:Function<Long, String> toStr = value -> String.valueOf(value);
4. Supplier<T>

參數:none
返回值:T
示例:Supplier<Date> now = () -> new Date();
5. UnaryOperator<T>

參數:T
返回值:T
示例:UnaryOperator<Boolean> negation = value -> !value.booleanValue();
6. BinaryOperator<T>

參數:(T, T)
返回值:T
示例:BinaryOperator<Integer> intDouble = (i, j) -> i + j;
7. Runnable

參數:none
返回值:void
示例:Runnable helloWord = () -> System.out.println("Hello World");
8. Callable<T>

參數:nont
返回值:T
示例:Callable<Date> now1 = () -> new Date();

當然我們也可以根據需求自定義函數接口,為了保證接口的有效性,可以在上面添加 @FunctionalInterface 注解,該注解會強制 javac 檢測一個接口是否符合函數式接口的規(guī)范,例如:

    @FunctionalInterface
    interface CustomFunctionalInterface{
        void print(String msg);
    }
    CustomFunctionalInterface cfi= msg -> System.out.println(msg);

2.3 方法引用

Lambda 表達式一種常用方法便是直接調用其他方法,針對這種情況,Java8 提供了一個簡寫語法,及方法引用,用于重用已有方法。

凡是可以使用 Lambda 表達式的地方,都可以使用方法引用。

方法應用的標準語法為 ClassName::methodName,雖然這是一個方法,但不需要再后面加括號,因為這里并不直接調用該方法。

    Function<User, String> f1 = user->user.getName();
    Function<User, String> f2 = User::getName;

    Supplier<User> s1 = ()->new User();
    Supplier<User> s2 = User::new;

    Function<Integer, User[]> sa1 = count -> new User[count];
    Function<Integer, User[]> sa2 = User[]::new;

方法引用主要分為如下幾種類型:

  • 靜態(tài)方法引用:className::methodName
  • 實例方法引用:instanceName::methodName
  • 超類實體方法引用:supper::mehtodName
  • 構造函數方法引用:className::new
  • 數組構造方法引用:ClassName[]::new

2.4 類型推斷

類型推斷,是 Java7 就引入的目標類型推斷的擴展,在 Java8 中對其進行了改善,程序員可以省略 Lambda 表達式中的所有參數類型,Javac 會根據 Lambda 表達式式上下文信息自動推斷出參數的正確類型。

大多數情況下 javac 能夠準確的完成類型推斷,但由于 Lambda 表達式與函數名無關,只與方法簽名相關,因此會出現(xiàn)類型對推斷失效的情況,這時可以使用手工類型轉換幫助 javac 進行正確的判斷。

    // Supplier<String>, Callable<String> 具有相同的方法簽名
    private void print(Supplier<String> stringSupplier){
        System.out.println("Hello " + stringSupplier.get());
    }

    private void print(Callable<String> stringCallable){
        try {
            System.out.println("Hello " + stringCallable.call());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    {
        // Error, 因為兩個print同時滿足需求
        print(()->"World");
        // 使用類型轉換,為編譯器提供更多信息
        print((Supplier<String>) ()->"World");

        print((Callable<String>) ()-> "world");

    }

3. 類庫的升級改造

Java8 另一個變化是引入了 默認方法 和接口的 靜態(tài)方法 ,自此以后 Java 接口中方法也可以包含代碼體了。

3.1 默認方法

默認方法允許接口方法定義默認實現(xiàn),而所有子類都將擁有該方法及實現(xiàn)。使其能夠在不改變子類實現(xiàn)的情況下(很多時候我們無法拿到子類的源碼),為所有子類添加新的功能,從而最大限度的保證二進制接口的兼容性。

默認方法的另一個優(yōu)勢是該方法是可選的,子類可以根據不同的需求 Override 默認實現(xiàn),為其提供擴展性保證。

其中 Collection 中的 forEach,stream 功能都是通過該技術統(tǒng)一添加到接口中的。

    // Collection 中的forEache實現(xiàn)
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    // Collection中的stream實現(xiàn)
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

從上可見,默認方法的寫法也是比較簡單的,只需在方法聲明中添加 defalut 關鍵字,然后提供方法的默認實現(xiàn)即可。

和類不同,接口中沒有成員變量,因此默認方法只能通過調用子類的方法來修改子類本身,避免了對子類的實現(xiàn)做出各種假設。

3.1.1 默認方法與子類

添加默認方法特性后,方法的重寫規(guī)則也發(fā)生了變化,具體的場景如下:

a. 沒有重寫

沒有重寫是最簡單的情況,子類調用該方法的時候,自然繼承了默認方法。

    interface Parent{
        default void welcome(){
            System.out.println("Parent");
        }
    }

    // 調用Parent中的welcome, 輸入"Parent"
    class ParentNotImpl implements Parent{

    }

b. 子接口重寫

子接口對父接口中的默認方法進行了重新,其子類方法被調用時,執(zhí)行子接口中的默認方法

     interface Parent{
        default void welcome(){
            System.out.println("Parent");
        }
    }


    interface ChildInterface extends Parent{
        @Override
        default void welcome(){
            System.out.println("ChildInterface");
        }
    }

    // 執(zhí)行ChildInterface中的welcome, 輸入 "ChildInterface"
    class ChildImpl implements ChildInterface{

    }

c. 類重寫

一旦類中重寫了默認方法,優(yōu)先選擇類中定義的方法,如果存在多級類繼承,遵循類繼承邏輯。

     interface Parent{
        default void welcome(){
            System.out.println("Parent");
        }
    }


    interface ChildInterface extends Parent{
        @Override
        default void welcome(){
            System.out.println("ChildInterface");
        }
    }

    //執(zhí)行子類中的welcome方法,輸出"ChildImpl"
    class ChildImpl1 implements ChildInterface{
        @Override
        public void welcome(){
            System.out.println("ChildImpl");
        }
    }
3.1.2 多重繼承

接口允許多重繼承,因此有可能會碰到兩個接口包含簽名相同的默認方法的情況,此時 javac 并不明確應該繼承哪個接口中的方法,因此會導致編譯出錯,這時需要在類中實現(xiàn)該方法,如果想調用特定父接口中的默認方法,可以使用 ParentInterface.super.method() 的方式來指明具體的接口。

    interface Parent1 {
        default void print(){
            System.out.println("parent1");
        }
    }

    interface Parent2{
        default void print(){
            System.out.println("parent2");
        }
    }

    class Child implements Parent1, Parent2{
        @Override
        public void print() {
            System.out.println("self");
            Parent1.super.print();
            Parent2.super.print();
        }
    }

現(xiàn)在的接口提供了某種形式上的多繼承功能,然而多重繼承存在很多詬病。很多人認為多重繼承的問題在于對象狀態(tài)的繼承,而不是代碼塊的繼承,默認方法避免了狀態(tài)的繼承,也因此避免了 C++ 中多重繼承最大的缺點。

接口和抽象類之間還是有明顯的區(qū)別。接口允許多重繼承,卻沒有成員變量;抽象類可以繼承成員變量,卻不能多重繼承。

從某種角度出發(fā),Java 通過接口默認方法實現(xiàn)了代碼多重繼承,通過類實現(xiàn)了狀態(tài)單一繼承。

3.1.3 三定律

如果對默認方法的工作原理,特別是在多重繼承下的行為沒有把握,可以通過下面三條簡單定律幫助大家。

  1. 類勝于方法。
    如果在繼承鏈中有方法體或抽象的方法聲明,那么就可以忽略接口中定義的方法。
  2. 子類勝于父類。
    如果一個接口繼承另一個接口,且兩個接口都定義了一個默認方法,那么子接口中定義的方法勝出。
  3. 沒有規(guī)則三。
    如果上面兩條規(guī)則不適用,子類要么實現(xiàn)該方法,要么將該方法聲明為抽象方法。

3.2 接口靜態(tài)方法

人們在編程過程中積累了這樣一條經驗,創(chuàng)建一個包含很多靜態(tài)方法的一個類。很多時候類是一個放置工具方法的好地方,比如 Java7 引入的 Objects 類,就包含很多工具方法,這些方法不是屬于具體的某個類。

如果一個方法有充分的語義原因和某個概念相關,那么就應該講該方法和相關的類或接口放在一起,而不是放到另一個工具類中,這非常有助于更好的組織代碼。

在接口中定義靜態(tài)方法,只需使用 static 關鍵字進行描述即可,例如 Stream 接口中的 of 方法。

    /**
     * Returns a sequential {@code Stream} containing a single element.
     *
     * @param t the single element
     * @param <T> the type of stream elements
     * @return a singleton sequential stream
     */
    public static<T> Stream<T> of(T t) {
        return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
    }

3.3 Stream

Stream 是 Java8 中最耀眼的亮點,它使得程序員得以站在更高的抽象層次對集合進行操作。

Stream 是用函數式編程方式在集合類上進行復雜操作的工具。

3.3.1. 從外部迭代到內部迭代

Java 程序員使用集合時,一個通用模式就是在集合上進行迭代,然后處理返回的每一個元素,盡管這種操作可行但存在幾個問題:

  • 大量的樣板代碼
  • 模糊了程序本意
  • 串行化執(zhí)行

常見集合遍歷如下:

     // 常見寫法1,不推薦使用
    public void printAll1(List<String> msg){
        for (int i=0; i< msg.size(); i++){
            String m = msg.get(i);
            System.out.println(m);
        }
    }

    // Java5之前,正確寫法,過于繁瑣
    public void printAll2(List<String> msg){
        Iterator<String> iterator = msg.iterator();
        while (iterator.hasNext()){
            String m = iterator.next();
            System.out.println(m);
        }
    }

    // Java5之后,加強for循環(huán),采用語法糖,簡化for循環(huán),內部轉化為Iterator方式
    public void printAll3(List<String> msg){
        for (String m : msg){
            System.out.println(m);
        }
    }

整個迭代過程,通過顯示的調用 Iterator 對象的 hasNext 和 next 方法完成整個迭代,這成為外部迭代。

外部迭代

另一種方式成為內部迭代,及將操作行為作為參數傳遞給 Stream,在 Stream 內部完成迭代操作。

     // Java8中,使用Stream進行內部迭代操作
    public void printAll4(List<String> msg){
        msg.stream().forEach(System.out::println);
    }

內部迭代:

內部迭代
3.3.2. 惰性求值 VS 及早求值

Stream 中存在兩類方法,不產生值的方法稱為惰性方法;從 Stream 中產生值的方法叫做及早求值方法。

判斷一個方法的類別很簡單:如果返回值是 Stream,那么就是惰性方法;如果返回值是另一個值或為空,那么就是及早求值方法。

惰性方法返回的 Stream 對象不是一個新的集合,而是創(chuàng)建新集合的配方,Stream 本身不會做任何迭代操作,只有調用及早求值方法時,才會開始真正的迭代。

整個過程與 Builder 模式有共通之處,惰性方法負責對 Stream 進行裝配(設置 builder 的屬性),調用及早求值方法時(調用 builder 的 build 方法),按照之前的裝配信息進行迭代操作。

常見 Stream 操作:

3.3.2.1 collect(toList())

及早求值方法:

collect(toList()) 方法由 Stream 里面的值生成一個列表,是一個及早求值操作。

collect 的功能不僅限于此,它是一個非常強大的結構。

     @Data
    class User{
        private String name;
    }
    public List<String> getNames(List<User> users){
        List<String> names = new ArrayList<>();
        for (User user : users){
            names.add(user.getName());
        }
        return names;
    }

    public List<String> getNamesUseStream(List<User> users){
      // 方法引用
      //return users.stream().map(User::getName).collect(toList());
        // lambda表達式
        return users.stream().map(user -> user.getName()).collect(toList());
    }

3.3.2.2. count、max、min

及早求值方法:

Stream 上最常用的操作之一就是求總數、最大值和最小值,count、max 和 min 足以解決問題。

    public Long getCount(List<User> users){
        return users.stream().filter(user -> user != null).count();
    }
    // 求最小年齡
    public Integer getMinAge(List<User> users){
        return users.stream().map(user -> user.getAge()).min(Integer::compareTo).get();
    }

    // 求最大年齡
    public Integer getMaxAge(List<User> users){
        return users.stream().map(user -> user.getAge()).max(Integer::compareTo).get();
    }

min 和 max 入參是一個 Comparator 對象,用于元素之間的比較,返回值是一個 Optional<T>,它代表一個可能不存在的值,如果 Stream 為空,那么該值不存在,如果不為空,該值存在。通過 get 方法可以獲取 Optional 中的值。

3.3.2.3 findAny、findFirst

及早求值方法:

兩個函數都以Optional為返回值,用于表示是否找到。

    public Optional<User> getAnyActiveUser(List<User> users){
        return users.stream()
                .filter(user -> user.isActive())
                .findAny();
    }

    public Optional<User> getFirstActiveUser(List<User> users){
        return users.stream()
                .filter(user -> user.isActive())
                .findFirst();
    }

3.3.2.4 allMatch、anyMatch、noneMatch

及早求值方法:

均以 Predicate 作為輸入參數,對集合中的元素進行判斷,并返回最終的結果。

    // 所有用戶是否都已激活
    boolean allMatch = users.stream().allMatch(user -> user.isActive());
    // 是否有激活用戶
    boolean anyMatch = users.stream().anyMatch(user -> user.isActive());
    // 是否所有用戶都沒有激活
    boolean noneMatch = users.stream().noneMatch(user -> user.isActive());

3.3.2.6. forEach

及早求值:

以 Consumer 為參數,對 Stream 中復合條件的對象進行操作。

    public void printActiveName(List<User> users){
        users.stream()
                .filter(user -> user.isActive())
                .map(user -> user.getName())
                .forEach(name -> System.out.println(name));
    }

3.3.2.7 reduce

及早求值方法:

reduce 操作可以實現(xiàn)從一組值中生成一個值,之前提到的 count、min、max 方法因為比較通用,單獨提取成方法,事實上,這些方法都是通過 reduce 完成的。

下圖展示的是對 stream 進行求和的過程,以 0 為起點,每一步都將 stream 中的元素累加到 accumulator 中,遍歷至最后一個元素,accumulator 就是所有元素值的和。

Stream求和過程

3.3.2.8. filter

惰性求值方法:

以 Predicate 作為參數(相當于 if 語句),對 Stream 中的元素進行過濾,只有復合條件的元素才能進入下面的處理流程。

處理流程如下:

Stream Filter操作
    public List<User> getActiveUser(List<User> users){
        return users.stream()
                .filter(user -> user.isActive())
                .collect(toList());
    }

3.3.2.9 map

及早求值方法:
以 Function 作為參數,將 Stream 中的元素從一種類型轉換成另外一種類型。

處理過程如下:

Stream Map
    public List<String> getNames(List<User> users){
        return users.stream()
                .map(user -> user.getName())
                .collect(toList());
    }

3.3.2.10 peek

Stream 提供的是內迭代,有時候為了功能調試,需要查看每個值,同時能夠繼續(xù)操作流,這時就會用到 peek 方法。

    public void printActiveName(List<User> users){
        users.stream()
                .filter(user -> user.isActive())
                .peek(user -> System.out.println(user.isActive()))
                .map(user -> user.getName())
                .forEach(name -> System.out.println(name));
    }

3.3.2.11 其他

針對集合 Stream 還提供了許多功能強大的操作,暫不一一列舉,簡單匯總一下。

  • distinct:進行去重操作
  • sorted:進行排序操作
  • limit:限定結果輸出數量
  • skip:跳過 n 個結果,從 n+1 開始輸出

3.4 Optional

Java 程序中出現(xiàn)最多的異常就是 NullPointerException,沒有之一。Optional 的出現(xiàn)力求改變這一狀態(tài)。

Optional 對象相當于值的容器,而該值可以通過 get 方法獲取,同時 Optional 提供了很多函數用于對值進行操作,從而最大限度的避免 NullPointerException 的出現(xiàn)。

Optional 與 Stream 的用法基本類型,所提供的方法同樣分為惰性和及早求值兩類,惰性方法主要用于流程組裝,及早求值用于最終計算。

3.4.1 of

使用工廠方法 of,可以從一個值中創(chuàng)建一個 Optional 對象,如果值為 null,會報 NullPointerException。

    Optional<String> dataOptional = Optional.of("a");
    String data = dataOptional.get(); // data is "a"

    Optional<String> dataOptional = Optional.of(null);
    String data = dataOptional.get(); // throw NullPointerException
3.4.2 empty

工廠方法 empty,可以創(chuàng)建一個不包含任何值的 Optional 對象。

    Optional<String> dataOptional = Optional.empty();
    String data = dataOptional.get(); //throw NoSuchElementException
3.4.3 ofNullable

工廠方法 ofNullable,可將一個空值轉化成 Optional。

     public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }
3.4.4 get、orElse、orElseGet、orElseThrow

直接求值方法,用于獲取 Optional 中值,避免空指針異常的出現(xiàn)。

    Optional<String> dataOptional = Optional.of("a");
    dataOptional.get(); // 獲取Optional中的值, 不存在會拋出NoSuchElementException
    dataOptional.orElse("b"); //獲取Optional中的值,不存在,直接返回"B"
    dataOptional.orElseGet(()-> String.valueOf(System.currentTimeMillis())); //獲取Optional中的值,不存在,對Supplier進行計算,并返回計算結果
    dataOptional.orElseThrow(()-> new XXXException()); //獲取Optional中的值,不存在,拋出自定義異常
3.4.5 isPresent、ifPresent

直接求值方法,isPresent 用于判斷 Optional 中是否有值,ifPresent 接收 Consumer 對象,當 Optional 有值的情況下執(zhí)行。

    Optional<String> dataOptional = Optional.of("a");
    String value = null;
    if (dataOptional.isPresent()){
        value = dataOptional.get();
    }else {
        value = "";
    }
    //等價于
    String value2 = dataOptional.orElse("");
    // 當Optional中有值的時候執(zhí)行
    dataOptional.ifPresent(v->System.out.println(v));
3.4.6 map

惰性求值方法。map 與 Stream 中的用法基本相同,用于對 Optional 中的值進行映射處理,從而避免了大量 if 語句嵌套,多個 map 組合成鏈,只需對最終的結果進行操作,中間過程中如果存在 null 值,之后的 map 不會執(zhí)行。

    @Data
    static class Order{
        private Name owner;
    }

    @Data
    static class User{
        private Name name;
    }

    @Data
    static class Name{
        String firstName;
        String midName;
        String lastName;
    }
    private String getFirstName(Order order){
        if (order == null){
            return "";
        }
        if (order.getOwner() == null){
            return "";
        }
        if (order.getOwner().getFirstName() == null){
            return "";
        }
        return order.getOwner().getFirstName();
    }
    private String getFirstName(Optional<Order> orderOptional){
        return orderOptional.map(order -> order.getOwner())
                .map(user->user.getFirstName())
                .orElse("");
    }
3.4.7 filter

惰性求值,對 Optional 中的值進行過濾,如果 Optional 為 empty,直接返回 empty;如果 Optional 中存在值,則對值進行驗證,驗證通過返回原 Optional,驗證不通過返回 empty。

     public Optional<T> filter(Predicate<? super T> predicate) {
        Objects.requireNonNull(predicate);
        if (!isPresent())
            return this;
        else
            return predicate.test(value) ? this : empty();
    }

4. Lambda 下模式的進化

設計模式是人們熟悉的一種設計思路,他是軟件架構中解決通用問題的模板,將解決特定問題的最佳實踐固定下來,但設計模式本身會比較復雜,包含多個接口、若干個實現(xiàn)類,應用過程相對繁瑣,這也是影響其應用的原因之一。

Lambda 表達式大大簡化了 Java 中行為傳遞的問題,對于很多行為式設計模式而言,減少了不少構建成本。

4.1 命令模式

命令者是一個對象,其封裝了調用另一個方法的實現(xiàn)細節(jié),命令者模式使用該對象可以編寫根據運行時條件,順序調用方法的一般性代碼。

大多數命令模式中的命令對象,其實是一種行為的封裝,甚至是對其他對象內部行為的一種適配,這種情況下,Lambda 表達式并有了用武之地。

    interface Command{
        void act();
    }

    interface Editor{
        void open();
        void write(String data);
        void save();
    }

    class CommandRunner{
        private List<Command> commands = new ArrayList<>();

        public void run(Command command){
            command.act();
            this.commands.add(command);
        }

        public void redo(){
            this.commands.forEach(Command::act);
        }
    }

    class OpenCommand implements Command{
        private final Editor editor;

        OpenCommand(Editor editor) {
            this.editor = editor;
        }

        @Override
        public void act() {
            this.editor.open();
        }
    }

    class WriteCommand implements Command{
        private final Editor editor;
        private final String data;
        WriteCommand(Editor editor, String data) {
            this.editor = editor;
            this.data = data;
        }

        @Override
        public void act() {
            editor.write(this.data);
        }
    }

    class SaveCommand implements Command{
        private final Editor editor;

        SaveCommand(Editor editor) {
            this.editor = editor;
        }

        @Override
        public void act() {
            this.editor.save();
        }
    }

    public void useCommand(){
        CommandRunner commandRunner = new CommandRunner();
        Editor editor = new EditorImpl();
        String data1 = "data1";
        String data2 = "data2";
        commandRunner.run(new OpenCommand(editor));
        commandRunner.run(new WriteCommand(editor, data1));
        commandRunner.run(new WriteCommand(editor, data2));
        commandRunner.run(new SaveCommand(editor));
    }

    public void useLambda(){
        CommandRunner commandRunner = new CommandRunner();
        Editor editor = new EditorImpl();
        String data1 = "data1";
        String data2 = "data2";
        commandRunner.run(()->editor.open());
        commandRunner.run(()->editor.write(data1));
        commandRunner.run(()->editor.write(data2));
        commandRunner.run(()->editor.save());
    }



    class EditorImpl implements Editor{

        @Override
        public void open() {

        }

        @Override
        public void write(String data) {

        }

        @Override
        public void save() {

        }
    }

從代碼中可見,Lambda 表達式的應用,減少了創(chuàng)建子類的負擔,增加了代碼的靈活性。

4.2 策略模式

策略模式能夠在運行時改變軟件的算法行為,其核心的實現(xiàn)思路是,使用不同的算法來解決同一個問題,然后將這些算法封裝在一個統(tǒng)一的接口背后。

可見策略模式也是一種行為行為傳遞的模式。

策略模式下的壓縮算法
    interface CompressionStrategy{
        OutputStream compress(OutputStream outputStream) throws IOException;
    }

    class GzipBasedCompressionStrategy implements CompressionStrategy{

        @Override
        public OutputStream compress(OutputStream outputStream) throws IOException {
            return new GZIPOutputStream(outputStream);
        }
    }

    class ZipBasedCompressionStrategy implements CompressionStrategy{

        @Override
        public OutputStream compress(OutputStream outputStream) throws IOException {
            return new ZipOutputStream(outputStream);
        }
    }

    class Compressor{
        private final CompressionStrategy compressionStrategy;

        Compressor(CompressionStrategy compressionStrategy) {
            this.compressionStrategy = compressionStrategy;
        }

        public void compress(Path inFile, File outFile) throws IOException {
            try (OutputStream outputStream = new FileOutputStream(outFile)){
                Files.copy(inFile, this.compressionStrategy.compress(outputStream));
            }
        }
    }

    {
        Compressor gzipCompressor = new Compressor(new GzipBasedCompressionStrategy());
        gzipCompressor.compress(in,out);

        Compressor ziCompressor = new Compressor(new ZipBasedCompressionStrategy());
        ziCompressor.compress(in,out);
    }

    {
        Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);
        gzipCompressor.compress(in,out);

        Compressor ziCompressor = new Compressor(ZipOutputStream::new);
        ziCompressor.compress(in,out);
    }

4.3 觀察者模式

觀察者模式中,被觀察者持有觀察者的一個列表,當被觀察者的狀態(tài)發(fā)送變化時,會通知觀察者。

對于一個觀察者來說,往往是對一個行為的封裝。

    interface NameObserver{
        void onNameChange(String oName, String nName);
    }

    @Data
    class User {
        private final List<NameObserver> nameObservers = new ArrayList<>();
        @Setter(AccessLevel.PRIVATE)
        private String name;

        public void updateName(String nName){
            String oName = getName();
            setName(nName);
            nameObservers.forEach(nameObserver -> nameObserver.onNameChange(oName, nName));
        }

        public void addObserver(NameObserver nameObserver){
            this.nameObservers.add(nameObserver);
        }
    }

    class LoggerNameObserver implements NameObserver{

        @Override
        public void onNameChange(String oName, String nName) {
            System.out.println(String.format("old Name is %s, new Name is %s", oName, nName));
        }
    }

    class NameChangeNoticeObserver implements NameObserver{

        @Override
        public void onNameChange(String oName, String nName) {
            notic.send(String.format("old Name is %s, new Name is %s", oName, nName));
        }
    }

    {
        User user = new User();
        user.addObserver(new LoggerNameObserver());
        user.addObserver(new NameChangeNoticeObserver());
        user.updateName("張三");
    }

    {
        User user = new User();
        user.addObserver((oName, nName) ->
                System.out.println(String.format("old Name is %s, new Name is %s", oName, nName)));
        user.addObserver((oName, nName) ->
                notic.send(String.format("old Name is %s, new Name is %s", oName, nName)));
        user.updateName("張三");
    }

4.4 模板方法模式

模板方法將整體算法設計成一個抽象類,他有一系列的抽象方法,代表方法中可被定制的步驟,同時這個類中包含一些通用代碼,算法的每一個變種都由具體的類實現(xiàn),他們重新抽象方法,提供相應的實現(xiàn)。

模板方法,實際是行為的一種整合,內部大量用到行為的傳遞。
先看一個標準的模板方法:

    interface UserChecker{
        void check(User user);
    }

    abstract class AbstractUserChecker implements UserChecker{
        @Override
        public final void check(User user){
            checkName(user);
            checkAge(user);
        }
        abstract void checkName(User user);

        abstract void checkAge(User user);
    }

    class SimpleUserChecker extends AbstractUserChecker {

        @Override
        void checkName(User user) {
            Preconditions.checkArgument(StringUtils.isNotEmpty(user.getName()));
        }

        @Override
        void checkAge(User user) {
            Preconditions.checkArgument(user.getAge() != null);
            Preconditions.checkArgument(user.getAge().intValue() > 0);
            Preconditions.checkArgument(user.getAge().intValue() < 150);
        }
    }

    {
        UserChecker userChecker = new SimpleUserChecker();
        userChecker.check(new User());
    }

    class LambdaBaseUserChecker implements UserChecker{
        private final List<Consumer<User>> userCheckers = Lists.newArrayList();
        public LambdaBaseUserChecker(List<Consumer<User>>userCheckers){
            this.userCheckers.addAll(userCheckers);
        }

        @Override
        public void check(User user){
            this.userCheckers.forEach(userConsumer -> userConsumer.accept(user));
        }
    }

    {
        UserChecker userChecker = new LambdaBaseUserChecker(Arrays.asList(
                user -> Preconditions.checkArgument(StringUtils.isNotEmpty(user.getName())),
                user -> Preconditions.checkArgument(user.getAge() != null),
                user -> Preconditions.checkArgument(user.getAge().intValue() > 0),
                user -> Preconditions.checkArgument(user.getAge().intValue() < 150)
        ));

        userChecker.check(new User());
    }

    @Data
    class User{
        private String name;
        private Integer age;
    }

在看一個 Spring JdbcTemplate,如果使用 Lambda 進行簡化:

    public JdbcTemplate jdbcTemplate;

    public User getUserById(Integer id){
        return jdbcTemplate.query("select id, name, age from tb_user where id = ?", new PreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement preparedStatement) throws SQLException {
                preparedStatement.setInt(1, id);
            }
        }, new ResultSetExtractor<User>() {
            @Override
            public User extractData(ResultSet resultSet) throws SQLException, DataAccessException {
                User user = new User();
                user.setId(resultSet.getInt("id"));
                user.setName(resultSet.getString("name"));
                user.setAge(resultSet.getInt("age"));
                return user;
            }
        });
    }

    public User getUserByIdLambda(Integer id){
        return jdbcTemplate.query("select id, name, age from tb_user where id = ?",
                preparedStatement -> preparedStatement.setInt(1, id),
                resultSet -> {
                    User user = new User();
                    user.setId(resultSet.getInt("id"));
                    user.setName(resultSet.getString("name"));
                    user.setAge(resultSet.getInt("age"));
                    return user;
                });
    }

    @Data
    class User {
        private Integer id;
        private String name;
        private Integer age;
    }

5. Lambda 下并發(fā)程序

并發(fā)與并行:

  • 并發(fā)是兩個任務共享時間段,并行是兩個任務同一時間發(fā)生。
  • 并行化是指為了縮短任務執(zhí)行的時間,將任務分解為幾個部分,然后并行執(zhí)行,這和順序執(zhí)行的工作量是一樣的,區(qū)別是多個 CPU 一起來干活,花費的時間自然減少了。
  • 數據并行化。數據并行化是指將數據分為塊,為每塊數據分配獨立的處理單元。
并行和并發(fā)的區(qū)別

5.1 并行化流操作

并行化流操作是 Stream 提供的一個特性,只需改變一個方法調用,就可以讓其擁有并行操作的能力。

如果已經存在一個 Stream 對象,調用他的 parallel 方法就能讓其并行執(zhí)行。

如果已經存在一個集合,調用 parallelStream 方法就能獲取一個擁有并行執(zhí)行能力的 Stream。

并行流主要解決如何高效使用多核 CPU 的事情。

    @Data
    class Account{
        private String name;
        private boolean active;
        private Integer amount;
    }

    public int getActiveAmount(List<Account> accounts){
        return accounts.parallelStream()
                .filter(account -> account.isActive())
                .mapToInt(account -> account.getAmount())
                .sum();
    }

    public int getActiveAmount2(List<Account> accounts){
        return accounts.stream()
                .parallel()
                .filter(account -> account.isActive())
                .mapToInt(Account::getAmount)
                .sum();
    }

并行流底層使用 fork/join 框架,fork 遞歸式的分解問題,然后每個段并行執(zhí)行,最終有 join 合并結果,返回最后的值。

Fork/Join分解合并問題

5.2 阻塞 IO VS 非阻塞 IO

BIO VS NIO

BIO 阻塞式 IO,是一種通用且容易理解的方式,與程序交互時通常都符合這種順序執(zhí)行的方式,但其主要的缺陷在于每個 socket 會綁定一個 Thread 進行操作,當長鏈過多時會消耗大量的 Server 資源,從而導致其擴展性性下降。

NIO 非阻塞 IO,一般指的是 IO 多路復用,可以使用一個線程同時對多個 socket 的讀寫進行監(jiān)控,從而使用少量線程服務于大量 Socket。

由于客戶端開發(fā)的簡便性,大多數的驅動都是基于 BIO 實現(xiàn),包括 MySQL、Redis、Mongo 等;在服務器端,由于其高性能的要求,基本上是 NIO 的天下,以最大限度的提升系統(tǒng)的可擴展性。

由于客戶端存在大量的 BIO 操作,我們的客戶端線程會不停的被 BIO 阻塞,以等待操作返回值,因此線程的效率會大打折扣。

BIO Client

如上圖,線程在 IO 與 CPU 之間不停切換,走走停停,同時線程也沒有辦法釋放,一直等到任務完成。

5.3 Future

構建并發(fā)操作的另一種方案便是 Future,F(xiàn)uture 是一種憑證,調用方法不是直接返回值,而是返回一個 Future 對象,剛創(chuàng)建的 Future 為一個空對象,由后臺線程執(zhí)行耗時操作,并在結束時將結果寫回到 Future 中。

當調用 Future 對象的 get 方法獲取值時,會有兩個可能,如果后臺線程已經運行完成,則直接返回;如果后臺線程沒有運行完成,則阻塞調用線程,知道后臺線程運行完成或超時。

使用 Future 方式,可以以并行的方式運行多個子任務。

當主線程需要調用比較耗時的操作時,可以將其放在輔助線程中執(zhí)行,并在需要數據的時候從 future 中獲取,如果輔助線程已經運行完成,則立即拿到返回的結果,如果輔助線程還沒有運行完成,則主線程等待,并在完成時獲取結果。

基于Future的并發(fā)操作

一種常見的場景是在Controller中從多個Service中獲取結果,并將其封裝成一個View對象返回給前端用于顯示,假設需要從三個接口中獲取結果,每個接口的平均響應時間是20ms,那按照串行模式,總耗時為sum(i1, i2, i3) = 60ms;如果按照Future并發(fā)模式將加載任務交由輔助線程處理,總耗時為max(i1, i2, i3 ) = 20ms, 大大減少了系統(tǒng)的響應時間。

     private ExecutorService executorService = Executors.newFixedThreadPool(20);

    private User loadUserByUid(Long uid){
       sleep(20);
       return new User();
    }

    private Address loadAddressByUid(Long uid){
        sleep(20);
        return new Address();

    }

    private Account loadAccountByUid(Long uid){
        sleep(20);
        return new Account();
    }

    /**
     * 總耗時 sum(LoadUser, LoadAddress, LoadAccount) = 60ms
     * @param uid
     * @return
     */
    public View getViewByUid1(Long uid){
        User user = loadUserByUid(uid);
        Address address = loadAddressByUid(uid);
        Account account = loadAccountByUid(uid);
        View view = new View();
        view.setUser(user);
        view.setAddress(address);
        view.setAccount(account);
        return view;
    }

    /**
     * 總耗時 max(LoadUser, LoadAddress, LoadAccount) = 20ms
     * @param uid
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public View getViewByUid(Long uid) throws ExecutionException, InterruptedException {
        Future<User> userFuture = executorService.submit(()->loadUserByUid(uid));
        Future<Address> addressFuture = executorService.submit(()->loadAddressByUid(uid));
        Future<Account> accountFuture = executorService.submit(()->loadAccountByUid(uid));
        View view = new View();
        view.setUser(userFuture.get());
        view.setAddress(addressFuture.get());
        view.setAccount(accountFuture.get());
        return view;
    }



    private void sleep(long time){
        try {
            TimeUnit.MILLISECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Data
    class View{
        private User user;
        private Address address;
        private Account account;
    }

    class User{

    }

    class Address{

    }

    class Account{

    }

Future 方式存在一個問題,及在調用 get 方法時會阻塞主線程,這是資源的極大浪費,我們真正需要的是一種不必調用 get 方法阻塞當前線程,就可以操作 future 對象返回的結果。

上例中只是子任務能夠拆分并能并行執(zhí)行的一種典型案例,在實際開發(fā)過程中,我們會遇到更多、更復雜的場景,比如:

  • 將兩個 Future 結果合并成一個,同時第二個又依賴于第一個的結果
  • 等待 Future 集合中所有記錄的完成
  • 等待 Future 集合中的最快的任務完成
  • 定義任務完成后的操作

對此,我們引入了 CompletableFuture 對象。

5.4 CompletableFuture

  • CompletableFuture 結合了 Future 和回調兩種策略,以更好的處理事件驅動任務。
  • CompletableFuture 與Stream 的設計思路一致,通過注冊 Lambda 表達式,把高階函數鏈接起來,從而定制更復雜的處理流程。

CompletableFuture 提供了一組函數用于定義流程,其中包括:

5.4.1 創(chuàng)建函數

CompletableFuture 提供了一組靜態(tài)方法用于創(chuàng)建 CompletableFuture 實例:

public static <U> CompletableFuture<U> completedFuture(U value)// :使用已經創(chuàng)建好的值,創(chuàng)建 CompletableFuture 對象。

public static CompletableFuture<Void>   runAsync(Runnable runnable)// 基于 Runnable 創(chuàng)建 CompletableFuture 對象,返回值為 Void,及沒有返回值

public static CompletableFuture<Void>   runAsync(Runnable runnable, Executor executor)// 基于 Runnable 和自定義線程池創(chuàng)建 CompletableFuture 對象,返回值為 Void,及沒有返回值

public static <U> CompletableFuture<U>  supplyAsync(Supplier<U> supplier)// 基于 Supplier 創(chuàng)建 CompletableFuture 對象,返回值為 U

public static <U> CompletableFuture<U>  supplyAsync(Supplier<U> supplier, Executor executor)   // 基于 Supplier 和自定義線程池創(chuàng)建 CompletableFuture 對象,返回值為 U

以 Async 結尾并且沒有指定 Executor 的方法會使用 ForkJoinPool.commonPool() 作為它的線程池執(zhí)行異步代碼。

方法的參數類型都是函數式接口,所以可以使用 Lambda 表達式實現(xiàn)異步任務。

5.4.2 計算結果完成后

當 CompletableFuture 計算完成或者計算過程中拋出異常時進行回調。

public CompletableFuture<T>     whenComplete(BiConsumer<? super T,? super Throwable> action)

public CompletableFuture<T>     whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)

public CompletableFuture<T>     whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)

public CompletableFuture<T>     exceptionally(Function<Throwable,? extends T> fn)

Action 的類型是 BiConsumer<? super T,? super Throwable> 它可以處理正常的計算結果,或者異常情況。

方法不以 Async 結尾,意味著 Action 使用相同的線程執(zhí)行,而 Async 可能會使用其他線程執(zhí)行(如果是使用相同的線程池,也可能會被同一個線程選中執(zhí)行)。

exceptionally 針對異常情況進行處理,當原始的 CompletableFuture 拋出異常的時候,就會觸發(fā)這個 CompletableFuture 的計算。

下面一組方法雖然也返回 CompletableFuture 對象,但是對象的值和原來的 CompletableFuture 計算的值不同。當原先的 CompletableFuture 的值計算完成或者拋出異常的時候,會觸發(fā)這個 CompletableFuture 對象的計算,結果由 BiFunction 參數計算而得。因此這組方法兼有 whenComplete 和轉換的兩個功能。

public <?U> CompletableFuture<?U> handle(BiFunction<? super T,Throwable,? extends U> fn)

public <?U> CompletableFuture<?U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn)

public <?U> CompletableFuture<?U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)
5.4.3 轉化函數

轉化函數類似于 Stream 中的惰性求助函數,主要對 CompletableFuture 的中間結果進行流程定制。

public <U> CompletableFuture<U>     thenApply(Function<? super T,? extends U> fn)

public <U> CompletableFuture<U>     thenApplyAsync(Function<? super T,? extends U> fn)

public <U> CompletableFuture<U>     thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

通過函數完成對 CompletableFuture 中的值得轉化,Async 在線的線程池中處理,Executor 可以自定義線程池。

5.4.4 純消費函數

上面的方法當計算完成的時候,會生成新的計算結果 (thenApply, handle),或者返回同樣的計算結果 whenComplete,CompletableFuture 還提供了一種處理結果的方法,只對結果執(zhí)行 Action,而不返回新的計算值,因此計算值為 Void。

public CompletableFuture<Void> thenAccept(Consumer<? super T> action)

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

其他的參數類型與之前的含義一致,不同的是函數接口 Consumer,這個接口只有輸入,沒有返回值。

thenAcceptBoth 以及相關方法提供了類似的功能,當兩個 CompletionStage 都正常完成計算的時候,就會執(zhí)行提供的 action,它用來組合另外一個異步的結果。

public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<?
   extends U> other, BiConsumer<? super T,? super U> action) 
   
public <U>   CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends
   U> other, BiConsumer<? super T,? super U> action) 
   
public <U>
   CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends
   U> other, BiConsumer<? super T,? super U> action, Executor executor)
5.4.5. 組合函數

組合函數主要應用于后續(xù)計算需要 CompletableFuture 計算結果的場景。

public <U> CompletableFuture<U> thenCompose(Function<? super T,? extends CompletionStage<U>> fn)

public <U> CompletableFuture<U> thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn)

public <U> CompletableFuture<U> thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn, Executor executor)

這一組方法接受一個 Function 作為參數,這個 Function 的輸入是當前的 CompletableFuture 的計算值,返回結果將是一個新的 CompletableFuture,這個新的 CompletableFuture 會組合原來的 CompletableFuture 和函數返回的 CompletableFuture。因此它的功能類似:

A +–> B +—> C 

下面的一組方法 thenCombine 用來復合另外一個 CompletionStage 的結果。兩個 CompletionStage 是并行執(zhí)行的,它們之間并沒有先后依賴順序,other 并不會等待先前的 CompletableFuture 執(zhí)行完畢后再執(zhí)行,當兩個 CompletionStage 全部執(zhí)行完成后,統(tǒng)一調用 BiFunction 函數,計算最終的結果。

public <U,V> CompletableFuture<V> thenCombine(CompletionStage<?
   extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<?
   extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<?
   extends U> other, BiFunction<? super T,? super U,? extends V> fn,
   Executor executor)
5.4.6. Either

Either 系列方法不會等兩個 CompletableFuture 都計算完成后執(zhí)行計算,而是當任意一個 CompletableFuture 計算完成的時候就會執(zhí)行。

public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)

public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)

public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)

public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T,U> fn)

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn)

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn, Executor executor)
5.4.7 輔助方法

輔助方法主要指 allOf 和 anyOf,這兩個靜態(tài)方法用于組合多個 CompletableFuture。

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)// allOf方法是當所有的CompletableFuture都執(zhí)行完后執(zhí)行計算。

public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)// anyOf方法是當任意一個CompletableFuture執(zhí)行完后就會執(zhí)行計算。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容