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

前言

  Java8中函數接口有不少,大概有幾十個吧,具體到底是多少我也數不清,因此一開始看的時候感受一臉懵逼,不過其實根本沒那麼複雜,畢竟不該該也不必把一個東西設計的很複雜。java

幾個單詞

  在學習瞭解以前,但願你們能記住幾個單詞,掌握這幾個單詞,什麼3,40個官方的函數接口都是小問題了,不信的話接着往下看啦。ok,那這幾個單詞呢分別是supplier 提供者,consumer 消費者,function 函數,operation 運算符,binary 二元(就是數學裏二元一次方程那個二元,表明2個的意思),雙重的安全

四大基礎函數接口

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

Functio<t,r>接口

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

   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()方法得到這個返回值spa

        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對值封閉這個特性。

相關文章
相關標籤/搜索