【java8新特性】02:常見的函數式介面

2022-09-19 06:00:28

Jdk8提供的函數式介面都在java.util.function包下,Jdk8的函數式型別的介面都有@FunctionInterface註解所標註,但實際上即使沒有該註解標註的有且只有一個抽象方法的介面,都可以算是函數式介面。
在JDK8中內建的四大核心函數式介面如下:

函數式介面介面型別引數型別返回型別作用Stream流中的應用場景
Consumer<T> 消費型介面 T void 對型別為T的物件進行操作,包含方法為accpet(T t) 如forEach、peek等方法的函數式介面都是Consumer型別
Supplier<T> 供給型介面 T 返回型別為T的物件,包含方法為T get() 如collect等方法的某些方法過載就是用的Supplier型別
Function<T,R> 函數型介面 T R 對型別為T的物件進行操作,返回結果為 R型別的物件,包含方法為R apply(T t) 如map,flatMap等方法的函數式介面都是Function型別
Predicate<T> 斷言型介面 T boolean 確定型別為T的物件是否滿足約束,並返回約束結果,包含方法為boolean test(T t) 如filter等方法的函數式介面都是Predicate型別

Consumer<T>

Consumer<T>消費型介面,顧名思義就是消費並處理引數,且不反饋呼叫環境

基本使用

public class Main {
    /**
     * Consumer<T>
     *     消費型介面:顧名思義主要用於消費引數,不反饋呼叫環境(沒有返回值)
     *     accept: 抽象方法實現,用於呼叫方法。
     *     andThen: 預設實現方法,內部允許我們鏈式呼叫
     */
    public static void main(String[] args) {
        // 給定字串轉為大寫並輸出到控制檯,匿名內部類的方式實現
        Consumer<String> con1 = new Consumer<String>() {
            @Override
            public void accept(String str) {
                System.out.println("通過匿名內部類的方式:"+ str.toUpperCase());
            }
        };
        // 執行該方法的時候,我們傳入了給定引數字串,它會去執行我們上述實現的accept方法並傳入引數,最後執行我們給定的程式碼邏輯
        con1.accept("abc");
        // 給定字串轉為大寫並輸出到控制檯,通過Lambda表示式實現
        Consumer<String> con2 = (text)-> System.out.println("通過Lambda表示式的方式:"+ text.toUpperCase());
        con2.accept("goods");
        /**
         * 最終結果:
         * 通過匿名內部類的方式:ABC
         * 通過Lambda表示式的方式:GOOD
         * 使用lambda表示式,我們只需要記住參數列和執行邏輯即可,其他的我們無需關注。
         */
    }}

學習案例

public class Main {

    /**
     * Consumer<T>
     *     消費型介面:顧名思義主要用於消費引數,不反饋呼叫環境(沒有返回值)
     *     accept: 抽象方法實現,用於呼叫方法。
     *     andThen: 預設實現方法,內部允許我們鏈式呼叫
     */
    public static void main(String[] args) {
        // 1.我們需要將集合進行排序後在輸出到控制檯
        Consumer<List> con1 = list-> {
            System.out.println("排序前的集合:"+ list);
            Collections.sort(list);
            System.out.println("排序後的集合:"+list);
        };
        con1.accept(Arrays.asList(1,5,3,2,9,6,7));
        /**
         * 最終結果:
         * 排序前的集合:[1, 5, 3, 2, 9, 6, 7]
         * 排序後的集合:[1, 2, 3, 5, 6, 7, 9]
         */
        // 上面執行邏輯實現分兩步,第一步需要獲取到給定集合進行排序,第二個則是輸出排序後的集合
        // 如果以上兩個步驟分別用兩個consumer也可以實現,我們可以定義一個方法接收兩個consumer進行操作
        accept(Arrays.asList(1,5,3,2,9,6,7),list->
        {
            System.out.println("andThen鏈式呼叫前集合:"+list);
            Collections.sort(list);
        },list-> System.out.println("andThen鏈式呼叫後集合:"+ list));
        /**
         * 最終結果:
         * andThen鏈式呼叫前集合:[1, 5, 3, 2, 9, 6, 7]
         * andThen鏈式呼叫後集合:[1, 2, 3, 5, 6, 7, 9]
         */
        // 如果consumer引數多個的話,我們可以直接在Lambda表示式進行鏈式呼叫,不費那勁定義方法了
        Consumer<List> con2 = ((Consumer<List>) list -> {
            System.out.println("lambda表示式的鏈式呼叫前集合:" + list);
            Collections.sort(list);
        }).andThen(list -> System.out.println("lambda表示式的鏈式呼叫後集合:"+list));
        // 需要注意的是:要使用這種方式,第一個consumer要進行鏈式呼叫必須要強行指定為(Consumer)型別,後續的介面才能夠呼叫方法
        con2.accept(Arrays.asList(1,53,31,25,99,62,17));
        /**
         * 最終結果:
         * lambda表示式的鏈式呼叫前集合:[1, 53, 31, 25, 99, 62, 17]
         * lambda表示式的鏈式呼叫後集合:[1, 17, 25, 31, 53, 62, 99]
         */
    }

    public static void accept(List<Integer> list,Consumer<List> con1,Consumer<List> con2){
        // 鏈式呼叫時會優先執行左邊的介面實現,依次往右執行 我們的需求是先排序後輸出,第一個Consumer是排序,第二個是輸出。
        con1.andThen(con2).accept(list);
    }
}

總結

1.函數式介面的本質實際上就是將函數以引數的形式進行傳遞

2.Consumer是消費型的函數式介面,通常用於資料內部處理,沒有返回值

3.除了Consumer之外,還有各種消費型的函數式介面,還有IntConsumer、LongConsumer等、如果需要傳遞兩個引數則可以使用BIFunction、也可以根據自身需求進行自定義。

Supplier<T>

Consumer<T>供給型函數式介面,顧名思義就是供給資料給呼叫環境,不接收引數傳遞

基本使用

public class Main{
    /**
     * 供給型函數式介面顧名思義就是顧名思義就是供給資料給呼叫環境,不接收引數傳遞
     *  T get() : 返回泛型T型別的引數到呼叫環境
     */
    public static void main(String[] args) {
        // 返回一個0-100間的亂數
        Supplier<Integer> sup1 = new Supplier<Integer>() {
            @Override
            public Integer get() {
                int res = new Random().nextInt(100);
                System.out.println("通過匿名內部類的方式獲取到的亂數:"+ res);
                return res;
            }
        };
        // 執行該方法的時候,它會去執行我們上述實現的get方法。
        sup1.get();
        // 通過lambda表示式的方式進行實現
        Supplier<Integer> sup2 = ()-> {
            int res = new Random().nextInt(100);
            System.out.println("通過lambda表示式的方式獲取到的亂數:"+ res);
            return res;};
        sup2.get();
        /**
         * 最終結果:
         * 通過匿名內部類的方式獲取到的亂數:28
         * 通過lambda表示式的方式獲取到的亂數:62
         * 使用lambda表示式,我們只需要記住參數列和執行邏輯即可,其他的我們無需關注。
         */
    }
}

 學習案例

public class Main{

    public static Map<String,String> redis = new HashMap();
    
    /**
     * 供給型函數式介面顧名思義就是顧名思義就是供給資料給呼叫環境,不接收引數傳遞
     *  T get() : 返回泛型T型別的引數到呼叫環境
     */
    public static void main(String[] args) {
        // 1.(模擬)查詢某個Key在redis中有沒有快取,快取沒有則從資料庫取完存入redis再返回,有的話則直接返回
        String val = getCache("title");
        String val2 = getCache("title");
        String val3 = getCache("title");
        /**
         * 最終結果:
          從資料庫中獲取:我是標題
          從快取中獲取:我是標題
          從快取中獲取:我是標題
         */
        // 可以看到經過第一次後續都是直接從快取中取出的資料
    }

    public static String getCache(String key){
        String val = redis.get(key);
        if(Objects.isNull(val)){
            // 獲取資料庫的資料
            val = getDbVal(() -> "我是標題");
            System.out.println("從資料庫中獲取:"+val);
            redis.put(key,val);
            return val;
        }
        System.out.println("從快取中獲取:"+val);
        return val;
    }

    public static String getDbVal(Supplier<String> supplier){
       return  supplier.get();
    }
}

總結

1.函數式介面的本質實際上就是將函數以引數的形式進行傳遞

2.Supplier是供給型的函數式介面,通常用於構建某個物件處理後返回撥用環境

3.除了Supplier之外,還有各種供給型的函數式介面,還有BooleanSupplier、IntSupplier等。

Function<T,R>

 Function<T,R>函數型的函數式介面,泛型T是引數、泛型R則是返回值、主要應用場景做資料型別轉換等。

基本使用

public class Main{

    /**
     *  Function<T,R>函數型的函數式介面,泛型T是引數、泛型R則是返回值、主要應用場景做資料型別轉換等。
     *  R apply(T t): 抽象方法實現,用於呼叫方法並返回泛型R
     *  <V> Function<T, V> andThen: 預設實現方法,內部允許我們鏈式呼叫,與其他的andThen原理一致。
     *  <V> Function<V, R> compose: 預設實現方法,內部允許我們鏈式呼叫,呼叫方式與andThen一樣,但執行順序不一樣,compose是先執行compose中的函數介面,再執行左邊呼叫的函數介面,依次往左
     *  <T> Function<T, T> identity():返回當前執行的方法,從原始碼中我們也可以看到它返回的是當前的t
     */
    public static void main(String[] args) {

        // 傳入給定字串,返回轉換後的Integer型別
        Function<String,Integer> fun1 = new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                Integer convert = Integer.valueOf(s);
                System.out.println("通過匿名內部類的方式獲取到的值:"+ convert +",資料型別是否為Integer?結果:" + (convert instanceof Integer));
                return convert;
            }
        };
        // 執行該方法的時候,它會去執行我們上述實現的apply方法。
        fun1.apply("10086");
        // 通過lambda表示式的方式進行實現
        Function<String,Integer> fun2 = s->{
            Integer convert = Integer.valueOf(s);
            System.out.println("通過lambda表示式的方式獲取到的值:"+ convert +",資料型別是否為Integer?結果:" + (convert instanceof Integer));
            return convert;
        };
        fun2.apply("10000");
        /**
         * 通過匿名內部類的方式獲取到的值:10086,資料型別是否為Integer?結果:true
         * 通過lambda表示式的方式獲取到的值:10000,資料型別是否為Integer?結果:true
         * 使用lambda表示式,我們只需要記住參數列和執行邏輯即可,其他的我們無需關注。
         */
        // 我們繼續對Function的API做一些理解和補充,畢竟這玩意在工作中經常會用上
        // andThen 我們都知道常用於鏈式呼叫的,這裡必須保證T和V型別是一樣的,也就是引數泛型T和返回值泛型V
        Function<String,String> fun3 = x-> {
            System.out.println("我是fun3的方法");
            return x;
        };
        Function<String,String> fun4 = y-> {
            System.out.println("我是fun4的方法");
            return y;
        };
        fun3.andThen(fun4).apply("test");
        /**
         * 最終結果:
         * 我是fun3的方法
         * 我是fun4的方法
         */
        // 我們發現這裡是先執行fun3的apply方法再執行fun4的apply方法的。
        // compose 與andThen一樣都是鏈式呼叫,但結果卻大大不同,這裡必須保證T和V型別是一樣的,也就是引數泛型T和返回值泛型V
        fun3.compose(fun4).apply("test");
        /**
         * 最終結果:
         * 我是fun4的方法
         * 我是fun3的方法
         */
        // 我們發現這裡是先執行的fun4的apply方法再執行fun3的apply方法的
        // 由此我們推斷出compose和andThen的區別就在於,compose介面方法執行順序從右到左,而andThen則是從左到右。
        Function<Object, Object> identity = Function.identity();
        // Function.identity() 靜態方法這裡就不好演示了,這個通常在後面搭配Stream流轉Map型別的時候用到,它返回本身
    }
}

學習案例

public class Main{

    /**
     *  Function<T,R>函數型的函數式介面,泛型T是引數、泛型R則是返回值、主要應用場景做資料型別轉換等。
     *  R apply(T t): 抽象方法實現,用於呼叫方法並返回泛型R
     *  <V> Function<T, V> andThen: 預設實現方法,內部允許我們鏈式呼叫,與其他的andThen原理一致。
     *  <V> Function<V, R> compose: 預設實現方法,內部允許我們鏈式呼叫,呼叫方式與andThen一樣,但執行順序不一樣,compose是先執行compose中的函數介面,再執行左邊呼叫的函數介面,依次往左
     *  <T> Function<T, T> identity():返回當前執行的方法,從原始碼中我們也可以看到它返回的是當前的t
     */
    public static void main(String[] args) {
        List<Person> persons = Arrays.asList(new Person(1,"張三"),new Person(2,"李四"));
        // 給定一個person物件集、轉換成姓名屬性集合返回
        Function<List<Person>,List<String>> fun1 = list-> {
            List<String> arr = new ArrayList<>();
            for (int i = 0;  i < list.size(); i++) {
                arr.add(list.get(i).getName());
            }
            return arr;
        };
        List<String> personNames = fun1.apply(persons);
        System.out.println(personNames);
        /**
         * 最終結果:
         * 結果:[張三, 李四]
         */
    }
}

class Person{
    private Integer id;
    private String name;

    public Person(Integer id, String name) {
        this.id = id;
        this.name = name;
    }


    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

總結

1.函數式介面的本質實際上就是將函數以引數的形式進行傳遞

2.Function是函數型的函數式介面,通常用於構建某個物件處理後返回撥用環境

3.除了Function之外,還有各種函數型的函數式介面,還有BIFunction、ToIntFunction等。

Predicate

 Predicate<T> 斷言型的函數式介面,泛型T是引數、返回結果型別為布林型別的函數介面。

基本使用

public class Main{

    /**
     *  Predicate<T>斷言型的函數式介面,泛型T是引數、返回結果型別為布林型別的函數介面。
     *  boolean test(T t): 抽象方法實現,用於返回傳入的引數邏輯運算後布林型別結果
     *  Predicate<T> and: 預設實現方法,內部允許我們鏈式呼叫,用於同時判定多個Predicate函數介面的實現 類似於邏輯運算中的短路&操作。
     *  Predicate<T> negate: 預設實現方法,內部允許我們鏈式呼叫,用於同時判定多個Predicate函數介面的實現,用於將當前判定結果取反後返回,類似於邏輯運算中的!操作
     *  Predicate<T> or:預設實現方法,內部允許我們鏈式呼叫,用於同時判定多個Predicate函數介面的實現 類似於邏輯運算中的||操作。
     *  Predicate<T> isEqual:靜態方法,內部允許我們鏈式呼叫,在保證引數不是空的情況下它內部實現邏輯實際上呼叫的是Object的equals,具體equals看子類有沒有重寫
     */
    public static void main(String[] args) {
        Predicate<String> pre1 = new Predicate<String>() {
            @Override
            public boolean test(String o) {
                boolean bool = o.matches("[0-9]{1,}");
                System.out.println("通過匿名內部類的方式獲取到的值:"+ bool);
                return bool;
            }
        };
        // 執行該方法的時候,它會去執行我們上述實現的test方法。
        pre1.test("10086");
        Predicate<String> pre2 = text->{
            boolean bool = text.matches("[0-9]{1,}");
            System.out.println("通過lambda表示式的方式獲取到的值:"+ bool);
            return bool;
        };
        pre2.test("10086a");
        /**
         * 最終結果:
         * 通過匿名內部類的方式獲取到的值:true
         * 通過lambda表示式的方式獲取到的值:false
         * 使用lambda表示式,我們只需要記住參數列和執行邏輯即可,其他的我們無需關注。
         */
        // 我們繼續對Predicate的API做一些理解和補充,畢竟這玩意在工作中經常會用上
        // and 實際上等價於邏輯運運算元中的短路&操作
        Predicate<String> fun3 = x->
        {
            System.out.println("先計算fun3");
            return true;
        };
        Predicate<String> fun4 = x->
        {
            System.out.println("先計算fun4");
            return false;
        };
        System.out.println("第一次and結果:"+fun3.and(fun4).test("test"));
        /**
         * 最終結果:
         * 先計算fun3
         * 先計算fun4
         * 本次結果:false
         */
        // 那麼為什麼我們知道它是短路&的操作 而不是&的操作呢?,我們只需要將第一個函數式介面返回false,看看它還會不會執行第二個函數式介面即可
        Predicate<String> fun5 = x->
        {
            System.out.println("先計算fun5");
            return false;
        };
        Predicate<String> fun6 = x->
        {
            System.out.println("先計算fun6");
            return true;
        };
        System.out.println("第二次and結果:"+fun5.and(fun6).test("test"));
        /**
         * 最終結果:
         * 先計算fun5
         * 第二次and結果:false。
         */
        // 從結果我們其實可以推斷出,在第一個結果為true的情況下第二個fun6壓根沒進,所以是短路&
        // 並且起始在and方法原始碼中給我們也可以看到 return (t) -> test(t) && other.test(t); 是短路&

        // negate 實際上等價於邏輯運運算元中的!操作
        // 我們直接取上面的值做例子,本來結果應該為false,取反後應該為true
        System.out.println("negate結果:"+fun5.and(fun6).negate().test("test"));
        /**
         * 最終結果:
         * negate結果:true
         */

        // or 等價於邏輯運運算元中的||操作
        // 我們直接取上面的做例子,第一個為false,第二個為true、||的最終結果應該為true
        System.out.println("or結果:"+fun5.or(fun6).test("test"));
        /**
         * 最終結果:
         * or結果:true
         */

        // isEqual 內部呼叫的是Object的equals方法,如果子類重寫了equals則調起子類的equals方法
        // 如我們常用的String就重寫了Object的equals方法,我們以它做例子
        Predicate<String> fun7 = Predicate.isEqual("Hello");
        System.out.println("isEquals第一次結果:"+ fun7.test("Hello"));
        /**
         * 最終結果:
         * isEquals第一次結果:true
         */
        Predicate<String> fun8 = Predicate.isEqual("World");
        System.out.println("isEquals第二次結果:"+ fun8.test("Hello"));
        // 自定義的物件型別也是可以比較的,但需要重寫equals和hashCode,這裡就不寫範例了,可以自己玩玩

        // 以上就是Predicate的相關API的介紹
    }
}

學習案例

public class Main{

    /**
     *  Predicate<T>斷言型的函數式介面,泛型T是引數、返回結果型別為布林型別的函數介面。
     *  boolean test(T t): 抽象方法實現,用於返回傳入的引數邏輯運算後布林型別結果
     *  Predicate<T> and: 預設實現方法,內部允許我們鏈式呼叫,用於同時判定多個Predicate函數介面的實現 類似於邏輯運算中的短路&操作。
     *  Predicate<T> negate: 預設實現方法,內部允許我們鏈式呼叫,用於同時判定多個Predicate函數介面的實現,用於將當前判定結果取反後返回,類似於邏輯運算中的!操作
     *  Predicate<T> or:預設實現方法,內部允許我們鏈式呼叫,用於同時判定多個Predicate函數介面的實現 類似於邏輯運算中的||操作。
     *  Predicate<T> isEqual:靜態方法,內部允許我們鏈式呼叫,在保證引數不是空的情況下它內部實現邏輯實際上呼叫的是Object的equals,具體equals看子類有沒有重寫
     */
    public static void main(String[] args) {
        // 判斷給定字串是否純數位並且小於10 可以使用and進行鏈式呼叫
        Predicate<String> pre1 = ((Predicate<String>) s -> s.matches("[0-9]{1,}")).and(x->Integer.valueOf(x) <10);
        System.out.println("使用and方式進行鏈式呼叫:"+pre1.test("9"));
        // 需要注意的是:要使用這種方式,第一個Predicate要進行鏈式呼叫必須要強行再指定為(Predicate)型別,後續的介面才能夠呼叫方法
        // 實際上這種方式用的比較少,因為比較麻煩,所以一般都會直接使用&&進行判定
        Predicate<String> pre2 = s->  s.matches("[0-9]{1,}") && Integer.valueOf(s) <10;
        System.out.println("使用&&方式呼叫:"+pre2.test("10"));
        /**
         * 最終結果:
         * 使用and方式進行鏈式呼叫:true
         * 使用&&方式呼叫:false
         */
    }
}

以上就是Jdk8提供的基礎的四大函數(除了這四個大的分類還有許多函數式介面,也可以自定義函數式介面實現我們的需求)的基本使用方式和一些簡單案例,具體該怎麼做怎麼寫則需要根據專案實際需求進行,通常函數式介面都會搭配Stream成套使用,目前也有很多框架支援函數式介面的方式、如MyBatis-plus等社群活躍度較高的框架。