Java8函數之旅(四) --四大函數接口



前言

  Java8中函數接口有很多,大概有幾十個吧,具體究竟是多少我也數不清,所以一開始看的時候感覺一臉懵逼,不過其實根本沒那么復雜,畢竟不應該也沒必要把一個東西設計的很復雜。

幾個單詞

  在學習了解之前,希望大家能記住幾個單詞,掌握這幾個單詞,什么3,40個官方的函數接口都是小問題了,不信的話接着往下看啦。ok,那這幾個單詞呢分別是supplier 提供者,consumer 消費者,function 函數,operation 運算符,binary 二元(就是數學里二元一次方程那個二元,代表2個的意思),雙重的

四大基礎函數接口

  函數接口,你可以理解為對一段行為的抽象,簡單點說可以在方法就是將一段行為作為參數進行傳遞,這個行為呢,可以是一段代碼,也可以是一個方法,那你可以想象在java8之前要將一段方法作為參數傳遞只能通過匿名內部類來實現,而且代碼很難看,也很長,函數接口就是對匿名內部類的優化。
  雖然類庫中的基本函數接口特別多,但其實總體可以分成四類,就好像阿拉伯數字是無限多的,但總共就10個基本數字一樣,理解了這4個,其他的就都明白了。

Functio<T,R>接口

   function,顧名思義,函數的意思,這里的函數是指數學上的函數哦,你也可以說是嚴格函數語言中的函數,例如haskell里的,他接受一個參數,返回一個值,永遠都是這樣,是一個恆定的,狀態不可改變的方法。其實想講函數這個徹底將明白可以再開一篇博客了,所以這里不詳細的說了。
   上面說到,函數接口是對行為的抽象,因此我方便大家理解,就用java中的方法作例子。

   Fcuntion接口是對接受一個T類型參數,返回R類型的結果的方法的抽象,通過調用apply方法執行內容。

public class Operation{

/* 
    下面這個方法接受一個int類型參數a,返回a+1,符合我上面說的接受一個參數,返回一個值
    所以呢這個方法就符合Function接口的定義,那要怎么用呢,繼續看例子 
*/
public static final int addOne(int a){
    return a+1;
}

/* 
    該方法第二個參數接受一個function類型的行為,然后調用apply,對a執行這段行為
*/
public static int oper(int a, Function<Integer,Integer> action){
    return action.apply(a);
}

/* 下面調用這個oper方法,將addOne方法作為參數傳遞 */
pulic static void main(String[] args){
    int x = 1;
    int y = oper(x,x -> addOne(x));//這里可以換成方法引用的寫法 int y = oper(x,Operation::addOne)
    System.out.printf("x= %d, y = %d", x, y); // 打印結果 x=1, y=2
    
    /* 當然你也可以使用lambda表達式來表示這段行為,只要保證一個參數,一個返回值就能匹配 */
     y = oper(x, x -> x + 3 ); // y = 4
     y = oper(x, x -> x * 3 ); // y = 3    
}

}

這里的箭頭指向的位置就是形參,可以看到第二個箭頭的Lambda表達式指向了Funtion接口

Consumer 接口

Consumer 接口翻譯過來就是消費者,顧名思義,該接口對應的方法類型為接收一個參數,沒有返回值,可以通俗的理解成將這個參數'消費掉了',一般來說使用Consumer接口往往伴隨着一些期望狀態的改變或者事件的發生,例如最典型的forEach就是使用的Consumer接口,雖然沒有任何的返回值,但是卻向控制台輸出了語句。
Consumer 使用accept對參數執行行為

    public static void main(String[] args) {
        Consumer<String> printString = s -> System.out.println(s);
        printString.accept("helloWorld!");
        //控制台輸出 helloWorld!
    }

Supplier 接口

Supplier 接口翻譯過來就是提供者,和上面的消費者相反,該接口對應的方法類型為不接受參數,但是提供一個返回值,通俗的理解為這種接口是無私的奉獻者,不僅不要參數,還返回一個值,使用get()方法獲得這個返回值

        Supplier<String> getInstance = () -> "HelloWorld!";
        System.out.println(getInstance.get());
        // 控偶值台輸出 HelloWorld

Predicate 接口

predicate<T,Boolean> 謂語接口,顧名思義,中文中的‘是’與‘不是’是中文語法的謂語,同樣的該接口對應的方法為接收一個參數,返回一個Boolean類型值,多用於判斷與過濾,當然你可以把他理解成特殊的Funcation<T,R>,但是為了便於區分語義,還是單獨的划了一個接口,使用test()方法執行這段行為

    public static void main(String[] args) {
        Predicate<Integer> predOdd = integer -> integer % 2 == 1;
        System.out.println(predOdd.test(5));
	    //控制台輸出 5
    }
    

其他的接口

介紹完正面這四種最基本的接口,剩余的接口就可以很容易的理解了,java8中定義了幾十種的函數接口,但是剩下的接口都是上面這幾種接口的變種,大多為限制參數類型,數量,下面舉幾個例子。

類型限制接口

  • 參數類型,例如IntPredicate,LongPredicate, DoublePredicate,這幾個接口,都是在基於Predicate 接口的,不同的就是他們的泛型類型分別變成了Integer,Long,Double, IntConsumer,LongConsumer, DoubleConsumer比如這幾個,對應的就是 Consumer<Integer>,Consumer<Long>,Consumer<Double>,其余的是一樣的道理,就不再舉例子了
  • 返回值類型,和上面類似,只是命名的規則上多了一個To,例如IntToDoubleFunction,IntToLongFunction, 很明顯就是對應的Funtion<Integer,Double>Fcuntion<Integer,Long>,其余同理,另外需要注意的是,參數限制與返回值限制的命名唯一不同就是To,簡單來說,前面不帶To的都是參數類型限制,帶To的是返回值類型限制,對於沒有參數的函數接口,那顯而易見只可能是對返回值作限制。例如LongFunction<R>就相當於Function<Long,R> 而多了一個To的ToLongFunction<T>就相當於Function<T,Long>,也就是對返回值類型作了限制。

數量限制接口

  • 有些接口需要接受兩名參數,此類接口的所有名字前面都是附加上Bi,是Binary的縮寫,開頭也介紹過這個單詞了,是二元的意思,例如BiPredicate,BiFcuntion等等,而由於java沒有多返回值的設定,所以Bi指的都是參數為兩個

Operator接口

  • 此類接口只有2個分別是UnaryOperator<T> 一元操作符接口,與BinaryOperator<T>二元操作符接口,這類接口屬於Function接口的簡寫,他們只有一個泛型參數,意思是Funtion的參數與返回值類型相同,一般多用於操作計算,計算 a + b的BiFcuntion如果限制條件為Integer的話 往往要這么寫BiFunction<Integer,Integer,Integer> 前2個泛型代表參數,最后一個代表返回值,看起來似乎是有點繁重了,這個時候就可以用BinaryOperator<Integer>來代替了。

下面是各種類型的接口的示意圖,相信只要真正理解了,其實問題並不大

關於lambda的限制

Java8中的lambda表達式,並不是完全閉包,lambda表達式對值封閉,不對變量封閉。簡單點來說就是局部變量在lambda表達式中如果要使用,必須是聲明final類型或者是隱式的final例如

int num = 123;
Consumer<Integer> print = () -> System.out.println(num);

就是可以的,雖然num沒有被聲明為final,但從整體來看,他和final類型的變量的表現是一致的,可如果是這樣的代碼

int num = 123;
num ++;
Consumer<Integer> print = () -> System.out.println(num);

則無法通過編譯器,這就是對值封閉(也就是棧上的變量封閉)
如果上文中的num是實例變量或者是靜態變量就沒有這個限制。
看到這里,自然而然就會有疑問為什么會這樣?或者說為什么要這么設計。理由有很多,例如函數的不變性,線程安全等等等,這里我給一個簡單的說明

  • 為什么局部變量會有限制而靜態變量和全局變量就沒有限制,因為局部變量是保存在棧上的,而眾所周知,棧上的變量都隱式的表現了它們僅限於它們所在的線程,而靜態變量與實例變量是保存在靜態區與堆中的,而這兩塊區域是線程共享的,所以訪問並沒有問題。
  • 現在我們假設如果lambda表達式可以局部變量的情況,實例變量存儲在堆中,局部變量存儲在棧上,而lambda表達式是在另外一個線程中使用的,那么在訪問局部變量的時候,因為線程不共享,因此lambda可能會在分配該變量的線程將這個變量收回之后,去訪問該變量。所以說,Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什么區別了。
  • 嚴格保證這種限制會讓你的代碼變得無比安全,如果你學習或了解過一些經典的函數式語言的話,就會知道不變性的重要性,這也是為什么stream流可以十分方便的改成並行流的重要原因之一。

總結

本篇介紹了四大函數接口和他們引申出的各類接口,終點是對不同種類行為的封裝導致了設計出不同的函數接口,另外在使用函數接口或者lambda表達式的時候,要注意lambda對值封閉這個特性。

下一篇:開始Java8之旅(五) -- Java8中的排序


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM