函數式接口詳細定義
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 包
接口 抽象方法
|
java.util.function.Predicate<T>
斷言 也就是條件測試器 接收條件,進行測試
接口定義了一個名叫test的抽象方法,它接受泛型T對象,並返回一個boolean。
test (條件測試) , and-or- negate(與或非) 方法
|
java.util.function.Consumer<T>
消費者 消費數據 接收參數,返回void 數據被消費了
定義了一個名叫accept的抽象方法,它接受泛型T的對象,沒有返回(void)
你如果需要訪問類型T的對象,並對其執行某些操作,就可以使用這個接口
|
java.util.function.Function<T, R>
函數 有輸入有輸出 數據轉換功能
接口定義了一個叫作apply的方法,它接受一個泛型T的對象,並返回一個泛型R的對象。
|
java.util.function.Supplier<T> 提供者 不需要輸入,產出T 提供數據 無參構造方法 提供T類型對象 |
接口中的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]
為什么要有基本類型擴展
只有對象類型才能作為泛型參數,對於基本類型就涉及到裝箱拆箱的操作,雖然是自動的
但是這不可避免給內存帶來了額外的開銷,裝箱和拆箱都會帶來開銷
所以為了減小這些性能開銷 對基本類型進行類型擴展
Stream 類的某些方法對基本類型和裝箱類型做了區分
Java 8中,僅對 整型、長整型和雙浮點型做了特殊處理 因為它們在數值計算中用得最多
對基本類型做特殊處理的方法在命名上有明確的規范
- 如果參數是基本類型,則不加前綴只需類型名即可
- 如果方法返回類型為基本類型,則在基本類型前再加上一個 To
總結一句話:
加了類型前綴[Int|Double|Long] 表示參數是基本類型, 如果在此基礎上又加上了To 表示返回類型是基本類型 |
如有可能,應盡可能多地使用對基本類型做過特殊處理的方法,進而改善性能
函數式接口的實例
函數式接口的實例可以通過 lambda表達式 方法引用 或者構造方法引用進行表示
Lambda表達式
可以把Lambda表達式理解為簡潔地表示可傳遞的匿名函數的一種方式,也就是用來表示匿名函數
它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。
特點
- 匿名——我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫得少而想得多!
- 函數——我們說它是函數,是因為Lambda函數不像方法那樣屬於某個特定的類。但和方法一樣,Lambda有參數列表、函數主體、返回類型,還可能有可以拋出的異常列表。
- 傳遞——Lambda表達式可以作為參數傳遞給方法或存儲在變量中。
- 簡潔——無需像匿名類那樣寫很多模板代碼。
基本語法
Lambda的基本語法是
(parameters) -> expression |
或(請注意語句的花括號)
(parameters) -> { statements; } |
Lambda表達式三個部分
- 參數列表
- 箭頭 ( -> 把參數列表與Lambda主體分隔開)
- 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 表達式中引用它,編譯器就會報錯
比如:
無需設置final 一切運行正常
一旦給hello變量重新賦值 ,編譯器將會報錯
方法引用
方法引用讓你可以重復使用現有的方法定義 並像Lambda一樣傳遞它們
方法引用使用 :: 來表示
方法引用主要有三類
(1) 指向靜態方法的方法引用(例如Integer的parseInt方法, 寫作Integer::parseInt)
也就是靜態方法作用於對象上
示例:字符串轉換為數值
(2)指向 任意類型實例方法 的方法引用(例如 String 的 length 方法,寫作String::length)
你在引用一個對象的方法,而這個對象本身是Lambda的一個參數。例如,Lambda表達式(String s) -> s.toUppeCase() 可以寫作String::toUpperCase
示例:打印字符串的長度 1個 3個 2個 (沒有空格和換行所以擠在一起了)
(3) 指向現有對象的實例方法的方法引用
比如lambda表達式中調用字符串helloString的charAt()方法 helloString就是一個現有對象
示例:獲取字符串位於給定序列的charAt值
構造函數引用
對於一個現有構造函數,你可以利用它的名稱和關鍵字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(); } }
可以看到分別調用了,無參構造方法 一個參數構造方法以及兩個參數構造方法
如果三個構造方法如何設置呢?
我們只需要定義函數接口即可
再次運行
類型檢查與類型推斷
類型檢查
我們知道當我們操作賦值運算時會有類型檢查
比如:
那么對於函數式接口與函數值呢
函數式接口 變量名 = Lambda-匿名函數/方法引用/構造方法引用; |
那么函數作為值是如何進行類型檢查的?
Lambda的類型是從使用Lambda的上下文推斷出來的
上下文中Lambda表達式需要的類型稱為目標類型
上下文是比如接受它傳遞的方法的形式參數,或接受它的值的局部變量
形式參數或者局部變量都會有類型的定義與聲明
比如篩選 1~9之中小於5的數值
List<Integer> listNum = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List filteredNum = listNum.stream().filter(i -> i.compareTo(5) < 0).collect(Collectors.toList());
System.out.println(filteredNum);
這個示例中接收 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<>();
// Predicate返回了一個boolean
Predicate<String> p = s -> list.add(s);
// 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());
.filter(i ->i.compareTo(5) < 0).collect(Collectors.toList());
如何使用函數式接口
函數式接口定義了函數的類型 有了類型就如同其他類型 比如 int 一樣
你可以定義變量
你可以傳遞參數
你可以返回
一個函數方法有方法簽名和方法體兩部分內容組成
函數接口只是有了方法簽名
方法體由函數式接口的實例傳遞(也就是Lambda表達式-匿名函數 方法引用 構造方法引用 )
具體的調用則是調用抽象方法 抽象方法的方法體就是函數式接口的實例
比如:
定義一個函數式接口,也可以使用預置的 比如 Predicate等
然后就是定義變量 使用Lambda實例化
再接着就是方法調用