[二] java8 函數式接口詳解 函數接口詳解 lambda表達式 匿名函數 方法引用使用含義 函數式接口實例 如何定義函數式接口

函數式接口詳細定義

package java.lang;
import java.lang.annotation.*;
/**
* An informative annotation type used to indicate that an interface
* type declaration is intended to be a <i>functional interface</i> as
* defined by the Java Language Specification.
*
* Conceptually, a functional interface has exactly one abstract
* method. Since {@linkplain java.lang.reflect.Method#isDefault()
* default methods} have an implementation, they are not abstract. If
* an interface declares an abstract method overriding one of the
* public methods of {@code java.lang.Object}, that also does
* <em>not</em> count toward the interface's abstract method count
* since any implementation of the interface will have an
* implementation from {@code java.lang.Object} or elsewhere.
*
* <p>Note that instances of functional interfaces can be created with
* lambda expressions, method references, or constructor references.
*
* <p>If a type is annotated with this annotation type, compilers are
* required to generate an error message unless:
*
* <ul>
* <li> The type is an interface type and not an annotation type, enum, or class.
* <li> The annotated type satisfies the requirements of a functional interface.
* </ul>
* <p>However, the compiler will treat any interface meeting the
* definition of a functional interface as a functional interface
* regardless of whether or not a {@code FunctionalInterface}
* annotation is present on the interface declaration.
*
* @jls 4.3.2. The Class Object
* @jls 9.8 Functional Interfaces
* @jls 9.4.3 Interface Method Body
* @since 1.8
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
一種用於表示一個接口是Java語言規範定義的函數式接口的註解類型.
 

關鍵概念

從文件註釋中咱們能夠看到函數式接口的關鍵概念
函數式接口只有一個抽象方法
因爲default方法有一個實現,因此他們不是抽象的.
若是一個接口定義了一個抽象方法,而他剛好覆蓋了Object的public方法,仍舊不算作接口的抽象方法, 由於它終將會在某處獲得一個實現.(若是不是public的那麼計數) 
也便是隻有一個抽象方法默認不算,Object的public也不算
函數式接口的實例能夠經過 lambda表達式  方法引用 或者構造方法引用進行表示
類型必須是接口,而不能是其餘的好比class 並且須要符合函數式接口的定義要求 不然使用註解時編譯器報錯
無論他們是否有使用註解FunctionalInterface 進行註解, 編譯器將會把任何知足函數式接口定義的接口當作一個函數式接口 也就是說不加也行,可是顯然不加的話,就沒有限制約束,後續可能增長了其餘方法致使出錯
 
 
 
 

經常使用函數式接口

四大基礎接口   java.util.function 包
     接口                           抽象方法
  • Predicate<T>           boolean test(T t);
  • Consumer<T>          void accept(T t);
  • Function<T, R>        R apply(T t);
  • Supplier<T>             T get();
 
 
java.util.function.Predicate<T>
斷言 也就是條件測試器 接收條件,進行測試
接口定義了一個名叫test的抽象方法,它接受泛型T對象,並返回一個boolean。
test (條件測試) , and-or- negate(與或非) 方法
image_5b791351_3059
 
 
java.util.function.Consumer<T>
消費者 消費數據 接收參數,返回void  數據被消費了
定義了一個名叫accept的抽象方法,它接受泛型T的對象,沒有返回(void)
你若是須要訪問類型T的對象,並對其執行某些操做,就可使用這個接口
image_5b791352_2446
 
 
java.util.function.Function<T, R>
函數 有輸入有輸出 數據轉換功能
接口定義了一個叫做apply的方法,它接受一個泛型T的對象,並返回一個泛型R的對象。
image_5b791352_59d1
 
java.util.function.Supplier<T>
提供者 不須要輸入,產出T 提供數據
無參構造方法 提供T類型對象
image_5b791352_112b
 

接口中的compose, andThen, and, or, negate 用來組合函數接口而獲得更強大的函數接口
四大接口爲基礎接口,其餘的函數接口都是經過這四個擴展而來的
此處的擴展是指概念的展開  不是日常說的繼承或者實現,固然實現上多是經過繼承好比UnaryOperator
 

擴展方式:

 
參數個數上擴展
        好比接收雙參數的,有 Bi 前綴, 好比 BiConsumer<T,U>, BiFunction<T,U,R> ;
特殊經常使用的變形
        好比 BinaryOperator , 是同類型的雙參數 BiFunction<T,T,T> ,二元操做符 ; UnaryOperator 是 Function<T,T> 一元操做符。
類型上擴展
        好比接收原子類型參數的,好比 [Int|Double|Long]  [Function|Consumer|Supplier|Predicate]
 
image_5b791352_7f5f

爲何要有基本類型擴展

 
只有對象類型才能做爲泛型參數,對於基本類型就涉及到裝箱拆箱的操做,雖然是自動的
可是這不可避免給內存帶來了額外的開銷,裝箱和拆箱都會帶來開銷
因此爲了減少這些性能開銷   對基本類型進行類型擴展
Stream 類的某些方法對基本類型和裝箱類型作了區分
Java 8中,僅對 整型長整型雙浮點型作了特殊處理  由於它們在數值計算中用得最多
 
對基本類型作特殊處理的方法在命名上有明確的規範
  • 若是參數是基本類型,則不加前綴只需類型名便可
  • 若是方法返回類型爲基本類型,則在基本類型前再加上一個 To
總結一句話:
加了類型前綴[Int|Double|Long] 表示參數是基本類型, 若是在此基礎上又加上了To  表示返回類型是基本類型 
若有可能,應儘量多地使用對基本類型作過特殊處理的方法,進而改善性能
 
 

函數式接口的實例

 
函數式接口的實例能夠經過 lambda表達式  方法引用 或者構造方法引用進行表示
 

Lambda表達式

能夠把Lambda表達式理解爲簡潔地表示可傳遞的匿名函數的一種方式,也就是用來表示匿名函數
它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個能夠拋出的異常列表。
特色
  • 匿名——咱們說匿名,是由於它不像普通的方法那樣有一個明確的名稱:寫得少而想得多!
  • 函數——咱們說它是函數,是由於Lambda函數不像方法那樣屬於某個特定的類。但和方法同樣,Lambda有參數列表、函數主體、返回類型,還可能有能夠拋出的異常列表。
  • 傳遞——Lambda表達式能夠做爲參數傳遞給方法或存儲在變量中。
  • 簡潔——無需像匿名類那樣寫不少模板代碼。 
基本語法
 
Lambda的基本語法是
(parameters) -> expression
或(請注意語句的花括號)
(parameters) -> { statements; } 
 
Lambda表達式三個部分
  1. 參數列表
  2. 箭頭   ( -> 把參數列表與Lambda主體分隔開)
  3. Lambda主體 (表達式或者語句)
一些變形
    1.  表達式不包含參數,使用空括號 () 表示沒有參數         () -> System.out.println("Hello World");      
    2.  包含且只包含一個參數,可省略參數的括號                s  -> System.out.println("Hello World"); 
    3.  Lambda 表達式的主體不只能夠是一個表達式,並且也能夠是一段代碼塊,使用大括號({})將代碼塊括起來
         該代碼塊和普通方法遵循的規則別無二致,能夠用返回或拋出異常來退出。
         只有一行代碼的 Lambda表達式也可以使用大括號,用以明確 Lambda表達式從何處開始、到哪裏結束。
() -> {
    System.out.print("Hello");
    System.out.println(" World");
};
  4.   Lambda 表達式也能夠表示包含多個參數的方法   (Long x, Long y) -> x + y; 
     5.    能夠把  4  中的表達式進行簡化,(x, y) -> x + y;    這藉助於類型推斷 下面會說到 
 
Lambda只能引用值,而不是變量(要求事實上的final)
匿名內部類,須要引用它所在方法裏的變量時,須要將變量聲明爲 final 
Lambda表達式不要求必須是final 變量  可是,該變量在既成事實上必須是final 
事實上的 final 是指只能給該變量賦值一次。換句話說,Lambda 表達式引用的是值,而不是變量 跟匿名內部類相似,使用的是變量值的拷貝 因此須要是不改變的
若是你試圖給該變量屢次賦值,而後在 Lambda 表達式中引用它,編譯器就會報錯
好比:
image_5b791352_6367
無需設置final 一切運行正常
image_5b791352_2916
一旦給hello變量從新賦值 ,編譯器將會報錯
image_5b791352_49aa
 
 

方法引用

方法引用讓你能夠重複使用現有的方法定義  並像Lambda同樣傳遞它們
方法引用使用  :: 來表示
方法引用主要有三類
(1) 指向靜態方法的方法引用(例如Integer的parseInt方法, 寫做Integer::parseInt) 
      也就是靜態方法做用於對象上
 
image_5b791352_7e3a
示例:字符串轉換爲數值
image_5b791352_549f
image_5b791352_66ad
 
 
 
(2)指向 任意類型實例方法 的方法引用(例如 String 的 length 方法,寫做String::length)
  你在引用一個對象的方法,而這個對象自己是Lambda的一個參數。例如,Lambda表達式(String s) -> s.toUppeCase()   能夠寫做String::toUpperCase  
 
image_5b791352_6794
 
示例:打印字符串的長度 1個 3個  2個   (沒有空格和換行因此擠在一塊兒了)
image_5b791352_4119
image_5b791352_5b8a
 
 
(3) 指向現有對象的實例方法的方法引用
好比lambda表達式中調用字符串helloString的charAt()方法  helloString就是一個現有對象
image_5b791352_242a
示例:獲取字符串位於給定序列的charAt值
image_5b791352_1b9c
image_5b791352_35bf
 

構造函數引用

對於一個現有構造函數,你能夠利用它的名稱和關鍵字new來建立它的一個引用:
ClassName::new
它的功能與指向靜態方法的引用相似
 
定義Class A   三個屬性 設置了默認值 以觀察構造方法的調用狀況
class A {
private String s1="a";
private String s2="b";
private String s3="c";
A(){
}
A(String s1){
this.s1=s1;
}

A(String s1,String s2){
this.s1=s1;
this.s2=s2;
}
A(String s1,String s2,String s3){
this.s1=s1;
this.s2=s2;
this.s3=s3;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("A{");
sb.append("s1='").append(s1).append('\'');
sb.append(", s2='").append(s2).append('\'');
sb.append(", s3='").append(s3).append('\'');
sb.append('}');
return sb.toString();
}
}

 

image_5b791352_489c
image_5b791352_425
能夠看到分別調用了,無參構造方法 一個參數構造方法以及兩個參數構造方法
 
若是三個構造方法如何設置呢?
咱們只須要定義函數接口便可
image_5b791352_6d59
 
image_5b791352_35c0
再次運行
image_5b791352_a33
 
 
 
 

類型檢查與類型推斷

 

類型檢查

咱們知道當咱們操做賦值運算時會有類型檢查
好比:
image_5b791352_16d2
那麼對於函數式接口與函數值呢 
函數式接口 變量名 = Lambda-匿名函數/方法引用/構造方法引用;
 
那麼函數做爲值是如何進行類型檢查的?
 
Lambda的類型是從使用Lambda的上下文推斷出來的
上下文中Lambda表達式須要的類型稱爲目標類型
上下文是好比接受它傳遞的方法的形式參數,或接受它的值的局部變量
形式參數或者局部變量都會有類型的定義與聲明
 
好比篩選 1~9之中小於5的數值

List<Integer> listNum = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); java

List filteredNum = listNum.stream().filter(i -> i.compareTo(5) < 0).collect(Collectors.toList()); web

System.out.println(filteredNum);express

image_5b791352_5a3e
這個示例中接收  Lambda表達式  做爲參數的形式參數爲  Predicate<? super T> predicate
也就是目標類型    函數接口爲Predicate<T> 
找到了目標類型 咱們的T爲Integer
也就是Predicate<Integer> 
他的抽象方法爲 boolean test(T t);   也就是  boolean test(Integer t);  接收一個Integer返回一個boolean
咱們的Lambda匿名函數 i -> i.compareTo(5) < 0 就是接收一個Integer  返回一個boolean 因此類型檢查經過
簡單說就是:
1. 經過形參類型或者變量類型 找到函數接口進而找到抽象方法的聲明
2. 而後在與參數值進行比對查看是否匹配
 
能夠看得出來,Lambda表達式最終匹配的是 函數接口中的抽象方法的方法簽名
若是不一樣的函數接口,具備相互兼容的抽象方法簽名  那麼一個Lambda表達式顯然能夠匹配多個函數接口
 
特殊的void兼容規則
若是一個Lambda的主體是一個語句表達式, 它就和一個返回void的函數描述符兼容(固然須要參數列表也兼容)。
就是說 若是主體是一個語句,無論作什麼或者調用方法返回其餘的類型,他均可以兼容void
 
例如
List的add方法   boolean add(E e);

List<String> list= new ArrayList<>(); app

// Predicate返回了一個boolean less

Predicate<String> p = s -> list.add(s); ide

// Consumer返回了一個void 函數

Consumer<String> b = s -> list.add(s);性能

上面的代碼均可以經過編譯,而且運行
 

類型推斷

 
類型推斷的概念,在Java中不是第一次出現
Java SE 7以前,聲明泛型對象的代碼以下
List<String> list = new ArrayList<String>();
Java 7中,可使用以下代碼:
List<String> list = new ArrayList<>();
這就是類型推斷  ,一個最直接直觀的好處就是能夠簡化代碼的書寫,這不就是語法糖麼
 
針對 Lambda表達式也有類型推斷
Java編譯器能夠根據  上下文(目標類型)推斷出用什麼函數式接口來配合Lambda表達式
而後就能夠獲取到函數接口對應的函數描述符也就是那個抽象方法的方法簽名
編譯器能夠了解Lambda表達式的參數類型,這樣就能夠在Lambda語法中省去標註參數類型
 
 
好比剛纔的篩選 1~9之中小於5的數值的例子中,就能夠有以下幾種寫法

.filter((Integer i) -> { return i.compareTo(5) < 0;}).collect(Collectors.toList()); 測試

.filter((Integer i) ->i.compareTo(5) < 0).collect(Collectors.toList()); ui

.filter(i ->i.compareTo(5) < 0).collect(Collectors.toList());

 
 

如何使用函數式接口

 
函數式接口定義了函數的類型   有了類型就如同其餘類型 好比 int 同樣  
你能夠定義變量
你能夠傳遞參數
你能夠返回
 
一個函數方法有方法簽名和方法體兩部份內容組成
函數接口只是有了方法簽名
方法體由函數式接口的實例傳遞(也就是Lambda表達式-匿名函數   方法引用 構造方法引用 )
具體的調用則是調用抽象方法  抽象方法的方法體就是函數式接口的實例
好比:
定義一個函數式接口,也可使用預置的 好比 Predicate等
image_5b791352_7e92
 
而後就是定義變量 使用Lambda實例化
再接着就是方法調用
image_5b791352_67c
image_5b791352_dfa
相關文章
相關標籤/搜索