Java編程思想——函數式編程Lambda

(一)函數式編程

函數式編程(FP)的意義所在:通過合併現有代碼來生成新功能而不是從頭開始編寫所有內容,我們可以更快地獲得更可靠的代碼.至少在某些情況下,這套理論似乎很有用.在這一過程中,一些非函數式語言已經習慣了使用函數式編程產生的優雅的語法

OO(object oriented,面向對象)是抽象數據,FP(functional programming,函數式編程)是抽象行爲.

Java 8 的 Lambda 表達式.由箭頭 -> 分隔開參數和函數體,箭頭左邊是參數,箭頭右側是從 Lambda 返回的表達式,即函數體.這實現了與定義類、匿名內部類相同的效果,但代碼少得多.

Java 8 的方法引用,由 :: 區分.在 :: 的左邊是類或對象的名稱,在 :: 的右邊是方法的名稱,但沒有參數列表

(二)Lambda表達式

(1)Lambda使用實例
Lambda 表達式是使用最小可能語法編寫的函數定義:

Lambda 表達式產生函數,而不是類. 在 JVM(Java Virtual Machine,Java 虛擬機)上,一切都是一個類,因此在幕後執行各種操作使 Lambda 看起來像函數 —— 但作爲程序員,你可以高興地假裝它們“只是函數”.

Lambda 語法儘可能少,這正是爲了使 Lambda 易於編寫和使用

interface Book {
    String getBookName(String name);
}

interface Person {
    String description(String name, int age);
}

interface Multi {
    String twoArg(String head, Double d);
}

public class LambdaExpressions {

    static Book book = b -> "書籍名稱:" + b;

    static Person person = (String a, int b) -> {
        return "姓名:" + a + "--年齡:" + b;
    };

    static Multi mult = (h, n) -> h + n;

    public static void main(String[] args) {
        System.out.println(book.getBookName("三國演義"));
        System.out.println(person.description("張三", 18));
        System.out.println(mult.twoArg("Pi! ", 3.14159));
    }
}

我們從三個接口開始,每個接口都有一個單獨的方法(功能性接口).但是,每個方法都有不同數量的參數,以便演示 Lambda 表達式語法

(2)任何 Lambda 表達式的基本語法

1:參數
2:接着 ->,可視爲“產出”
3:-> 之後的內容都是方法體

(3)Lambda 表達式的結構
1:Lambda 表達式可以具有零個,一個或多個參數.
2:可以顯式聲明參數的類型,也可以由編譯器自動從上下文推斷參數的類型.例如(int a) 與(a)相同
3:參數用小括號括起來,用逗號分隔.例如 (a, b) 或 (int a, int b) 或 (String a, int b, float c)
4:空括號用於表示一組空的參數.例如 () -> 42.
5::當有且僅有一個參數時,如果不顯式指明類型,則不必使用小括號.例如 a -> return a*a.
6:Lambda 表達式的正文可以包含零條,一條或多條語句.
7:如果 Lambda 表達式的正文只有一條語句,則大括號可不用寫,且表達式的返回值類型要與匿名函數的返回類型相同,同時可以不寫return關鍵字
static Person person = (String a, int b) -> “姓名:”+a +"–年齡:"+b;

8:如果 Lambda 表達式的正文有一條以上的語句必須包含在大括號(代碼塊)中,且表達式的返回值類型要與匿名函數的返回類型相同

9:如果在 Lambda 表達式中確實需要多行,則必須將這些行放在花括號中. 在這種情況下,就需要使用 return

(4)遞歸

遞歸函數是一個自我調用的函數.可以編寫遞歸的 Lambda 表達式,但需要注意:遞歸方法必須是實例變量或靜態變量,否則會出現編譯時錯誤. 我們將爲每個案例創建一個示例.

public class RecursiveFactorial {
    static IntCall fact;

    public static void main(String[] args) {
        fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
        for (int i = 0; i <= 10; i++) {
            System.out.println(fact.call(i));
        }
    }
}

interface IntCall {
    int call(int arg);
}

上述遞歸代碼中中間Lambda部分完整代碼就是下面這樣子:

IntCall fact = (n) -> {
            return n == 0 ? 1 : n * fact.call(n - 1);
        };

IntCall 接口中有唯一的單個參數的方法,這裏,fact 是一個靜態變量. 注意使用三元 if-else. 遞歸函數將一直調用自己,直到 i == 0.所有遞歸函數都有“停止條件”,否則將無限遞歸併產生異常.

我們可以將 Fibonacci 序列改爲使用遞歸 Lambda 表達式來實現,這次使用實例變量:

public class RecursiveFibonacci {
    IntCall fib;

    RecursiveFibonacci() {
        fib = n -> n == 0 ? 0 :
                n == 1 ? 1 :
                        fib.call(n - 1) + fib.call(n - 2);
    }

    int fibonacci(int n) {
        return fib.call(n);
    }

    public static void main(String[] args) {
        RecursiveFibonacci rf = new RecursiveFibonacci();
        for (int i = 0; i <= 10; i++) {
            System.out.println(rf.fibonacci(i));
        }
    }
}

下面來解釋一下上面的代碼,其中call方法內部實現對象通過構造函數創建而成,這裏可以理解爲在構造函數中構建了接口的匿名函數,然後返回了一個匿名對象實例,即子類實現了接口的方法,然後拿到該實例調用接口方法

構造函數內代碼完成格式如下:

 IntCall fib = (n) -> {
        return n == 0 ? 0 : (n == 1 ? 1 : this.fib.call(n - 1) + this.fib.call(n - 2));
    };

倆個三元運算,判斷n是否0,是否1,否則繼續調用

(三)方法引用

(1)方法引用
類名::方法名

如果Lambda表達式需要做的事情,在另外一個類當中已經做過了,那麼就可以使用方法引用的寫法

靜態方法: 類名稱::靜態方法名
成員方法: 對象實例名稱::成員方法名

下面看下《Java編程思想》裏面提供實例

import java.util.*;

interface Callable { // [1]
  void call(String s);
}

class Describe {
  void show(String msg) { // [2]
    System.out.println(msg);
  }
}

public class MethodReferences {
  static void hello(String name) { // [3]
    System.out.println("Hello, " + name);
  }
  static class Description {
    String about;
    Description(String desc) { about = desc; }
    void help(String msg) { // [4]
      System.out.println(about + " " + msg);
    }
  }
  static class Helper {
    static void assist(String msg) { // [5]
      System.out.println(msg);
    }
  }
  public static void main(String[] args) {
    Describe d = new Describe();
    Callable c = d::show; // [6]
    c.call("call()"); // [7]

    c = MethodReferences::hello; // [8]
    c.call("Bob");

    c = new Description("valuable")::help; // [9]
    c.call("information");

    c = Helper::assist; // [10]
    c.call("Help!");
  }
}
call()
Hello, Bob
valuable information
Help!

[1] 我們從單一方法接口開始(同樣,你很快就會了解到這一點的重要性).

[2] show() 的簽名(參數類型和返回類型)符合 Callable 的 call() 的簽名.

[3] hello() 也符合 call() 的簽名.

[4] help() 也符合,它是靜態內部類中的非靜態方法.

[5] assist() 是靜態內部類中的靜態方法.

[6] 我們將 Describe 對象的方法引用賦值給 Callable ,它沒有 show() 方法,而是 call() 方法. 但是,Java 似乎接受用這個看似奇怪的賦值,因爲方法引用符合 Callable 的 call() 方法的簽名.

[7] 我們現在可以通過調用 call() 來調用 show(),因爲 Java 將 call() 映射到 show().

[8] 這是一個靜態方法引用.

[9] 這是 [6] 的另一個版本:對已實例化對象的方法的引用,有時稱爲綁定方法引用.

[10] 最後,獲取靜態內部類的方法引用的操作與 [8] 中外部類方式一樣.

備註:
通過上面實例可以得出一個很重要的結論,那就是通過單一方法接口可以將具有相同方法簽名的方法與其他類方法映射,且使用該接口接收,通過這種方式去實現該接口具體功能

(2)Runnable接口

Runnable 接口自 1.0 版以來一直在 Java 中,因此不需要導入.它也符合特殊的單方法接口格式:它的方法 run() 不帶參數,也沒有返回值.因此,我們可以使用 Lambda 表達式和方法引用作爲 Runnable:

@FunctionalInterface
public interface Runnable {
   
    public abstract void run();
}
 public static void main(String[] args) {
        //傳統方式創建線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程運行2");
            }
        }).start();
        //Lambda方式創建線程
        new Thread(() -> System.out.println("線程運行1")).start();
    }

在這裏插入圖片描述

(四)函數式接口

(1)函數式接口
Java中使用Lambda表達式的前提是:必須有“函數式接口”
函數式接口:有且僅有一個抽象方法的接口;

如何才能萬無一失地檢測的那個接口是不是函數式接口?
用一個固定的格式寫在public interface之前接口
@FunctionalInterface

@FunctionalInterface 註解是可選的; 接口中如果有多個方法則會產生編譯時錯誤消息.

Lambda表達式要想使用,一定要有函數式接口的推斷環境
1:要麼通過方法的參數類型來確定那個函數式接口
2:要麼通過賦值操作來確定是哪個函數式接口

Lambda的格式就是爲了將抽象方法,翻譯成以下三點:
1:一些參數(方法參數)
2:一個箭頭
3:一些代碼(方法體)

例如抽象方法
public abstract int sum(int a ,int b)
翻譯成Lambda標準格式:
(int a ,int b) -> {return a+b ;}

(2)上下文推斷

1:如果作爲傳參時是根據調用方法的參數類型來判斷的
method(int a,int b)-> {return a+b;});
2:根據賦值語句左側的類型來進行Lambda上下文推斷
Calculator param=(int a,int b)-> {return a+b;};
method(param);
//(int a,int b)-> {return a+b;}; 錯誤寫法,沒有上下文環境,無法推斷是那個函數式接口

@FunctionalInterface
interface Functional {
    String goodbye(String arg);
}

interface FunctionalNoAnn {
    String goodbye(String arg);
}


public class FunctionalAnnotation {
    public String goodbye(String arg) {
        return "Goodbye, " + arg;
    }

    public static void main(String[] args) {
        FunctionalAnnotation fa =
                new FunctionalAnnotation();
        Functional f = fa::goodbye;
        FunctionalNoAnn fna = fa::goodbye;
        Functional fl = a -> "Goodbye, " + a;
        FunctionalNoAnn fnal = a -> "Goodbye, " + a;
    }
}

上面代碼中, Functional 和 FunctionalNoAnn 自定義接口,然而被賦值的只是方法 goodbye().首先,這只是一個方法而不是類;其次,它甚至都不是實現了該接口的類中的方法.Java 8 在這裏添加了一點小魔法:如果將方法引用或 Lambda 表達式賦值給函數式接口(類型需要匹配),Java 會適配你的賦值到目標接口. 編譯器會自動包裝方法引用或 Lambda 表達式到實現目標接口的類的實例中.

儘管 FunctionalAnnotation 確實適合 Functional 模型,但 Java 不允許我們將 FunctionalAnnotation 像 fac 定義一樣直接賦值給 Functional,因爲它沒有明確地實現 Functional 接口. 令人驚奇的是 ,Java 8 允許我們以簡便的語法爲接口賦值函數.

java.util.function 包旨在創建一組完整的目標接口,使得我們一般情況下不需再定義自己的接口.這主要是因爲基本類型會產生一小部分接口. 如果你瞭解命名模式,顧名思義就能知道特定接口的作用.

(3)函數式接口基本命名準則
1:如果只處理對象而非基本類型,名稱則爲 Function,Consumer,Predicate 等.參數類型通過泛型添加.

2:如果接收的參數是基本類型,則由名稱的第一部分表示,如 LongConsumer,DoubleFunction,IntPredicate 等,但基本 Supplier 類型例外.

3:如果返回值爲基本類型,則用 To 表示,如 ToLongFunction 和 IntToLongFunction.

4:如果返回值類型與參數類型一致,則是一個運算符:單個參數使用 UnaryOperator,兩個參數使用 BinaryOperator.

5:如果接收兩個參數且返回值爲布爾值,則是一個謂詞(Predicate).

6:如果接收的兩個參數類型不同,則名稱中有一個 Bi

(4)多參數函數式接口
java.util.functional 中的接口是有限的。比如有了 BiFunction,但它不能變化。 如果需要三參數函數的接口怎麼辦? 其實這些接口非常簡單,很容易查看 Java 庫源代碼並自行創建。代碼示例:

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}
public class TriFunctionTest {
    static double f(int i, long l, double d) {
        return i + l + d;
    }

    public static void main(String[] args) {
        TriFunction<Integer, Long, Double, Double> tf =
                TriFunctionTest::f;
        //調用函數,傳遞各個參數
        Double apply = tf.apply(2, 30L, 33.1);
        System.out.println(apply);
    }
}

在這裏插入圖片描述
解釋:
TriFunction接口定義了函數式接口,其中有三個不同的基本類型參數,有返回值
TriFunction<Integer, Long, Double, Double> tf =
TriFunctionTest::f;
通過方法引用,將對應參數和返回值裝配到函數式接口中,前三個爲參數,第四個參數爲返回值類型
apply方法用於傳遞參數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章