Java8 函式介面
函式介面
在函數語言程式設計中,純函式 的定義是:
- 此函式在相同的輸入值時,需產生相同的輸出。函式的輸出和輸入值以外的其他隱藏資訊或狀態無關,也和由 I/O 裝置產生的外部輸出無關。
- 該函式不能有語義上可觀察的函式副作用,諸如“觸發事件”,使輸出裝置輸出,或更改輸出值以外物件的內容等。
為使 Java 支援函數語言程式設計,函式介面
(Functional Interfaces
)正是 Java 8 引入的一個核心概念。函式介面要求介面中只能定義個唯一一個抽象方法
。同時,引入一個新的註解:@FunctionalInterface
,作為函式介面的標記。
Java 8 提供了四大類函式介面:謂詞
Predicate
,函式
Function
,提供者
Supplier
,消費者
Consumer
。以及衍生的函式介面,比如BiFunction<T, U, R>
支援兩個入參的函式等等,它們都定義在java.util.function
包下。
Lambda 表示式
函式式介面的重要屬性是:我們能夠使用 Lambda 例項化它們 。Lambda 表示式的引入給開發者帶來了不少優點:在 Java 8 之前,匿名內部類,監聽器和事件處理器的使用都顯得很冗長,程式碼可讀性很差,Lambda 表示式的應用則使程式碼變得更加緊湊,可讀性增強;Lambda 表示式使並行操作大集合 變得很方便,可以充分發揮多核 CPU 的優勢,更易於為多核處理器編寫程式碼。
Lambda 表示式由三個部分組成:第一部分為一個括號內用逗號分隔的函式入參;第二部分為箭頭符號->
;第三部分為方法體,可以是表示式和程式碼塊。語法如下:
-
(parameters) ‑> expression
方法體為表示式,該表示式的值作為返回值返回。 -
(parameters) ‑> { statements; }
方法體為程式碼塊,必須用{}
包裹起來,若該函式需要返回值則需要return
。
用 Lambda 表示式替換匿名內部類的寫法如下。再進一步,可以使用方法引用 (Method Reference )簡寫
// Anonymous inner class Optional.of("abc").map(new Function<String, Integer>() { @Override public Integer apply(String s) { return s.length(); } }); // Lambda Optional.of("abc").map(s -> s.length()); // Method reference Optional.of("abc").map(String::length);
使用parallelStream()
和 Lambda 表示式並行操作集合,預設執行緒數為 CPU 核數
urls.parallelStream() .filter(url -> url.endsWith(".html")) .map(url -> htmlParser.contentFrom(url)) .reduce((x, y) -> x + y) .orElse("");
謂詞 Predicate
在計算機語言的環境下,謂詞是指條件表示式的求值返回真或假的過程。在 Java 中就是進行邏輯判斷後返回值為boolean
的函式,對應函式介面為Predicate<T>
。在針對集合進行篩選時,通常會使用謂詞對集合元素進行條件判斷。
@FunctionalInterface public interface Predicate<T> { boolean test(T t); default Predicate<T> and(Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); } default Predicate<T> negate() { return (t) -> !test(t); } default Predicate<T> or(Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); } static <T> Predicate<T> isEqual(Object targetRef) { return (null == targetRef) ? Objects::isNull : object -> targetRef.equals(object); } }
Predicate<T>
介面定義的唯一抽象方法test(T t)
用以驗證輸入引數是否滿足謂詞條件。同時,它還提供了預設方法用以支援組合的與、或、非邏輯運算
。
Predicate<Apple> p1 = p ‑> p.getColor() == Color.RED; Predicate<Apple> p2 = p ‑> p.getSize() == Size.Medium; apples.stream().filter(p1.and(p2));
函式 Function
Function<T, R>
介面代表一個函式,準確地說,代表了具有輸入輸出的函式,這也是最常見的函式定義。具有兩個入參的函式對應介面為BiFunction<T, U, R>
。
/** * @param <T> the type of the input to the function * @param <R> the type of the result of the function */ @FunctionalInterface public interface Function<T, R> { R apply(T t); default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } static <T> Function<T, T> identity() { return t -> t; } }
函式也可以進行組合,組合後返回的結果也是一個函式,這就是所謂的組合子 。組合子方法分別為:
-
compose(before)
,組合前一個函式,相當於V -> T -> R
-
andThen(after)
,組合後一個函式,相當於T -> R -> V
提供者 Supplier
當一個函式僅僅用於返回結果時,我們可以將這種函式稱為提供者
,介面定義為Supplier<T>
。
@FunctionalInterface public interface Supplier<T> { T get(); }
只提供一個get()
方法,我們可以將這種提供者代表的函式看做是一種 Query 操作。
消費者 Consumer
與提供者對應的介面則被稱之為消費者
,用以接收一個輸入,但卻並不關心返回結果,即返回值為void
。消費者的對應介面為Consumer<T>
。
/** * Represents an operation that accepts a single input argument and returns no * result. Unlike most other functional interfaces, {@code Consumer} is expected * to operate via side-effects. */ @FunctionalInterface public interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
不像其他函式介面,Consumer
通常是有副作用的
。這也不難理解,該函式只有輸入沒有輸出,那資料通常是被外部系統消費了。同時Consumer
支援andThen(after)
組合
Consumer<String> c1 = s -> System.out.println(s); Consumer<String> c2 = s -> storeToDB(s); Optional.of("abc").ifPresent(c1.andThen(c2));
這種消費者代表的函式相當於是一個 Command 操作。軟體設計的一條原則就是CQS (Command-Query Separation)命令-查詢分離原則,即保證一個方法體內只有 Query 和 Command 的一種,就是因為查詢無副作用而命令可能會改變資料狀態。