一、函數介面 |介面|參數|返回類型|描述| |: :|: :|: :|: :| |Predicate<T>|T|boolean|用來比較操作| |Consumer<T>|T|void|沒有返回值的函數| |Function|T|R|有返回值的函數| |Supplier< ...
一、函數介面
介面 | 參數 | 返回類型 | 描述 |
---|---|---|---|
Predicate<T> | T | boolean | 用來比較操作 |
Consumer<T> | T | void | 沒有返回值的函數 |
Function<T, R> | T | R | 有返回值的函數 |
Supplier<T> | None | T | 工廠方法-返回一個對象 |
UnaryOperator<T> | T | T | 入參和出參都是相同對象的函數 |
BinaryOperator<T> | (T,T) | T | 求兩個對象的操作結果 |
為什麼要先從函數介面說起呢?因為我覺得這是 java8 函數式編程的入口呀!每個函數介面都帶有 @FunctionalInterface 註釋,有且僅有一個未實現的方法,表示接收 Lambda 表達式,它們存在的意義在於將代碼塊作為數據打包起來。
沒有必要過分解讀這幾個函數介面,完全可以把它們看成普通的介面,不過他們有且僅有一個抽象方法(因為要接收 Lambda 表達式啊)。
@FunctionalInterface 該註釋會強制 javac 檢查一個介面是否符合函數介面的標準。 如果該註釋添加給一個枚舉類型、 類或另一個註釋, 或者介面包含不止一個抽象方法, javac 就會報錯。
二、Lambda 表達式
1、Lambda 表達式和匿名內部類
先來複習一下匿名內部類的知識:
- 如果是介面,相當於在內部返回了一個介面的實現類,並且實現方式是在類的內部進行的;
- 如果是普通類,匿名類相當於繼承了父類,是一個子類,並可以重寫父類的方法。
- 需要特別註意的是,匿名類沒有名字,不能擁有一個構造器。如果想為匿名類初始化,讓匿名類獲得一個初始化值,或者說,想使用匿名內部類外部的一個對象,則編譯器要求外部對象為final屬性,否則在運行期間會報錯。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(123);
}
}).start();
new Thread(()-> System.out.println(123)).start();
如上,和傳入一個實現某介面的對象不同, 我們傳入了一段代碼塊 —— 一個沒有名字的函數。() 是參數列表, 和上面匿名內部類示例中的是一樣的。 -> 將參數和 Lambda 表達式的主體分開, 而主體是之後操作會運行的一些代碼。
Lambda 表達式簡化了匿名內部類的寫法,省略了函數名和參數類型。即參數列表 () 中可以僅指定參數名而不指定參數類型。
Java 是強類型語言,為什麼可以不指定參數類型呢?這得益於 javac 的類型推斷機制,編譯器能夠根據上下文信息推斷出參數的類型,當然也有推斷失敗的時候,這時就需要手動指明參數類型了。javac 的類型推斷機制如下:
- 對於類中有重載的方法,javac 在推斷類型時,會挑出最具體的類型。
- 如果只有一個可能的目標類型, 由相應函數介面里的參數類型推導得出;
- 如果有多個可能的目標類型, 由最具體的類型推導得出;
- 如果有多個可能的目標類型且最具體的類型不明確, 則需人為指定類型。
2、Lambda 表達式和集合
java8 在 java.util 包中引入了一個新的類 —— Stream.java。java8 之前我們迭代集合,都只能依賴外部迭代器 Iterator 對集合進行串列化處理。而 Stream 支持對集合順序和並行聚合操作,將更多的控制權交給集合類,是一種內部迭代方式。這有利於方便用戶寫出更簡單的代碼,明確要達到什麼轉化,而不是如何轉化。
Stream 的操作有兩種,一種是描述 Stream ,如 filter、map 等最終不產生結果的行為稱為"惰性求值";另外一種像 foreach、collect 等是從 Stream 中產生結果的行為稱為"及早求值"。
接下來讓我們瞧瞧 Stream 如何結合 Lambda 表達式優雅的處理集合...
- foreach - 迭代集合
list.forEach(e -> System.out.println(e));
map.forEach((k, v) -> {
System.out.println(k);
System.out.println(v);
});
- collect(toList()) - 由Stream里的值生成一個列表。
List<String> list = Stream.of("java", "C++", "Python").collect(Collectors.toList());
等價於:
List<String> asList = Arrays.asList("java", "C++", "Python");
- filter - 遍歷並檢查過濾其中的元素。
long count = list.stream().filter(x -> "java".equals(x)).count();
- map、mapToInt、mapToLong、mapToDouble - 將流中的值轉換成一個新的值。
List<String> mapList = list.stream().map(str -> str.toUpperCase()).collect(Collectors.toList());
List<String> list = Stream.of("java", "javascript", "python").collect(Collectors.toList());
IntSummaryStatistics intSummaryStatistics = list.stream().mapToInt(e -> e.length()).summaryStatistics();
System.out.println("最大值:" + intSummaryStatistics.getMax());
System.out.println("最小值:" + intSummaryStatistics.getMin());
System.out.println("平均值:" + intSummaryStatistics.getAverage());
System.out.println("總數:" + intSummaryStatistics.getSum());
mapToInt、mapToLong、mapToDouble 和 map 操作類似,只是把函數介面的返回值改為 int、long、double 而已。
- flatMap - 將多個 Stream 連接成一個 Stream
List<String> streamList = Stream.of(list, asList).flatMap(x -> x.stream()).collect(Collectors.toList());
flatMap 方法的相關函數介面和 map 方法的一樣, 都是 Function 介面, 只是方法的返回值限定為 Stream 類型罷了。
- Max-求最大值、Min-求最小值
String maxStr = list.stream().max(Comparator.comparing(e -> e.length())).get();
String minStr = list.stream().min(Comparator.comparing(e -> e.length())).get();
- reduce - 聚合操作,從一組元素中生成一個值,sum()、max()、min()、count() 等都是reduce操作,將他們單獨設為函數只是因為常用。
Integer sum1 = Stream.of(1, 2, 3).reduce(0, (acc, e) -> acc + e);
上述執行求和操作,有兩個參數: 傳入 Stream 中初始值和 acc。 將兩個參數相加,acc 是累加器,保存著當前的累加結果。
- 待續...
三、預設方法
java8 中新增了 Stream 操作,那麼第三方類庫中的自定義集合 MyList 要怎麼做到相容呢?總不能升級完 java8,第三方類庫中的集合實現全都不能用了吧?
為此,java8 在介面中引入了"預設方法"的概念!預設方法是指介面中定義的包含方法體的方法,方法名有 default 關鍵字做首碼。預設方法的出現是為了 java8 能夠向後相容。
public interface Iterable<T> {
/**
* Performs the given action for each element of the {@code Iterable}
* until all elements have been processed or the action throws an
* exception. Unless otherwise specified by the implementing class,
* actions are performed in the order of iteration (if an iteration order
* is specified). Exceptions thrown by the action are relayed to the
* caller.
*
* @implSpec
* <p>The default implementation behaves as if:
* <pre>{@code
* for (T t : this)
* action.accept(t);
* }</pre>
*
* @param action The action to be performed for each element
* @throws NullPointerException if the specified action is null
* @since 1.8
*/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
看 java8 中的這個 Iterable.java 中的預設方法 forEach(Consumer<? super T> action),表示“如果你們沒有實現 forEach 方法,就使用我的吧”。
預設方法除了添加了一個新的關鍵字 default,在繼承規則上和普通方法也略有差別:
- 類勝於介面。如果在繼承鏈中有方法體或抽象的方法聲明,那麼就可以忽略介面中定義的方法。
- 子類勝於父類。果一個介面繼承了另一個介面, 且兩個介面都定義了一個預設方法,那麼子類中定義的方法勝出。
- 如果上面兩條規則不適用, 子類要麼需要實現該方法, 要麼將該方法聲明為抽象方法。
四、其他
- 使用 Lambda 表達式,就是將複雜性抽象到類庫的過程。
- 面向對象編程是對數據進行抽象, 而函數式編程是對行為進行抽象。
- Java8 雖然在匿名內部類中可以引用非 final 變數, 但是該變數在既成事實上必須是final。即如果你試圖給該變數多次賦值, 然後在 Lambda 表達式中引用它, 編譯器就會報錯。
- Stream 是用函數式編程方式在集合類上進行複雜操作的工具。
- 無論何時,將 Lambda 表達式傳給 Stream 上的高階函數,都應該儘量避免副作用。唯一的例外是 forEach 方法,它是一個終結方法。沒有副作用指的是:函數不會改變程式或外界的狀態。
- 對於需要大量數值運算的演算法來說, 裝箱和拆箱的計算開銷, 以及裝箱類型占用的額外記憶體, 會明顯減緩程式的運行速度。為了減小這些性能開銷, Stream 類的某些方法對基本類型和裝箱類型做了區分。比如 IntStream、LongStream 等。
- Java8 對為 null 的欄位也引進了自己的處理,既不用一直用 if 判斷對象是否為 null,來看看?
public static List<AssistantVO> getAssistant(Long tenantId) {
// ofNullable 如果 value 為null,會構建一個空對象。
Optional<List<AssistantVO>> assistantVO = Optional.ofNullable(ASSISTANT_MAP.get(tenantId));
// orElse 如果 value 為null,選擇預設對象。
assistantVO.orElse(ASSISTANT_MAP.get(DEFAULT_TENANT));
return assistantVO.get();
}