函數式接口
什麼是函數式接口?
函數式接口,@FunctionalInterface,簡稱FI,簡單的說,FI就是指僅含有一個抽象方法的接口,以@Functionalnterface标注,這裡的抽象方法指的是該接口自己特有的抽象方法,而不包含它從其上級繼承過來的抽象方法,例如:
@FunctionalInterface
public interface ApplePredicate {
boolean test(Apple apple);
}
函數接口有哪些?
Java 7 中已經存在的函數式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.beans.PropertyChangeListener
除此之外,Java 8中增加了一個新的包:java.util.function,它裡面包含了常用的函數式接口,例如:
- Predicate < T > ——接收 T 并傳回 boolean
- Consumer< T >——接收 T,不傳回值
- Function< T, R >——接收 T,傳回 R
- Supplier< T >——提供 T 對象(例如工廠),不接收值
- UnaryOperator< T >——接收 T 對象,傳回 T
- BinaryOperator< T >——接收兩個 T,傳回 T
除了上面的這些基本的函數式接口,我們還提供了一些針對原始類型(Primitive type)的特化(Specialization)函數式接口,例如 IntSupplier 和 LongBinaryOperator。(我們隻為 int、long 和 double 提供了特化函數式接口,如果需要使用其它原始類型則需要進行類型轉換)同樣的我們也提供了一些針對多個參數的函數式接口,例如 BiFunction
如何定義自己的函數式接口?
在上面的ApplePredicate接口中可以看到我們使用了注解@FunctionalInterface來聲明這是一個函數式接口,但在實際開發過程中我們并不需要通過注解來聲明,編譯器會自動根據接口自行判斷該接口是否是一個函數式接口(判斷過程并非簡單的對接口方法計數:一個接口可能備援的定義了一個 Object 已經提供的方法,比如 toString(),或者定義了靜态方法或預設方法,這些都不屬于函數式接口方法的範疇),使用注解的好處在于顯示聲明該接口為函數式接口,防止其他開發人員往該接口添加其他方法。
接口介紹
java.util.function.* 接口中定義了多種不同的函數接口,這裡首先介紹三個泛型函數式接口:Predicate,Consumer,Function
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
java.util.function.Predicate 定義了一個test方法接受一個泛型傳回一個布爾型
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
java.util.function.Consumer 定義了一個accept方法接受一個泛型沒有傳回類型,如:
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T i : list) {
c.accept(i);
}
}
public static void main(String arg[]) {
forEach(Arrays.asList(,,,,,), integer -> {if (integer % == ) System.out.println(integer);});
}
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
java.util.function.Function< T, R >定義了一個apply方法接受一個泛型T傳回一個泛型R,如:
public static void main(String arg[]) {
List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), s -> s.length());
}
public static <T, R> List<R> map (List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<R>();
for (T s : list) {
result.add(f.apply(s));
}
return result;
}
原始類型特化
Java類型要麼是引用類型(比如Byte、Integer、Object、List),要麼是原始類型(比如int、double、byte、char)。但是泛型(比如Consumer<T>中的T)隻能綁定到引用類型。這是由泛型内部的實作造成的。是以,在Java裡有一個将原始類型轉換為對應的引用類型的機制。這個機制叫做裝箱(boxing)。相反的操作,也就是将引用類型轉換為對應的原始類型,叫做拆箱(unboxing)。Java還有一個自動裝箱機制來幫助程式員執行這一任務:裝箱和拆箱是自動完成的。比如,這就是為什麼下面的代碼是有效的:
List<Integer> list = new ArrayList<>();
for (int i = ; i < ; i++){
list.add(i);
}
但是這在性能方面是要付出代價的。裝箱後的值本質上就是把原始類型包裝起來,并儲存在隊裡。是以,裝箱後的值需要更多的記憶體,并需要額外的記憶體搜尋來擷取包裝的原始值。
Java8為我們前面所說的函數式接口帶來了一個專門的版本,一遍在輸入和輸出都是原始類型時避免自動裝箱的操作。比如,使用IntPredicate就避免了對值1000進行裝箱操作,但要是用Predicate<Integer>就會把參數1000裝箱到一個Integer對象中:
public interface IntPredicate{
boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % == ;
evenNumbers.test();//無裝箱
Predicate<Integer> oddNumbers = (Integer i) -> i % == ;
oddNumbers.test();//裝箱
true
下表展示了Java 8中的常用函數式接口,
函數接口 | 函數描述符 | 原始類型特化 |
---|---|---|
Predicate< T > | T->boolean | IntPredicate,LongPredicate,DoublePredicate |
Consumer< T > | T->void | IntConsumer,LongConsumer, DoubleConsumer |
Function< T,R > | T->R | IntFunction< R >, |
IntToDoubleFunction, | ||
IntToLongFunction, | ||
LongFunction< R >, | ||
LongToDoubleFunction, | ||
LongToIntFunction, | ||
DoubleFunction< R >, | ||
ToIntFunction< T >, | ||
ToDoubleFunction, | ||
ToLongFunction | ||
Supplier< T > | ()->T | BooleanSupplier,IntSupplier, LongSupplier,DoubleSupplier |
UnaryOperator | T->T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate | (L,R)->boolean | |
BiConsumer | (T,U)->void | ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer |
BiFunction | (T,U)->R | ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction |
下表展示了Lambdas及函數式接口的例子
使用案例 | Lambda的例子 | 對應的函數式接口 |
---|---|---|
布爾表達式 | (List< String > list) -> list.isEmpty() | Predicate< List< String > > |
建立對象 | () -> new Apple(10) | Supplier< Apple > |
消費一個對象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer< Apple > |
從一個對象中選擇/提取 | (String s) -> s.length() | Function< String, Integer >或 ToIntFunction< String > |
合并兩個值 | (int a, int b) -> a * b | IntBinaryOperator |
比較兩個對象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator< Apple >或 BiFunction< Apple, Apple, Integer > 或 ToIntBiFunction< Apple, Apple > |
之前講到了雙冒号目标引用方法,這裡我們來看一些例子:
lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
如何建構方法引用
方法引用隻是Lamdba單一方法的文法糖。
方法引用主要有三類。
(1) 指向靜态方法的方法引用(例如Integer的parseInt方法,寫Integer::parseInt)。
(2) 指 向 任 意 類 型 實 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 寫 作String::length)。
(3) 指向現有對象的執行個體方法的方法引用(假設你有一個局部變量expensiveTransaction用于存放Transaction類型的對象,它支援執行個體方法getValue,那麼你就可以寫expensiveTransaction::getValue)。