天天看點

java8——函數式接口

函數式接口

什麼是函數式接口?

  函數式接口,@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)。