函數式編程
在正式學習Lambda之前,我們先來了解一下什么是函數式編程
我們先看看什么是函數。函數是一種最基本的任務,一個大型程序就是一個頂層函數調用若干底層函數,這些被調用的函數又可以調用其他函數,即大任務被一層層拆解並執行。所以函數就是面向過程的程序設計的基本單元。
Java不支持單獨定義函數,但可以把靜態方法視為獨立的函數,把實例方法視為自帶this
參數的函數。而函數式編程(請注意多了一個“式”字)——Functional Programming,雖然也可以歸結到面向過程的程序設計,但其思想更接近數學計算。
我們首先要搞明白計算機(Computer)和計算(Compute)的概念。在計算機的層次上,CPU執行的是加減乘除的指令代碼,以及各種條件判斷和跳轉指令,所以,匯編語言是最貼近計算機的語言。而計算則指數學意義上的計算,越是抽象的計算,離計算機硬件越遠。對應到編程語言,就是越低級的語言,越貼近計算機,抽象程度低,執行效率高,比如C語言;越高級的語言,越貼近計算,抽象程度高,執行效率低,比如Lisp語言。
函數式編程就是一種抽象程度很高的編程范式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之為沒有副作用。而允許使用變量的程序設計語言,由於函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。
函數式編程的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數!
函數式編程最早是數學家阿隆佐·邱奇研究的一套函數變換邏輯,又稱Lambda Calculus(λ-Calculus),所以也經常把函數式編程稱為Lambda計算。
Java平台從Java 8開始,支持函數式編程。
Lambda初體驗
先從一個例子開始,讓我們來看一下Lambda可以用在什么地方。
例一:創建線程
常見創建線程的方法(JDK1.8以前)
//JDK1.7通過匿名內部類的方式創建線程
Thread thread = new Thread(new Runnable() {
@Override
public void run() { //實現run方法
System.out.println("Thread Run...");
}
});
thread.start();
通過匿名內部類的方式創建線程,省去了取名字的煩惱,但是還能不能再簡化一些呢?
JDK1.8 Lambda表達式寫法
Thread thread = new Thread(() -> System.out.println("Thread Run")); //一行搞定
thread.start();
我們可以看到Lambda一行代碼就完成了線程的創建,簡直不要太方便。(至於Lambda表達式的語法,我們下面章節再詳細介紹)
如果你的邏輯不止一行代碼,那么你還可以這么寫
Thread thread = new Thread(() -> {
System.out.println("Thread Run");
System.out.println("Hello");
});
thread.start();
用{}
將代碼塊包裹起來
例二:自定義比較器
我們先來看一下JDK1.7是如何實現自定義比較器的
List<String> list = Arrays.asList("Hi", "Life", "Hello~", "World");
Collections.sort(list, new Comparator<String>(){// 接口名
@Override
public int compare(String s1, String s2){// 方法名
if(s1 == null)
return -1;
if(s2 == null)
return 1;
return s1.length()-s2.length();
}
});
//輸出排序好的List
for (String s : list) {
System.out.println(s);
}
這里的sort方法傳入了兩個參數,一個是待排序的list,一個是比較器(排序規則),這里也是通過匿名內部類的方式實現的比較器。
下面我們來看一下Lambda表達式如何實現比較器?
List<String> list = Arrays.asList("Hi", "Life", "Hello~", "World");
Collections.sort(list, (s1, s2) ->{// 省略了參數的類型,編譯器會根據上下文信息自動推斷出類型
if(s1 == null)
return -1;
if(s2 == null)
return 1;
return s1.length()-s2.length();
});
//輸出排序好的List
for (String s : list) {
System.out.println(s);
}
我們可以看到,Lambda表達式和匿名內部類的作用相同,但是省略了很多代碼,可以大大加快開發速度
Lambda表達式語法
Lambda 表達式,也可稱為閉包,它是推動 Java 8 發布的最重要新特性。Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。
使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。上一章節我們已經見識到了Lambda表達式的優點,那么Lambda表達式到底該怎么寫呢?
語法
lambda 表達式的語法格式如下:
(parameters) -> expression //一行代碼
或
(parameters) ->{ statements; } //多行代碼
lambda表達式的重要特征:
- 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
- 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
- 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
- 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
// 1. 不需要參數,返回值為 5
() -> 5
// 2. 接收一個參數(數字類型),返回其2倍的值
x -> 2 * x
// 3. 接受2個參數(數字),並返回他們的差值
(x, y) -> x – y
// 4. 接收2個int型整數,返回他們的和
(int x, int y) -> x + y
// 5. 接受一個 string 對象,並在控制台打印,不返回任何值(看起來像是返回void)
(String s) -> System.out.print(s)
函數接口
上面幾個章節給大家介紹Lambda表達式的基本使用,那么是不是在任意地方都可以使用Lambda表達式呢?
其實Lambda表達式使用是有限制的。也許你已經想到了,能夠使用Lambda的依據是必須有相應的函數接口。(函數接口,是指內部只有一個抽象方法的接口)
自定義函數接口
自定義函數接口很容易,只需要編寫一個只有一個抽象方法的接口即可。
// 自定義函數接口
@FunctionalInterface
public interface PersonInterface<T>{
void accept(T t);
}
上面代碼中的@FunctionalInterface是可選的,但加上該標注編譯器會幫你檢查接口是否符合函數接口規范。就像加入@Override標注會檢查是否重載了函數一樣。
那么根據上面的自定義函數式接口,我們就可以寫出如下的Lambda表達式。
PersonInterface p = str -> System.out.println(str);
Lambda和匿名內部類
經過上面幾部分的介紹,相信大家對Lambda表達式已經有了初步認識,學會了如何使用。但想必大家心中始終有一個疑問,Lambda表達式似乎只是為了簡化匿名內部類的寫法,其他也沒啥區別了。這看起來僅僅通過語法糖在編譯階段把所有的Lambda表達式替換成匿名內部類就可以了,事實真的如此嗎?
public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous class");
}
}).start();
}
}
匿名內部類也是一個類,只不過我們不需要顯示為他定義名稱,但是編譯器會自動為匿名內部類命名。Main編輯后的文件如下圖
我們可以看到共有兩個class文件,一個是Main.class,而另一個則是編輯器為我們命名的內部類。
下面我們來看一下Lambda表達式會產生幾個class文件
public class Main {
public static void main(String[] args) {
new Thread(() -> System.out.println("Lambda")).start();
}
}
Lambda表達式通過invokedynamic指令實現,書寫Lambda表達式不會產生新的類
Lambda在集合中的運用
既然Lambda表達式這么方便,那么哪些地方可以使用Lambda表達式呢?
我們先從最熟悉的Java集合框架(Java Collections Framework, JCF)開始說起。
為引入Lambda表達式,Java8新增了java.util.funcion
包,里面包含常用的函數接口,這是Lambda表達式的基礎,Java集合框架也新增部分接口,以便與Lambda表達式對接。
首先回顧一下Java集合框架的接口繼承結構:
上圖中綠色標注的接口類,表示在Java8中加入了新的接口方法,當然由於繼承關系,他們相應的子類也都會繼承這些新方法。下表詳細列舉了這些方法。
接口名 | Java8新加入的方法 |
---|---|
Collection | removeIf() spliterator() stream() parallelStream() forEach() |
List | replaceAll() sort() |
Map | getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge() |
這些新加入的方法大部分要用到java.util.function
包下的接口,這意味着這些方法大部分都跟Lambda表達式相關。我們將逐一學習這些方法。
Collection中的新方法
如上所示,接口Collection
和List
新加入了一些方法,我們以是List
的子類ArrayList
為例來說明。了解Java7ArrayList
實現原理,將有助於理解下文。
forEach()
該方法的簽名為void forEach(Consumer<? super E> action)
,作用是對容器中的每個元素執行action
指定的動作,其中Consumer
是個函數接口,里面只有一個待實現方法void accept(T t)
(后面我們會看到,這個方法叫什么根本不重要,你甚至不需要記憶它的名字)。
需求:假設有一個字符串列表,需要打印出其中所有長度大於3的字符串.
Java7及以前我們可以用增強的for循環實現:
// 使用曾強for循環迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list){
if(str.length()>3)
System.out.println(str);
}
現在使用forEach()
方法結合匿名內部類,可以這樣實現:
// 使用forEach()結合匿名內部類迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
@Override
public void accept(String str){
if(str.length()>3)
System.out.println(str);
}
});
上述代碼調用forEach()
方法,並使用匿名內部類實現Comsumer
接口。到目前為止我們沒看到這種設計有什么好處,但是不要忘記Lambda表達式,使用Lambda表達式實現如下:
// 使用forEach()結合Lambda表達式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
if(str.length()>3)
System.out.println(str);
});
上述代碼給forEach()
方法傳入一個Lambda表達式,我們不需要知道accept()
方法,也不需要知道Consumer
接口,類型推導幫我們做了一切。
removeIf()
該方法簽名為boolean removeIf(Predicate<? super E> filter)
,作用是刪除容器中所有滿足filter
指定條件的元素,其中Predicate
是一個函數接口,里面只有一個待實現方法boolean test(T t)
,同樣的這個方法的名字根本不重要,因為用的時候不需要書寫這個名字。
需求:假設有一個字符串列表,需要刪除其中所有長度大於3的字符串。
我們知道如果需要在迭代過程沖對容器進行刪除操作必須使用迭代器,否則會拋出ConcurrentModificationException
,所以上述任務傳統的寫法是:
// 使用迭代器刪除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()){
if(it.next().length()>3) // 刪除長度大於3的元素
it.remove();
}
現在使用removeIf()
方法結合匿名內部類,我們可是這樣實現:
// 使用removeIf()結合匿名名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){ // 刪除長度大於3的元素
@Override
public boolean test(String str){
return str.length()>3;
}
});
上述代碼使用removeIf()
方法,並使用匿名內部類實現Precicate
接口。相信你已經想到用Lambda表達式該怎么寫了:
// 使用removeIf()結合Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length()>3); // 刪除長度大於3的元素
使用Lambda表達式不需要記憶Predicate
接口名,也不需要記憶test()
方法名,只需要知道此處需要一個返回布爾類型的Lambda表達式就行了。
replaceAll()
該方法簽名為void replaceAll(UnaryOperator<E> operator)
,作用是對每個元素執行operator
指定的操作,並用操作結果來替換原來的元素。其中UnaryOperator
是一個函數接口,里面只有一個待實現函數T apply(T t)
。
需求:假設有一個字符串列表,將其中所有長度大於3的元素轉換成大寫,其余元素不變。
Java7及之前似乎沒有優雅的辦法:
// 使用下標實現元素替換
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(int i=0; i<list.size(); i++){
String str = list.get(i);
if(str.length()>3)
list.set(i, str.toUpperCase());
}
使用replaceAll()
方法結合匿名內部類可以實現如下:
// 使用匿名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>(){
@Override
public String apply(String str){
if(str.length()>3)
return str.toUpperCase();
return str;
}
});
上述代碼調用replaceAll()
方法,並使用匿名內部類實現UnaryOperator
接口。我們知道可以用更為簡潔的Lambda表達式實現:
// 使用Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
if(str.length()>3)
return str.toUpperCase();
return str;
});
sort()
該方法定義在List
接口中,方法簽名為void sort(Comparator<? super E> c)
,該方法根據c
指定的比較規則對容器元素進行排序。Comparator
接口我們並不陌生,其中有一個方法int compare(T o1, T o2)
需要實現,顯然該接口是個函數接口。
需求:假設有一個字符串列表,按照字符串長度增序對元素排序。
由於Java7以及之前sort()
方法在Collections
工具類中,所以代碼要這樣寫:
// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>(){
@Override
public int compare(String str1, String str2){
return str1.length()-str2.length();
}
});
現在可以直接使用List.sort()方法
,結合Lambda表達式,可以這樣寫:
// List.sort()方法結合Lambda表達式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());
spliterator()
方法簽名為Spliterator<E> spliterator()
,該方法返回容器的可拆分迭代器。從名字來看該方法跟iterator()
方法有點像,我們知道Iterator
是用來迭代容器的,Spliterator
也有類似作用,但二者有如下不同:
Spliterator
既可以像Iterator
那樣逐個迭代,也可以批量迭代。批量迭代可以降低迭代的開銷。Spliterator
是可拆分的,一個Spliterator
可以通過調用Spliterator<T> trySplit()
方法來嘗試分成兩個。一個是this
,另一個是新返回的那個,這兩個迭代器代表的元素沒有重疊。
可通過(多次)調用Spliterator.trySplit()
方法來分解負載,以便多線程處理。
stream()和parallelStream()
stream()
和parallelStream()
分別返回該容器的Stream
視圖表示,不同之處在於parallelStream()
返回並行的Stream
。Stream
是Java函數式編程的核心類,我們會在后面章節中學習。
Map中的新方法
相比Collection
,Map
中加入了更多的方法,我們以HashMap
為例來逐一探秘。
forEach()
該方法簽名為void forEach(BiConsumer<? super K,? super V> action)
,作用是對Map
中的每個映射執行action
指定的操作,其中BiConsumer
是一個函數接口,里面有一個待實現方法void accept(T t, U u)
。BinConsumer
接口名字和accept()
方法名字都不重要,請不要記憶他們。
需求:假設有一個數字到對應英文單詞的Map,請輸出Map中的所有映射關系.
Java7以及之前經典的代碼如下:
// Java7以及之前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
System.out.println(entry.getKey() + "=" + entry.getValue());
}
使用Map.forEach()
方法,結合匿名內部類,代碼如下:
// 使用forEach()結合匿名內部類迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>(){
@Override
public void accept(Integer k, String v){
System.out.println(k + "=" + v);
}
});
上述代碼調用forEach()
方法,並使用匿名內部類實現BiConsumer
接口。當然,實際場景中沒人使用匿名內部類寫法,因為有Lambda表達式:
// 使用forEach()結合Lambda表達式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}
getOrDefault()
該方法跟Lambda表達式沒關系,但是很有用。方法簽名為V getOrDefault(Object key, V defaultValue)
,作用是按照給定的key
查詢Map
中對應的value
,如果沒有找到則返回defaultValue
。使用該方法程序員可以省去查詢指定鍵值是否存在的麻煩.
需求;假設有一個數字到對應英文單詞的Map,輸出4對應的英文單詞,如果不存在則輸出NoValue
// 查詢Map中指定的值,不存在時使用默認值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及之前做法
if(map.containsKey(4)){ // 1
System.out.println(map.get(4));
}else{
System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2
putIfAbsent()
該方法跟Lambda表達式沒關系,但是很有用。方法簽名為V putIfAbsent(K key, V value)
,作用是只有在不存在key
值的映射或映射值為null
時,才將value
指定的值放入到Map
中,否則不對Map
做更改.該方法將條件判斷和賦值合二為一,使用起來更加方便.
remove()
我們都知道Map
中有一個remove(Object key)
方法,來根據指定key
值刪除Map
中的映射關系;Java8新增了remove(Object key, Object value)
方法,只有在當前Map
中key
正好映射到value
時才刪除該映射,否則什么也不做.
replace()
在Java7及以前,要想替換Map
中的映射關系可通過put(K key, V value)
方法實現,該方法總是會用新值替換原來的值.為了更精確的控制替換行為,Java8在Map
中加入了兩個replace()
方法,分別如下:
replace(K key, V value)
,只有在當前Map
中key
的映射存在時才用value
去替換原來的值,否則什么也不做.replace(K key, V oldValue, V newValue)
,只有在當前Map
中key
的映射存在且等於oldValue
時才用newValue
去替換原來的值,否則什么也不做.
replaceAll()
該方法簽名為replaceAll(BiFunction<? super K,? super V,? extends V> function)
,作用是對Map
中的每個映射執行function
指定的操作,並用function
的執行結果替換原來的value
,其中BiFunction
是一個函數接口,里面有一個待實現方法R apply(T t, U u)
.不要被如此多的函數接口嚇到,因為使用的時候根本不需要知道他們的名字.
需求:假設有一個數字到對應英文單詞的Map,請將原來映射關系中的單詞都轉換成大寫.
Java7以及之前經典的代碼如下:
// Java7以及之前替換所有Map中所有映射關系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
entry.setValue(entry.getValue().toUpperCase());
}
使用replaceAll()
方法結合匿名內部類,實現如下:
// 使用replaceAll()結合匿名內部類實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
@Override
public String apply(Integer k, String v){
return v.toUpperCase();
}
});
上述代碼調用replaceAll()
方法,並使用匿名內部類實現BiFunction
接口。更進一步的,使用Lambda表達式實現如下:
// 使用replaceAll()結合Lambda表達式實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());
簡潔到讓人難以置信.
merge()
該方法簽名為merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
,作用是:
- 如果
Map
中key
對應的映射不存在或者為null
,則將value
(不能是null
)關聯到key
上; - 否則執行
remappingFunction
,如果執行結果非null
則用該結果跟key
關聯,否則在Map
中刪除key
的映射.
參數中BiFunction
函數接口前面已經介紹過,里面有一個待實現方法R apply(T t, U u)
.
merge()
方法雖然語義有些復雜,但該方法的用方式很明確,一個比較常見的場景是將新的錯誤信息拼接到原來的信息上,比如:
map.merge(key, newMsg, (v1, v2) -> v1+v2);
compute()
該方法簽名為compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
,作用是把remappingFunction
的計算結果關聯到key
上,如果計算結果為null
,則在Map
中刪除key
的映射.
要實現上述merge()
方法中錯誤信息拼接的例子,使用compute()
代碼如下:
map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));
computeIfAbsent()
該方法簽名為V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
,作用是:只有在當前Map
中不存在key
值的映射或映射值為null
時,才調用mappingFunction
,並在mappingFunction
執行結果非null
時,將結果跟key
關聯.
Function
是一個函數接口,里面有一個待實現方法R apply(T t)
.
computeIfAbsent()
常用來對Map
的某個key
值建立初始化映射.比如我們要實現一個多值映射,Map
的定義可能是Map<K,Set<V>>
,要向Map
中放入新值,可通過如下代碼實現:
Map<Integer, Set<String>> map = new HashMap<>();
// Java7及以前的實現方式
if(map.containsKey(1)){
map.get(1).add("one");
}else{
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
// Java8的實現方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");
使用computeIfAbsent()
將條件判斷和添加操作合二為一,使代碼更加簡潔.
computeIfPresent()
該方法簽名為V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
,作用跟computeIfAbsent()
相反,即,只有在當前Map
中存在key
值的映射且非null
時,才調用remappingFunction
,如果remappingFunction
執行結果為null
,則刪除key
的映射,否則使用該結果替換key
原來的映射.
這個函數的功能跟如下代碼是等效的:
// Java7及以前跟computeIfPresent()等效的代碼
if (map.get(key) != null) {
V oldValue = map.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null)
map.put(key, newValue);
else
map.remove(key);
return newValue;
}
return null;
- Java8為容器新增一些有用的方法,這些方法有些是為完善原有功能,有些是為引入函數式編程,學習和使用這些方法有助於我們寫出更加簡潔有效的代碼.
- 函數接口雖然很多,但絕大多數時候我們根本不需要知道它們的名字,書寫Lambda表達式時類型推斷幫我們做了一切.