一、函數接口
| 接口 | 參數 | 返回類型 | 描述 |
|---|---|---|---|
| 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 表達式和匿名內部類
先來復習一下匿名內部類的知識:
- 如果是接口,相當於在內部返回了一個接口的實現類,並且實現方式是在類的內部進行的;
- 如果是普通類,匿名類相當於繼承了父類,是一個子類,並可以重寫父類的方法。
- 需要特別注意的是,匿名類沒有名字,不能擁有一個構造器。如果想為匿名類初始化,讓匿名類獲得一個初始化值,或者說,想使用匿名內部類外部的一個對象,則編譯器要求外部對象為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 在推斷類型時,會挑出最具體的類型。
- 如果只有一個可能的目標類型, 由相應函數接口里的參數類型推導得出;
- 如果有多個可能的目標類型, 由最具體的類型推導得出;
- 如果有多個可能的目標類型且最具體的類型不明確, 則需人為指定類型。
三、Lambda 表達式和集合
java8 在 java.util 包中引入了一個新的類 —— Stream.java。java8 之前我們迭代集合,都只能依賴外部迭代器 Iterator 對集合進行串行化處理。而 Stream 支持對集合順序和並行聚合操作,將更多的控制權交給集合類,是一種內部迭代方式。這有利於方便用戶寫出更簡單的代碼,明確要達到什么轉化,而不是如何轉化。
Stream 的操作有兩種,一種是描述 Stream ,如 filter、map、peek 等最終不產生結果的行為稱為"惰性求值";另外一種像 foreach、collect 等是從 Stream 中產生結果的行為稱為"及早求值"。
接下來讓我們瞧瞧 Stream 如何結合 Lambda 表達式優雅的處理集合...
1、及早求值
收集器:一種通用的、從流生成復雜值的結構。只要將它傳給 collect 方法, 所有的流就都可以使用它了。在 java.util.stream.Collectors 中提供了一些有用的收集器。比如 toList、toSet、toMap 等。
在一個有序集合中創建一個流時,流中的元素就按出現順序排列;如果集合本身就是無序的,由此生成的流也是無序的。需要注意的是,forEach 方法不能保證元素是按順序處理的,如果需要保證按順序處理,應該使用forEachOrdered 方法。當然,我們可以使用 sorted 方法對Stream 中的元素進行自定義排序。
foreach/forEachOrdered - 迭代集合
list.forEach(e -> System.out.println(e));
map.forEach((k, v) -> {
System.out.println(k);
System.out.println(v);
});
對 Stream 進行自定義排序
List<String> collectSort = collect.stream().sorted(Comparator.comparing(String::length)).collect(Collectors.toList());
allMatch、anyMatch、noneMatch - 檢查元素是否匹配
private boolean isPrime(int number) {
return IntStream.range(2, number)
.allMatch(x -> (number % x) != 0);
}
collect(toList()) - 由Stream里的值生成一個 List/Set/自定義 集合
List<String> list = Stream.of("java", "C++", "Python").collect(Collectors.toList());
等價於:
List<String> asList = Arrays.asList("java", "C++", "Python");
Set<String> set = Stream.of("java", "python", "php").collect(Collectors.toSet());
TreeSet<String> treeSet = Stream.of("java", "python", "php").collect(Collectors.toCollection(() -> new TreeSet<>()));
collect(toMap()) - 由Stream里的值生成一個 Map 集合
使用 toMap() 需要注意的是:Map中的key不能重復,如果重復的話,會拋出異常,因為 JVM 弄不清楚我是用新的Value、還是要用舊的Value呢?所以代碼寫成了如下的樣子~~
Map<String, String> strMap = Stream.of("java", "python", "php").collect(Collectors.toMap(String::new, String::new, (oldValue,newValue) -> oldValue));
如上使用 toMap() 仍然會有一個問題,就是 toMap 轉化的時候,如果 value 為 null,會報一個 NullPointerException ,可用如下方式解決:
Map<Object, Object> mapResult = Stream.of("java", "python", "php").collect(HashMap::new, (map, str) -> map.put(str, str), HashMap::putAll);
collect(maxBy())、collect(minBy())、collect(averagingInt()) - 求值操作
Optional<String> optionalMaxBy = Stream.of("java", "python", "php").collect(Collectors.maxBy(Comparator.comparing(str -> str.length())));
System.out.println(optionalMaxBy.get());
Optional<String> optionalMinBy = Stream.of("java", "python", "php").collect(Collectors.minBy(Comparator.comparing(str -> str.length())));
System.out.println(optionalMinBy.get());
Double aDouble = Stream.of("java", "python", "php").collect(Collectors.averagingInt(String::length));
System.out.println(aDouble);
collect(Collectors.joining()) - 字符串拼接操作
String joinStr = Stream.of("java", "python", "php").collect(Collectors.joining(",", "[", "]"));
joining() 的三個參數依次為 分隔符、前綴、后綴。
collect(partitioningBy())、collect(groupingBy()) - 聚合統計操作
List<String> collect = Stream.of("java", "python", "php").collect(Collectors.toList());
// 數據分堆,按照 Boolean 值,將數據分成兩堆
Map<Boolean, List<String>> listMap = collect.stream().collect(Collectors.partitioningBy(str -> str.equals("java")));
// 數據分組,有點像 SQL 的 GROUP BY 用法,按照對象的某個屬性分組
Map<Integer, List<String>> collectGroudBy = collect.stream().collect(Collectors.groupingBy(String::hashCode));
// 分組統計(類似SQL分組統計) - 先將集合分組,然后統計分組的值 - 比如計算每個城市的姓氏集合-> 先按城市分組,再計算姓氏的集合。
Map<Integer, Long> longMap = collect.stream().collect(Collectors.groupingBy(String::length, Collectors.counting()));
Map<Integer, List<Integer>> listMap = collect.stream().collect(Collectors.groupingBy(String::length, Collectors.mapping(String::length, Collectors.toList())));
2、惰性求值
range - 以步長為1的循環
private boolean isPrime(int number) {
return IntStream.range(2, number)
.allMatch(x -> (number % x) != 0);
}
等價於:
for (int i = 2; i < number ; i++) { ... }
filter - 遍歷並檢查過濾其中的元素
long count = list.stream().filter(x -> "java".equals(x)).count();
distinct - 對流中的元素去重
流中的元素去重根據的是對象的 equal() 方法,對於有序列的流,相同的元素以第一個為准;對於無序列的流,去重的穩定性不做保證。
Stream.of("java", "python", "php", "java").distinct().forEach(e -> System.out.println(e));
findAny、findFirst - 返回一個流中的元素
Optional<String> first = Stream.of("java", "python", "php").findFirst();
Optional<String> any = Stream.of("java", "python", "php").findAny();
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 而已。
peek - 逐個處理流中的元素,無返回值
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
peek 和 map 的區別在於:peek 接受的是 Consumer 高階函數,無返回值;map 接受的是 Function 高階函數,有返回值。
flatMap - 將多個 Stream 連接成一個 Stream
List<String> streamList = Stream.of(list, asList).flatMap(x -> x.stream()).collect(Collectors.toList());
flatMap 方法的相關函數接口和 map 方法的一樣, 都是 Function 接口, 只是方法的返回值限定為 Stream 類型罷了。
reduce - 聚合操作,從一組元素中生成一個值,sum()、max()、min()、count() 等都是reduce操作,將他們單獨設為函數只是因為常用
Integer sum1 = Stream.of(1, 2, 3).reduce(0, (acc, e) -> acc + e);
String maxStr = list.stream().max(Comparator.comparing(e -> e.length())).get();
String minStr = list.stream().min(Comparator.comparing(e -> e.length())).get();
上述執行求和操作,有兩個參數: 傳入 Stream 中初始值和 acc。 將兩個參數相加,acc 是累加器,保存着當前的累加結果。
3、Stream 的並行操作
在 Java8 中,編寫並行化的程序很容易。並行化操作流只需改變一個方法調用。如果已經有一個Stream對象,調用它的 parallel 方法就能讓其擁有並行操作的能力。如果想從一個集合類創建一個流,調用 parallelStream 就能立即獲得一個擁有並行能力的流。在底層,並行流還是沿用了 fork/join 框架。fork 遞歸式地分解問題,然后每段並行執行,最終由 join 合並結果,返回最后的值。
List<String> paraList = Stream.of("java", "php", "python").parallel().collect(Collectors.toList());
List<String> resultList = paraList.parallelStream().collect(Collectors.toList());
需要注意的是:在要對流求值時,不能同時處於兩種模式,要么是並行的,要么是串行的。如果同時調用了 parallel 和 sequential 方法,最后調用的那個方法起效。
並行化流操作的用武之地是使用操作處理大量數據。在處理少量數據時,效果並不明顯,因為要把時間花銷在數據的分塊上。
影響並行流性能的五要素是:數據大小、源數據結構、值是否裝箱、可用的 CPU 核數量以及處理每個元素所花的時間。
| 性能 | 例子 | 描述 |
|---|---|---|
| 好 | ArrayList、數組或IntStream.range | 這些數據結構支持隨機讀取,也就是說它們能輕而易舉地被任意分解。 |
| 一般 | HashSet、TreeSet | 這些數據結構不易公平地被分解,但是大多數時候分解是可能的。 |
| 差 | LinkedList、Streams.iterate和BufferedReader.lines | 數據結構難以分解或是長度未知,很難預測該在哪里分解。 |
在討論流中單獨操作每一塊的種類時,可以分成兩種不同的操作:無狀態的和有狀態的。無狀態操作整個過程中不必維護狀態,有狀態操作則有維護狀態所需的開銷和限制。如果能避開有狀態,選用無狀態操作,就能獲得更好的並行性能。無狀態操作包括 map、filter 和 flatMap,有狀態操作包括 sorted、distinct 和 limit。
四、默認方法
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 是用函數式編程方式在集合類上進行復雜操作的工具。
- 對於需要大量數值運算的算法來說, 裝箱和拆箱的計算開銷, 以及裝箱類型占用的額外內存, 會明顯減緩程序的運行速度。為了減小這些性能開銷, Stream 類的某些方法對基本類型和裝箱類型做了區分。比如 IntStream、LongStream 等。
- Java8 對為 null 的字段也引進了自己的處理,既不用一直用 if 判斷對象是否為 null。但是需要注意的是:使用任何像 Optional 的類型作為字段或方法參數都是不可取的. Optional 只設計為類庫方法的, 可明確表示可能無值情況下的返回類型. Optional 類型不可被序列化, 用作字段類型會出問題的。
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();
}
- java8 對Map集合中value為 null 的情況也引起了自己的處理,利用 computeIfAbsent 方法,當 get()方法得到的value為 null 時,會把新的value放進集合並返回。
Map<String, String> map = new HashMap<>(8);
map.put("java", "java");
map.put("php", "php");
String python = map.computeIfAbsent("python", k -> k.toUpperCase());
- 從某種角度來說,大量代碼塞進一個方法會讓可讀性變差是決定如何使用 Lambda 表達式的黃金法則。
- 在過去十年中,人們批評單例模式讓程序變得更脆弱,且難於測試。敏捷開發的流行,讓測試顯得更加重要,單例模式的這個問題把它變成了一個反模式:一種應該避免使用的模式。
- 軟件開發最重要的設計工具不是什么技術,而是一顆在設計原則方面訓練有素的頭腦。
