一、為什麼引入函式式介面
作為Java函數語言程式設計愛好者,我們都知道方法引用和 Lambda 表示式都必須被賦值,同時賦值需要型別資訊才能使編譯器保證型別的正確性。
我們先看一個Lambda程式碼示例:
x -> x.toString()
我們清楚這裡返回型別必須是 String,但 x
是什麼型別呢?
Lambda 表示式包含型別推導(編譯器會自動推匯出型別資訊,避免了程式設計師顯式地宣告),編譯器必須能夠以某種方式推匯出 x
的型別以生成正確的程式碼。
同樣方法引用也存在此問題,假設你要傳遞 System.out :: println
到你正在編寫的方法 ,你怎麼知道傳遞給方法的引數的型別?
為了解決上述問題,Java 8 引入了函式式介面,在 java.util.function
包,它包含一組介面,這些介面是 Lambda 表示式和方法引用的目標型別,每個介面只包含一個抽象方法,稱為函式式方法。只有確保介面中有且僅有一個抽象方法,Lambda表示式的型別資訊才能順利地進行推導。
二、如何使用函式式介面
在編寫介面時,可以使用 @FunctionalInterface
註解強制執行此函式式方法模式:
在介面上使用註解
@FunctionalInterface
,一旦使用該註解來定義介面,編譯器將會強制檢查該介面是否確實有且僅有一個抽象方法,否則將會報錯。@FunctionalInterface
public interface MyFunction {
/**
* 自定義的抽象方法
*/
void run();
}在函式式介面,有且僅有一個抽象方法,
Object
的public
方法除外@FunctionalInterface
public interface MyFunction {
/**
* 自定義的抽象方法
*/
void run();
/**
* Object的equals方法
* @param obj
* @return
*/
@Override
boolean equals(Object obj);
/**
* Object的toString方法
* @return
*/
@Override
String toString();
/**
* Object的hashCode方法
* @return
*/
@Override
int hashCode();
}在函式式介面中,我們可以使用
default
修飾符定義預設方法,使用static
修飾符定義靜態方法@FunctionalInterface
public interface MyFunction {
/**
* 自定義的抽象方法
*/
void run();
/**
* static修飾符定義靜態方法
*/
static void staticRun() {
System.out.println("介面中的靜態方法");
}
/**
* default修飾符定義預設方法
*/
default void defaultRun() {
System.out.println("介面中的預設方法");
}
}
為大家演示下自定義無泛型的函式式介面測試例項:
/**
* 自定義的無泛型函式式介面
*/
@FunctionalInterface
public interface MyFunction {
/**
* 自定義的抽象方法
* @param x
*/
void run(Integer x);
/**
* default修飾符定義預設方法
* @param x
*/
default void defaultMethod(Integer x) {
System.out.println("介面中的預設方法,接收引數是:" + x);
}
}
/**
* 測試類
*/
public class MyFunctionTest {
@Test
public void functionTest() {
test(6, (x) -> System.out.println("介面中的抽象run方法,接收引數是:" + x));
}
public void test(int n, MyFunction function) {
System.out.println(n);
function.defaultMethod(n);
function.run(n);
}
}輸出結果:
6
介面中的預設方法,接收引數是:6
介面中的抽象run方法,接收引數是:6為大家演示下自定義有泛型的函式式介面測試例項:
/**
* 自定義的有泛型函式式介面
*/
@FunctionalInterface
public interface MyFunctionGeneric<T> {
/**
* 轉換值
* @param t
* @return
*/
T convertValue(T t);
}
/**
* 測試類
*/
public class MyFunctionGenericTest {
@Test
public void convertValueTest() {
String result = toLowerCase((x) -> x.toLowerCase(), "ABC");
System.out.println(result);
}
public String toLowerCase(MyFunctionGeneric<String> functionGeneric, String value) {
return functionGeneric.convertValue(value);
}
}輸出結果:
abc
注意:作為引數傳遞 Lambda 表示式:為了將 Lambda 表示式作為引數傳遞,接收Lambda 表示式的引數型別必須是與該 Lambda 表示式相容的函式式介面 的型別。
三、Java8四大內建核心函式式介面
首先總覽下四大函式式介面的特點說明:
介面 | 引數型別 | 返回型別 | 方法 | 說明 |
---|---|---|---|---|
Consumer | T | void | void accept(T t) | 消費型介面,對型別T引數操作,無返回結果 |
Supplier | - | T | T get() | 供給型介面,創造T型別引數 |
Function | T | R | R apply(T t) | 函式型介面,對型別T引數操作,返回R型別引數 |
Predicate | T | boolean | boolean test(T t) | 斷言型介面,對型別T進行條件篩選操作 |
消費型介面
Consumer<T>
java.util.function.Consumer<T>
介面是消費一個資料,其資料型別由泛型決定。
介面原始碼:
package java.util.function;
import java.util.Objects;
@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); };
}
}
抽象方法: void accept(T t)
,接收並消費一個指定泛型的資料,無需返回結果。預設方法: default Consumer<T> andThen(Consumer<? super T> after)
,如果一個方法的引數和返回值全都是 Consumer 型別,那麼就可以實現效果:消費資料的時候,首先做一個操作,然後再做一個操作,實現組合
public class ConsumerTest {
/**
* 先計算總分,再計算平均分
*/
@Test
public void calculate() {
Integer[] fraction = new Integer[] { 65, 76, 85, 92, 88, 99 };
consumer(fraction, x -> System.out.println(Arrays.stream(x).mapToInt(Integer::intValue).sum()),
y -> System.out.println(Arrays.stream(y).mapToInt(Integer::intValue).average().getAsDouble()));
}
public void consumer(Integer[] fraction, Consumer<Integer[]> x, Consumer<Integer[]> y) {
x.andThen(y).accept(fraction);
}
}
輸出結果:
505
84.16666666666667
由於Consumer
的default
方法所帶來的巢狀呼叫(連鎖呼叫),對行為的抽象的函數語言程式設計理念,展示的淋漓盡致。
其他的消費型函式式介面彙總說明:
介面名稱 | 方法名稱 | 方法簽名 |
---|---|---|
DoubleConsumer | accept | (double) -> void |
IntConsumer | accept | (int) -> void |
LongConsumer | accept | (long) -> void |
ObjDoubleConsumer | accept | (T, double) -> void |
ObjIntConsumer | accept | (T, int) -> void |
ObjLongConsumer | accept | (T, long) -> void |
供給型介面
Supplier<T>
java.util.function.Supplier<T>
介面僅包含一個無參的方法: T get()
,用來獲取一個泛型引數指定型別的物件資料。
介面原始碼:
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
T get();
}
由於這是一個函式式介面,意味著對應的Lambda表示式需要對外提供一個符合泛型型別的物件資料。
public class SupplierTest {
public int getMax(Supplier<Integer> supplier) {
return supplier.get();
}
/**
* 獲取陣列元素最大值
*/
@Test
public void getMaxTest() {
Integer[] data = new Integer[] { 5, 4, 6, 3, 2, 1 };
int result = getMax(() -> {
int max = 0;
for (int i = 0; i < data.length; i++) {
max = Math.max(max, data[i]);
}
return max;
});
System.out.println(result);
}
}
其他的供給型函式式介面彙總說明:
介面名稱 | 方法名稱 | 方法簽名 |
---|---|---|
BooleanSupplier | getAsBoolean | () -> boolean |
DoubleSupplier | getAsDouble | () -> double |
IntSupplier | getAsInt | () -> int |
LongSupplier | getAsLong | () -> long |
函式型介面
Function
java.util.function.Function<T,R>
介面用來根據一個型別的資料得到另一個型別的資料,前者稱為前置條件,後者稱為後置條件。
介面原始碼:
package java.util.function;
import java.util.Objects;
@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;
}
}
抽象方法
apply(T t)
:該方法接收入參是一個泛型T物件,並返回一個泛型T物件。預設方法
andThen(Function<? super R, ? extends V> after)
:該方法接受一個行為,並將父方法處理過的結果作為引數再處理。compose(Function<? super V, ? extends T> before)
:該方法正好與andThen
相反,它是先自己處理然後將結果作為引數傳給父方法執行。@Test
public void andThenAndComposeTest() {
// 計算公式相同
Function<Integer, Integer> andThen1 = x -> x + 1;
Function<Integer, Integer> andThen2 = x -> x * 2;
Function<Integer, Integer> compose1 = y -> y + 1;
Function<Integer, Integer> compose2 = y -> y * 2;
// 注意呼叫的先後順序
// 傳入引數2後,先執行andThen1計算,將結果再傳入andThen2計算
System.out.println(andThen1.andThen(andThen2).apply(2));
// 傳入引數2後,先執行compose2計算,將結果再傳入compose1計算
System.out.println(compose1.compose(compose2).apply(2));
}輸出結果:
6
5靜態方法
identity()
:獲取到一個輸入引數和返回結果一樣的Function例項。
來一個自駕九寨溝的程式碼示例:
public class FunctionTest {
@Test
public void findByFunctionTest() {
Function<BigDecimal, BigDecimal> getMoney = m -> m.add(new BigDecimal(1000));
BigDecimal totalCost = getMoney.apply(new BigDecimal(500));
System.out.println("張三的錢包原本只有500元,自駕川西得去銀行再取1000元,取錢後張三錢包總共有" + Function.identity().apply(totalCost) + "元");
BigDecimal surplus = cost(totalCost, (m) -> {
System.out.println("第二天出發前發現油不足,加油前有" + m + "元");
BigDecimal lubricate = m.subtract(new BigDecimal(300));
System.out.println("加油300後還剩餘" + lubricate + "元");
return lubricate;
}, (m) -> {
System.out.println("到達景區門口,買景區票前有" + m + "元");
BigDecimal tickets = m.subtract(new BigDecimal(290));
System.out.println("買景區票290後還剩餘" + tickets + "元");
return tickets;
});
System.out.println("最後張三返程到家還剩餘" + surplus + "元");
}
public BigDecimal cost(BigDecimal money, Function<BigDecimal, BigDecimal> lubricateCost,
Function<BigDecimal, BigDecimal> ticketsCost) {
Function<BigDecimal, BigDecimal> firstNight = (m) -> {
System.out.println("第一晚在成都住宿前有" + m + "元");
BigDecimal first = m.subtract(new BigDecimal(200));
System.out.println("交完200住宿費還剩餘" + first + "元");
return first;
};
Function<BigDecimal, BigDecimal> secondNight = (m) -> {
System.out.println("第二晚在九寨縣住宿前有" + m + "元");
BigDecimal second = m.subtract(new BigDecimal(200));
System.out.println("交完200住宿費還剩餘" + second + "元");
return second;
};
return lubricateCost.andThen(ticketsCost).andThen(secondNight).compose(firstNight).apply(money);
}
}
輸出結果:
張三的錢包原本只有500元,自駕川西得去銀行再取1000元,取錢後張三錢包總共有1500元
第一晚在成都住宿前有1500元
交完200住宿費還剩餘1300元
第二天出發前發現油不足,加油前有1300元
加油300後還剩餘1000元
到達景區門口,買景區票前有1000元
買景區票290後還剩餘710元
第二晚在九寨縣住宿前有710元
交完200住宿費還剩餘510元
最後張三返程到家還剩餘510元
其他的函式型函式式介面彙總說明:
介面名稱 | 方法名稱 | 方法簽名 |
---|---|---|
BiFunction | apply | (T, U) -> R |
DoubleFunction | apply | (double) -> R |
DoubleToIntFunction | applyAsInt | (double) -> int |
DoubleToLongFunction | applyAsLong | (double) -> long |
IntFunction | apply | (int) -> R |
IntToDoubleFunction | applyAsDouble | (int) -> double |
IntToLongFunction | applyAsLong | (int) -> long |
LongFunction | apply | (long) -> R |
LongToDoubleFunction | applyAsDouble | (long) -> double |
LongToIntFunction | applyAsInt | (long) -> int |
ToDoubleFunction | applyAsDouble | (T) -> double |
ToDoubleBiFunction | applyAsDouble | (T, U) -> double |
ToIntFunction | applyAsInt | (T) -> int |
ToIntBiFunction | applyAsInt | (T, U) -> int |
ToLongFunction | applyAsLong | (T) -> long |
ToLongBiFunction | applyAsLong | (T, U) -> long |
斷言型介面
Predicate<T>
java.util.function.Predicate<T>
介面中包含一個抽象方法: boolean test(T t)
,用於條件判斷的場景。預設方法:and or nagte
(取反)。
介面原始碼:
package java.util.function;
import java.util.Objects;
@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
條件使用與邏輯連線起來實現並且的效果時,類始於 Consumer
介面 andThen()
函式 其他三個雷同。
public class PredicateTest {
/**
* 查詢在渝北的Jack
*/
@Test
public void findByPredicateTest() {
List<User> list = Lists.newArrayList(new User("Johnson", "渝北"), new User("Tom", "渝中"), new User("Jack", "渝北"));
getNameAndAddress(list, (x) -> x.getAddress().equals("渝北"), (x) -> x.getName().equals("Jack"));
}
public void getNameAndAddress(List<User> users, Predicate<User> name, Predicate<User> address) {
users.stream().filter(user -> name.and(address).test(user)).forEach(user -> System.out.println(user.toString()));
}
}
輸出結果:
User [name=Jack, address=渝北]
其他的斷言型函式式介面彙總說明:
介面名稱 | 方法名稱 | 方法簽名 |
---|---|---|
BiPredicate | test | (T, U) -> boolean |
DoublePredicate | test | (double) -> boolean |
IntPredicate | test | (int) -> boolean |
LongPredicate | test | (long) -> boolean |
四、總結
Lambda 表示式和方法引用並沒有將 Java 轉換成函式式語言,而是提供了對函數語言程式設計的支援。這對 Java 來說是一個巨大的改進,因為這允許你編寫更簡潔明瞭,易於理解的程式碼。