小碼哥教育Android學院
引入
我們都知道java是一門面向物件的語言,我們也清楚面向物件思想的強大。但是,這個概念,在java8出現之後,被推翻了。
java8引入了函式式開發,其實C語言就是標準的函式式開發。 那我們就更疑惑了,為什麼如此好的一種思想,還會在後期被推翻了呢?
其實原因很簡單,隨著網際網路的發展,越來越重視並行開發和基於事件的開發,而函式式開發,就特別擅長做這些事情。
什麼是函數語言程式設計
函數語言程式設計的起源,是一門叫做範疇論(Category Theory)的數學分支。什麼是範疇呢?
“範疇就是使用箭頭連線的物體。”(In mathematics, a category is an algebraic structure that comprises “objects” that are linked by “arrows”. )
也就是說,彼此之間存在某種關係的概念、事物、物件等等,都構成”範疇”。隨便什麼東西,只要能找出它們之間的關係,就能定義一個”範疇”。
範疇論使用函式,表達範疇之間的關係。
伴隨著範疇論的發展,就發展出一整套函式的運算方法。這套方法起初只用於數學運算,後來有人將它在計算機上實現了,就變成了今天的”函數語言程式設計”。
本質上,函數語言程式設計只是範疇論的運算方法,跟數理邏輯、微積分、行列式是同一類東西,都是數學方法,只是碰巧它能用來寫程式。
函式式是一種數學運算,原始目的就是求值
環境準備
如果還沒有安裝Java 8,那麼你應該先安裝才能使用lambda和stream。 如果是使用Eclipse工具,那麼需要安裝4.4以上的版本。之後,就可以使用Java 8的新特性了,包括lambda表示式,可重複的註解,緊湊的概要檔案和其他特性。
面向物件程式設計
我們先使用面向物件程式設計,來完成兩個小案例。
1. 對陣列中的物件進行排序
2. 建立一個執行緒,並開啟執行緒。
class User{ public String name; public int score; public User(String name, int score) { super(); this.name = name; this.score = score; } @Override public String toString() { return "User [name=" + name + ", score=" + score + "]"; } } @Test public void arrayComparableTest(){ User[] us = new User[]{new User("張三", 10), new User("李四", 15), new User("王五", 12)}; Arrays.sort(us, new Comparator<User>() { // 對陣列中的元素進行排序 @Override public int compare(User o1, User o2) { return Integer.compare(o1.score, o2.score); } }); System.out.println(Arrays.toString(us)); } @Test public void threadTest(){ Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("hello lambda"); } }); t.start(); }
其實,上面兩個案例,都是體現的函數語言程式設計。方法中需要另外一個方法來實現業務邏輯。因為java中之前沒有函數語言程式設計,所以想用另外一個方法來實現邏輯,只能通過匿名介面的方式。
現在,我們對這段程式碼進行分析:
User[] us = new User[]{new User("張三", 10), new User("李四", 15), new User("王五", 12)}; Arrays.sort(us, new Comparator<User>() { // 對陣列中的元素進行排序 @Override public int compare(User o1, User o2) { return Integer.compare(o1.score, o2.score); } });
- sort方法,第二個引數,一定是一個Comparator介面。
- Comparator介面,一定需要實現一個compare方法。
compare方法,一定需要返回一個int型別的結果。
那我們就有疑問了,這些一定會發生的事情,編譯器是否可以幫助我們做,幫助我們推導出來結果呢?
假設,編譯器可以幫助我們推導,那麼,我們就可以將程式碼改成下面的樣子。
我們,將一定會發生的事情,刪除掉,讓編譯器去推導。User[] us = new User[]{new User("張三", 10), new User("李四", 15), new User("王五", 12)}; Arrays.sort(us, (User o1, User o2) { return Integer.compare(o1.score, o2.score); }); System.out.println(Arrays.toString(us));
此時,語法不正確,程式碼當然編譯不能通過。那我們該如何處理呢?之前有講到一個概念,範疇論。使用箭頭來連線物體。所以,這裡提出一種新的語法: -> ,指向某處的意思(goes to)。
User[] us = new User[]{new User("張三", 10), new User("李四", 15), new User("王五", 12)};
Arrays.sort(us,
(User o1, User o2) -> {
return Integer.compare(o1.score, o2.score);
});
System.out.println(Arrays.toString(us));
不負眾望,編譯通過了。編譯通過,說明語法沒問題。
那這種新的語法是什麼呢?這個,就是Lambda表示式。
面向函數語言程式設計
有沒有覺得很厲害很神奇?
其實不然,回過頭,我們去對比一下,發現,其實,使用Lambda表示式,就是將我們分析出來的,一定、必不可少的東西刪除了,讓編譯器自己去推匯出結果。從一定需要程式設計師來書寫的東西,交給了編譯器而已。
所以說,儘管我們使用了一種簡潔的方式去寫,但是,編譯器也能推匯出來,程式碼在編譯的時候,也會推導成我們最開始的寫法,其實Lambda表示式是新特性嗎?不是,僅僅是編譯器的新特性,解放程式設計師而已。
但是,有問題。我發現,感覺也沒之前的寫法方便多少呀,而且我還要記新的語法。
其實,我們還可以對上面的程式碼進行優化,優化的原則就是:將一定需要做的事情,簡化掉。
Arrays.sort(us,
(User o1, User o2) -> {
return Integer.compare(o1.score, o2.score);
});
這已經是一個Lambda表示式了。那我們再來分析,還有什麼地方是一定要做的呢?
1. compare方法中只有一行程式碼,並且,這一行程式碼,一定會返回一個int型別的值。那麼,我們去刪除它。
Arrays.sort(us, (User o1, User o2)-> Integer.compare(o1.score, o2.score));
編譯通過。
2. Compare方法,有兩個引數,這兩個引數的型別,可以通過傳遞的陣列來推導元素型別,一定是User型別。那麼,我們刪除它。
Arrays.sort(us, (o1, o2)-> Integer.compare(o1.score, o2.score));
編譯通過。
到這裡,我們在來比較一下:
Arrays.sort(us, new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return Integer.compare(o1.score, o2.score);
}
});
//Arrays.sort(us, (o1, o2)-> Integer.compare(o1.score, o2.score));
對於程式碼的簡潔程度來說,高下立判。
好,我們快速的將第二個案例修改一下。
Thread t = new Thread(()-> System.out.println("hello lambda"));
t.start();
Lambda表示式的含義
表示式的含義是什麼?表示式就是一個組合,可以求得結果。Lambda表示式,首先是一個表示式,那麼,代表著這個表示式在運算之後,也會有一個結果的。因為我們已經很清楚的為函式式定義過:函式式是一種數學運算,原始目的就是求值,不做其他事情。
所以,現在我們可以嘗試,將Lambda表示式作為一個結果,賦值給一個變數。
Runnable r = new Runnable(){
public void run(){
System.out.println("hello lambda");
}
}
// 將上面的程式碼,用Lambda表示式去修改。
Runnable r1 = () -> System.out.println("hello lambda");
當編譯器看到這一段Lambda表示式的時候,就會去做推導,根據接受變數的型別,來推匯出Lambda的常規寫法。
Lambda表示式詳解
概念
我們先看一下,百度百科對Lambda表示式的定義:
“Lambda 表示式”(lambda expression)是一個匿名函式,Lambda表示式基於數學中的λ演算得名,直接對應於其中的lambda抽象(lambda abstraction),是一個匿名函式,即沒有函式名的函式。Lambda表示式可以表示閉包(注意和數學傳統意義上的不同)。
我們將帶有引數變數的表示式,就稱之為Lambda表示式。
Lambda表示式基本語法
Lambda表示式的基本語法:方法引數列表 -> 表示式; 引數列表,是介面的方法引數列表,表示式,是介面方法的具體邏輯實現。
方法引數列表
方法沒有引數
這個案例其實我們已經做過了,建立一個執行緒,傳遞一個Runnable物件即可。
沒有引數,需要注意的是,引數列表的 () 是一定不能省略的。因為括號才表示是方法的引數列表。new Thread(() -> System.out.println("沒有引數")).start();
方法有一個引數
先引入一個案例:Frame f = new Frame("lambda"); f.setBounds(100, 100, 100, 300); Button b = new Button("按鈕"); b.addActionListener((ActionEvent event) -> System.out.println("按鈕被點選")); f.setVisible(true);
如果是一個引數,需要注意:
- 如果引數寫型別,那麼,必須寫方法的()
如果引數不寫型別(編譯器可以推匯出來引數的型別),那麼,可以省略方法的()
所以,我們還可以這麼寫:Frame f = new Frame("lambda"); f.setBounds(100, 100, 100, 300); Button b = new Button("按鈕"); b.addActionListener(event -> System.out.println("按鈕被點選")); f.setVisible(true);
方法有兩個或多個引數
如果是兩個或者多個引數,那麼需要注意,不管是否有引數型別,都必須有()Integer[] arr = new Integer[]{1,5,3,4}; Arrays.sort(arr, (x, y) -> Integer.compare(x, y)); System.out.println(Arrays.toString(arr));
如果有其他的修飾符修飾
比如,可以用final修飾區域性變數,此時,如果要寫額外的修飾符的時候,引數必須得有型別。不管是一個引數還是多個引數。Integer[] arr = new Integer[]{1,5,3,4}; Arrays.sort(arr, (final Integer x, final Integer y) -> Integer.compare(x, y)); System.out.println(Arrays.toString(arr));
表示式
- 如果表示式只有一行程式碼
可以不需要{},直接寫,不能寫return關鍵字。編譯器幫助我們推導return。 - 如果表示式超過一行
那麼這一段程式碼,就必須寫{},就必須遵循方法的規則,並且如果方法有返回,就必須有返回語句。
變數作用域
在Lambda表示式中,可以存在三種變數。
1. 區域性變數
2. 方法引數
3. 自由變數(不是區域性變數,也不是方法引數)
我們分析這三種變數,其實對於區域性變數和方法引數變數,跟我們普通的方法使用規則都是一樣的。這裡就不介紹了。我們要說兩個比較特殊的情況:
比如下面的這一段程式碼
@Test
public void test1(){
print("lambda", 5);
}
public void print(String content, int times){
new Thread(()-> {
for (int i = 0; i < times; i++) {
System.out.println(content);
}
}).start();
}
- 自由變數,在Lambda表示式中,可以隨意使用自由變數。比如,我們可以在run方法中使用到外層方法的引數。
其實這種寫法,編譯器在編譯的時候,會推導成常規的寫法,常規的我們會如何寫?我們會使用final修飾content 和 times,為什麼嗎?因為print方法可能執行完畢之後,子執行緒才會執行,那麼如果沒有用final修飾content,方法執行完畢之後,這個區域性變數就會被回收,相當於是執行緒中訪問了一塊已經被回收的記憶體,所以我們使用final修飾,將區域性變數放置到常量池中儲存起來。
那Lambda中,可以直接使用content變數,我們可以如何推斷?其實編譯器已經預設將自由變數用final修飾,儲存起來了。我們只能使用這個變數,不能修改final變數。 - this,在匿名內部類中,如果我們使用this,那麼,這個this其實是匿名內部類對自己的引用。但是,在Lambda表示式中的this,卻是指向的該Lambda表示式所在方法,這個方法所在的類的引用。這一點需要跟以前的情況區分開。
- 操作自由變數的程式碼塊,我們就稱之為“閉包”。
函式式介面
想想我們寫的幾個案例,使用的都是介面,比如Runnable、Comparator、ActionListener等,發現,這些介面都只有一個抽象方法。那麼,我們可以用Lambda寫一個介面,介面中有多個方法嗎?
不可以,我們能夠寫Lambda表示式的地方,只能是介面,並且這個介面中只能有一個抽象方法。這種介面,我們就稱之為函式式介面。
思考兩個問題:
1. 為什麼Lambda表示式中,介面不能有多個方法?
其實,要回答這個問題,也很容易。Lambda主要是如何執行的?編譯器推匯出常規的寫法來執行的。那麼,大家可以試想一下,如果一個介面中包含多個方法,那麼,編譯器可以很容易的推導麼?當然不可以!
2. 我們可不可以自定義一個介面,然後,只要在有介面引數的地方,就用Lambda表示式來寫?可以!
@Test
public void test5(){
this.test4(()->System.out.println("my work"));
}
public void test4(IWork work){
System.out.println("begin work");
work.doWork();
System.out.println("end work");
}
interface IWork {
void doWork();
}
- 在java中,如果介面中只有一個抽象方法,那麼這個介面就是函式式介面。我們可以使用註解@FunctionalInterface 來檢測這個介面是否是函式式介面。
- @FunctionalInterface註解只是用來檢測,並不能定義某個介面是否是函式式介面,函式式介面主要是看介面的方法數。
- 簡化函式式介面的使用,是Lambda表示式出現的唯一作用。
回過頭,我們在看一下這一句程式碼。
this.test4(()->System.out.println("my work"));
好像,我們可以用一個介面變數來接收Lambda表示式的結果。所以,程式碼變成了這樣:
IWork work = ()->System.out.println("my work");
this.test4(work);
那麼,問題來了,
Runnable r = ()->System.out.println("my work");
這樣去申明一個Runnable介面,也是可以的。 那,如果我僅僅這麼寫
()->System.out.println("my work");
這個Lambda表示式,到底是用什麼型別的介面去接收呢?
所以,結論是什麼呢?結論就是,單靠Lambda表示式,是不能知道它的返回型別的,必須通過接收的變數型別,去推導這個Lambda表示式是否符合規範。而不能從Lambda表示式推匯出變數型別。
- 函式式介面中,是可以寫Object中的方法的,因為介面也是一個特殊的類,重寫Object中的方法,不影響函式式介面。
- Lambda表示式中的異常處理, 要麼就是在Lambda的程式碼塊中自行try,要麼是在介面中的方法中申明throws。
方法引用
引入
我們還是看之前寫過的一個案例。
Integer[] arr = new Integer[]{1,5,3,4};
Arrays.sort(arr, (x, y) -> Integer.compare(x, y));
System.out.println(Arrays.toString(arr));
如何分析這一段程式碼呢?我們發現,方法需要兩個引數 x/y,而這兩個引數,原封不動的交給了Integer的compare方法的引數列表。那我們就有一個推論,既然這種情況出現,那麼可不可以通過某種方式,讓編譯器自己去推導常規寫法呢?
這裡,我們引入一種新的語法結構 :: ,如果Lambda表示式中,介面方法的實現,只調用其他方法來真正完成邏輯,並且介面方法的引數原封不動的傳遞給了那個完成邏輯的方法,那麼我們就可以使用這種語法。
我們還修正一下上面的程式碼。
Integer[] arr = new Integer[]{1,5,3,4};
Arrays.sort(arr, Integer::compare);// 兩個引數,原封不動的傳遞給了compare方法
System.out.println(Arrays.toString(arr));
這種寫法,我們就稱之為方法引用。
方法引用詳解
在Lambda表示式中,支援三種方法引用:
1. 類 :: 靜態方法
2. 物件 :: 普通方法
3. 物件 :: 靜態方法
對於靜態方法,第一種和第三種其實是一樣的,因為儘管用物件去呼叫靜態方法,其實編譯器還是使用的類去訪問。
類 :: 靜態方法, 我們講解的案例,就是這種情況。
那現在我們來研究一下,物件 :: 普通方法
案例一:
我們對上面的程式碼進行改造:
@Test
public void test3(){
Integer[] arr = new Integer[]{1,5,3,4};
LambdaTest test = new LambdaTest();
// 物件 :: 普通方法
Arrays.sort(arr, test::myCompare);
System.out.println(Arrays.toString(arr));
}
public int myCompare(int x, int y){
return Integer.compare(x, y);
}
程式碼寫到這裡,立馬就會發現一個問題,為啥我們還要去建立一個物件呢?在類的普通方法裡面,this就代表當前物件。所以,程式碼,我們還可以這麼寫:
@Test
public void test3(){
Integer[] arr = new Integer[]{1,5,3,4};
// 物件 :: 普通方法
Arrays.sort(arr, this::myCompare);
System.out.println(Arrays.toString(arr));
}
public int myCompare(int x, int y){
return Integer.compare(x, y);
}
另外,我們花半分鐘的時間,將這個普通方法改成靜態方法,再用Lambda表示式寫一遍。
@Test
public void test3(){
Integer[] arr = new Integer[]{1,5,3,4};
// 類 :: 靜態方法
Arrays.sort(arr, LambdaTest::myCompare);
System.out.println(Arrays.toString(arr));
}
public static int myCompare(int x, int y){
System.out.println("一些其他的邏輯程式碼");
return Integer.compare(x, y);
}
案例二:
疑問,我們都是呼叫的系統的方法,那麼我們是否可以自己定義一個介面,用Lambda表示式來寫呢?
// 定義的函式式介面
@FunctionalInterface
public interface IWork {
void work(int x, int y);// 介面方法沒有返回值。
}
public class lambdatest {
public void wrap(IWork work){
System.out.println("some work");
work.work(3, 4);
}
@Test
public void test1(){
this.wrap(this::getSum);// 使用物件來呼叫普通方法,注意,這個方法是有返回值的。
}
public int getSum(int x, int y){
System.out.println(x + y);
return x + y;
}
}
對於這個案例,我們分析一下這個介面方法返回值和呼叫的普通方法返回值的問題。
1. 如果介面方法有返回值,我們使用方法引用,那麼引用的方法,也必須有對應的返回值。因為介面方法需要return語句,僅僅是我們沒有寫出來而已,編譯器會推導一個return語句。
2. 如果介面方法沒有返回值,那麼,引用的普通方法,返回值型別就隨意定。
要想這個問題其實也很簡單。比如
// 情況一
public void run(){
// 如果這個是一個介面方法,這個介面方法裡面可以呼叫有返回的普通方法,允許。
otherMethod();
}
// 情況二
public int run1(){
// 如果這個是一個介面方法,這個介面方法呼叫普通方法,普通方法必須有對應的return語句才行
return otherMethod();
}
案例三
java8中,List集合為我們提供了一個forEach方法,從字面意思,我們就能猜出,這個方法是用來遍歷元素的,這個方法需要傳遞一個介面Consumer,Consumer就是一個函式式介面。所以,我們完成以下,遍歷集合,列印元素。
@Test
public void forEach(){
Integer[] arr = new Integer[]{1,2,3,4,5};
List<Integer> ls = Arrays.asList(arr);
// 既然是函式式介面,那麼我們就可以使用Lambda表示式來書寫
ls.forEach(x -> System.out.println(x));
}
我們分析這一個Lambda表示式
ls.forEach(x -> System.out.println(x));
這句表示式的含義是,Consumer介面的方法中需要一個引數,我們將這個引數直接傳遞給了列印操作。將引數原封不動的傳遞給某一個方法,我們也可以嘗試的使用方法引用來改造程式碼。如何修改呢?
記住語法格式: 物件 :: 普通方法
println 是一個普通的方法,沒有用static修飾,所以,我們需要用物件呼叫,那物件又是誰呢? System.out就是一個列印流物件。所以,我們可以將程式碼修改成這樣:
ls.forEach(System.out::println);
這,也是我們以後使用Lambda表示式做列印操作,最常用的寫法。
構造器引用
引入
其實,構造器,也是一個特殊的方法,但是這個方法,我們是結合關鍵字new一起使用的。比如,建立一個物件,我們就 new 類名(引數列表);
現在我們有這種使用場景:
當我們呼叫一個方法,返回的是一個介面物件的時候,一般,我們必須要返回這個介面物件的實現類物件,因為介面不能建立物件,那麼,我們之前怎麼做的呢?
// Arrays工具類中的as List 方法。
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
但是,這個方法是返回一個固定的ArrayList,如果我們期望,可以指定新增到一個集合物件中,比如這裡,我希望可以將元素新增到LinkedList中,那我們怎麼做?我們是不是應該在定義方法的時候,就要顯示的將要建立的物件的型別傳遞給方法。所以,我們需要這麼定義。
public static <T> List<T> asList(Class clz, T ... a){
// 通過反射機制,來建立傳遞過來的類的物件,然後再將元素新增到這個物件中,進行返回。
}
但是這樣寫,程式碼耦合度太高,我們為了模組之間的解耦,可以使用一個介面來實現這種方式。
// 申明泛型為List 或者其子類型別
public interface IMyCreator<T extends List<?>>{
T create();
}
// 測試類
public class ListTest {
// 傳入一個介面型別,通過介面方法,返回一個類物件,再將元素新增到這個物件中,返回回去
@SuppressWarnings("unchecked")
public <T> List<T> myAsList(IMyCreator<List<T>> create, T ... a){
List<T> list = create.create();
for (T t : a){
list.add(t);
}
return list;
}
@Test
public void add(){
// 這裡就使用了Lambda表示式,介面引數列表為空,介面中的方法具體邏輯就是返回一個ArrayList
List<Integer> list = this.myAsList(()->new ArrayList<Integer>(), 1, 2, 3, 4);
System.out.println(list);
}
}
分析這一段程式碼:
this.myAsList(()->new ArrayList<Integer>(), 1, 2, 3, 4);// 這已經是一個Lambda表示式了
這種場景,就是一個介面中定義一個方法,返回一個物件。那麼,在Lambda表示式中,這種情況,我們就稱之為構造器引用,語法格式是: 類 :: new
我們可以使用構造器引用的方式,來改造上面的Lambda表示式:
this.myAsList(ArrayList<Integer>::new, 1, 2, 3, 4);// 建立物件的時候,指定泛型
構造器引用細節
- 構造器引用的函式式介面,這個介面中的方法不能有引數。
Lambda表示式需要返回的物件,這個物件必須要有一個無參的構造器。
如果不滿足這兩點,我們就不能使用構造器引用,只能使用普通的Lambda表示式,比如:this.myAsList((x)->new MyArrayList<Integer>(x), 1, 2, 3, 4);// 只能使用簡單的Lambda表示式
Lambda表示式的優點
Lambda表示式,看起來挺先進,其實經過我們的分析,還是由編譯器來幫助我們推導成為常規程式碼。所以,很多人都覺得不建議使用Lambda表示式,因為他們覺得,如此寫程式碼,感覺儘管簡單,但是難懂,難以除錯,不利於維護。
是嗎?不是的,Lambda表示式,他的一些缺點,掩蓋不了它的優點。
1. 程式碼簡潔,開發快速
函數語言程式設計大量使用函式,減少了程式碼的重複,因此程式比較短,開發速度較快。
2. 接近自然語言,易於理解
函數語言程式設計的自由度很高,可以寫出很接近自然語言的程式碼。
3. 更方便的程式碼管理
函數語言程式設計不依賴、也不會改變外界的狀態,只要給定輸入引數,返回的結果必定相同。因此,每一個函式都可以被看做獨立單元,很有利於進行單元測試(unit testing)和除錯(debugging),以及模組化組合。
4. 易於”併發程式設計”
函數語言程式設計不需要考慮”死鎖”(deadlock),因為它不修改變數,所以根本不存在”鎖”執行緒的問題。不必擔心一個執行緒的資料,被另一個執行緒修改,所以可以很放心地把工作分攤到多個執行緒,部署”併發程式設計”(concurrency)。
5. 程式碼的熱升級
函數語言程式設計沒有副作用,只要保證介面不變,內部實現是外部無關的。所以,可以在執行狀態下直接升級程式碼,不需要重啟,也不需要停機。