當前位置:
首頁 > 最新 > Lambda表達式/計算機程序的思維邏輯

Lambda表達式/計算機程序的思維邏輯

在之前的章節中,我們的討論基本都是基於Java 7的,從本節開始,我們探討Java 8的一些特性,主要內容包括:

傳遞行為代碼 - Lambda表達式

函數式數據處理 - 流

組合式非同步編程 - CompletableFuture

新的日期和時間API

本節,我們先討論Lambda表達式,它是什麼?有什麼用呢?

Lambda表達式是Java 8新引入的一種語法,是一種緊湊的傳遞代碼的方式,它的名字來源於學術界的λ演算,具體我們就不探討了。

理解Lambda表達式,我們先回顧一下介面、匿名內部類和代碼傳遞。

通過介面傳遞代碼

我們在19節介紹過介面以及面向介面的編程,針對介面而非具體類型進行編程,可以降低程序的耦合性、提高靈活性、提高復用性。介面常被用於傳遞代碼,比如,在59節,我們介紹過File的如下方法:

public String[] list(FilenameFilter filter)

public File[] listFiles(FilenameFilter filter)

list和listFiles需要的其實不是FilenameFilter對象,而是它包含的如下方法:

boolean accept(File dir, String name);

或者說,list和listFiles希望接受一段方法代碼作為參數,但沒有辦法直接傳遞這個方法代碼本身,只能傳遞一個介面。

再比如,我們在53節介紹過Collections的一些演算法,很多方法都接受一個參數Comparator,比如:

public static int binarySearch(List

public static T max(Collection

public static void sort(List list, Comparator

它們需要的也不是Comparator對象,而是它包含的如下方法:

int compare(T o1, T o2);

但是,沒有辦法直接傳遞方法,只能傳遞一個介面。

我們在77節介紹過非同步任務執行服務ExecutorService,提交任務的方法有:

Future submit(Callable task);

Future submit(Runnable task, T result);

Future submit(Runnable task);

Callable和Runnable介面也用於傳遞任務代碼。

通過介面傳遞行為代碼,就要傳遞一個實現了該介面的實例對象,在之前的章節中,最簡潔的方式是使用匿名內部類,比如:

//列出當前目錄下的所有後綴為.txt的文件

File f = new File(".");

File[] files = f.listFiles(new FilenameFilter(){

@Override

public boolean accept(File dir, String name) {

if(name.endsWith(".txt")){

return true;

}

return false;

}

});

將files按照文件名排序,代碼為:

Arrays.sort(files, new Comparator() {

@Override

public int compare(File f1, File f2) {

return f1.getName().compareTo(f2.getName());

}

});

提交一個最簡單的任務,代碼為:

Lambda表達式

語法

Java 8提供了一種新的緊湊的傳遞代碼的語法 - Lambda表達式。對於前面列出文件的例子,代碼可以改為:

File f = new File(".");

File[] files = f.listFiles((File dir, String name) -> {

if (name.endsWith(".txt")) {

return true;

}

return false;

});

可以看出,相比匿名內部類,傳遞代碼變得更為直觀,不再有實現介面的模板代碼,不再聲明方法,也名字也沒有,而是直接給出了方法的實現代碼。Lambda表達式由->分隔為兩部分,前面是方法的參數,後面{}內是方法的代碼。

上面代碼可以簡化為:

File[] files = f.listFiles((File dir, String name) -> {

return name.endsWith(".txt");

});

當主體代碼只有一條語句的時候,括弧和return語句也可以省略,上面代碼可以變為:

File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));

注意,沒有括弧的時候,主體代碼是一個表達式,這個表達式的值就是函數的返回值,結尾不能加分號;,也不能加return語句。

方法的參數類型聲明也可以省略,上面代碼還可以繼續簡化為:

File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

之所以可以省略方法的參數類型,是因為Java可以自動推斷出來,它知道listFiles接受的參數類型是FilenameFilter,這個介面只有一個方法accept,這個方法的兩個參數類型分別是File和String。

這樣簡化下來,代碼是不是簡潔清楚多了?

排序的代碼用Lambda表達式可以寫為:

Arrays.sort(files,(f1, f2) -> f1.getName().compareTo(f2.getName()));

提交任務的代碼用Lambda表達式可以寫為:

參數部分為空,寫為()。

當參數只有一個的時候,參數部分的括弧可以省略,比如,File還有如下方法:

public File[] listFiles(FileFilter filter)

FileFilter的定義為:

public interface FileFilter {

boolean accept(File pathname);

}

使用FileFilter重寫上面的列舉文件的例子,代碼可以為:

File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

變數引用

與匿名內部類類似,Lambda表達式也可以訪問定義在主體代碼外部的變數,但對於局部變數,它也只能訪問final類型的變數,與匿名內部類的區別是,它不要求變數聲明為final,但變數事實上不能被重新賦值。比如:

可以訪問局部變數msg,但msg不能被重新賦值,如果這樣寫:

Java編譯器會提示錯誤。

這個原因與匿名內部類是一樣的,Java會將msg的值作為參數傳遞給Lambda表達式,為Lambda表達式建立一個副本,它的代碼訪問的是這個副本,而不是外部聲明的msg變數。如果允許msg被修改,則程序員可能會誤以為Lambda表達式會讀到修改後的值,引起更多的混淆。

為什麼非要建副本,直接訪問外部的msg變數不行嗎?不行,因為msg定義在棧中,當Lambda表達式被執行的時候,msg可能早已被釋放了。如果希望能夠修改值,可以將變數定義為實例變數,或者,將變數定義為數組,比如:

與匿名內部類比較

從以上內容可以看出,Lambda表達式與匿名內部類很像,主要就是簡化了語法,那它是不是語法糖,內部實現其實就是內部類呢?答案是否定的,Java會為每個匿名內部類生成一個類,但Lambda表達式不會,Lambda表達式通常比較短,為每個表達式生成一個類會生成大量的類,性能會受到影響。

Java利用了Java 7引入的為支持動態類型語言引入的invokedynamic指令、方法句柄(method handle)等,具體實現比較複雜,我們就不探討了,感興趣可以參看http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html,我們需要知道的是,Java的實現是非常高效的,不用擔心生成太多類的問題。

Lambda表達式不是匿名內部類,那它的類型到底是什麼呢?是函數式介面。

函數式介面

Java 8引入了函數式介面的概念,函數式介面也是介面,但只能有一個抽象方法,前面提及的介面都只有一個抽象方法,都是函數式介面。之所以強調是"抽象"方法,是因為Java 8中還允許定義其他方法,我們待會會談到。Lambda表達式可以賦值給函數式介面,比如:

如果看這些介面的定義,會發現它們都有一個註解@FunctionalInterface,比如:

@FunctionalInterface

public interface Runnable {

public abstract void run();

}

對於基本類型boolean, int, long和double,為避免裝箱/拆箱,Java 8提供了一些專門的函數,比如,int相關的主要函數有:

這些函數有什麼用呢?它們被大量使用於Java 8的函數式數據處理Stream相關的類中,關於Stream,我們下節介紹。

即使不使用Stream,也可以在自己的代碼中直接使用這些預定義的函數,我們看一些簡單的示例。

Predicate示例

為便於舉例,我們先定義一個簡單的學生類Student,有name和score兩個屬性,如下所示,我們省略了getter/setter方法。

static class Student {

String name;

double score;

public Student(String name, double score) {

this.name = name;

this.score = score;

}

}

有一個學生列表:

List students = Arrays.asList(new Student[] {

new Student("zhangsan", 89d),

new Student("lisi", 89d),

new Student("wangwu", 98d) });

在日常開發中,列表處理的一個常見需求是過濾,列表的類型經常不一樣,過濾的條件也經常變化,但主體邏輯都是類似的,可以藉助Predicate寫一個通用的方法,如下所示:

public static List filter(List list, Predicate pred) {

List retList = new ArrayList();

for (E e : list) {

if (pred.test(e)) {

retList.add(e);

}

}

return retList;

}

這個方法可以這麼用:

// 過濾90分以上的

students = filter(students,t -> t.getScore() > 90);

Function示例

列表處理的另一個常見需求是轉換,比如,給定一個學生列表,需要返回名稱列表,或者將名稱轉換為大寫返回,可以藉助Function寫一個通用的方法,如下所示:

public static List map(List list, Function mapper) {

List retList = new ArrayList(list.size());

for (T e : list) {

retList.add(mapper.apply(e));

}

return retList;

}

根據學生列表返回名稱列表的代碼可以為:

List names = map(students,t -> t.getName());

將學生名稱轉換為大寫的代碼可以為:

students = map(students,t -> new Student(t.getName().toUpperCase(), t.getScore()));

Consumer示例

在上面轉換學生名稱為大寫的例子中,我們為每個學生創建了一個新的對象,另一種常見的情況是直接修改原對象,具體怎麼修改通過代碼傳遞,這時,可以用Consumer寫一個通用的方法,比如:

public static void foreach(List list, Consumer consumer) {

for (E e : list) {

consumer.accept(e);

}

}

上面轉換為大寫的例子可以改為:

foreach(students,t -> t.setName(t.getName().toUpperCase()));

以上這些示例主要用於演示函數式介面的基本概念,實際中應該使用下節介紹的流API。

方法引用

基本用法

Lambda表達式經常就是調用對象的某個方法,比如:

List names = map(students,t -> t.getName());

這時,它可以進一步簡化,如下所示:

List names = map(students,Student::getName);

Student::getName這種寫法,是Java 8引入的一種新語法,稱之為方法引用,它是Lambda表達式的一種簡寫方法,由::分隔為兩部分,前面是類名或變數名,後面是方法名。方法可以是實例方法,也可以是靜態方法,但含義不同。

我們看一些例子,還是以Student為例,先增加一個靜態方法:

public static String getCollegeName(){

return "Laoma School";

}

靜態方法

對於靜態方法,如下語句:

Supplier s =Student::getCollegeName;

等價於:

Supplier s =() -> Student.getCollegeName();

它們的參數都是空,返回類型為String。

實例方法

而對於實例方法,它第一個參數就是該類型的實例,比如,如下語句:

Function f =Student::getName;

等價於:

Function f =(Student t) -> t.getName();

對於Student::setName,它是一個BiConsumer,即:

BiConsumer c =Student::setName;

等價於:

BiConsumer c =(t, name) -> t.setName(name);

通過變數引用方法

如果方法引用的第一部分是變數名,則相當於調用那個對象的方法,比如:

Student t = new Student("張三", 89d);

Supplier s =t::getName;

等價於:

Supplier s =() -> t.getName();

而:

Consumer consumer =t::setName;

等價於:

Consumer consumer =(name) -> t.setName(name);

構造方法

對於構造方法,方法引用的語法是::new,如Student::new,如下語句:

BiFunction s =(name, score) -> new Student(name, score);

等價於:

BiFunction s =Student::new;

函數的複合

在前面的例子中,函數式介面都用作方法的參數,其他部分通過Lambda表達式傳遞具體代碼給它,函數式介面和Lambda表達式還可用作方法的返回值,傳遞代碼回調用者,將這兩種用法結合起來,可以構造複合的函數,使程序簡潔易讀。

下面我們會看一些例子,在介紹例子之前,我們先需要介紹Java 8對介面的增強。

介面的靜態方法和默認方法

在Java 8之前,介面中的方法都是抽象方法,都沒有實現體,Java 8允許在介面中定義兩類新方法:靜態方法和默認方法,它們有實現體,比如:

test()就是一個靜態方法,可以通過IDemo.test()調用。在介面不能定義靜態方法之前,相關的靜態方法往往定義在單獨的類中,比如,Collection介面有一個對應的單獨的類Collections,在Java 8中,就可以直接寫在介面中了,比如Comparator介面就定義了多個靜態方法。

hi()是一個默認方法,由關鍵字default標識,默認方法與抽象方法都是介面的方法,不同在於,它有默認的實現,實現類可以改變它的實現,也可以不改變。引入默認方法主要是函數式數據處理的需求,是為了便於給介面增加功能。

在沒有默認方法之前,Java是很難給介面增加功能的,比如List介面,因為有太多非Java JDK控制的代碼實現了該介面,如果給介面增加一個方法,則那些介面的實現就無法在新版Java 上運行,必須改寫代碼,實現新的方法,這顯然是無法接受的。函數式數據處理需要給一些介面增加一些新的方法,所以就有了默認方法的概念,介面增加了新方法,而介面現有的實現類也不需要必須實現它。

看一些例子,List介面增加了sort方法,其定義為:

defaultvoid sort(Comparator

Object[] a = this.toArray();

Arrays.sort(a, (Comparator) c);

ListIterator i = this.listIterator();

for (Object e : a) {

i.next();

i.set((E) e);

}

}

Collection介面增加了stream方法,其定義為:

default Stream stream() {

return StreamSupport.stream(spliterator(), false);

}

需要說明的是,即使能定義方法體了,介面與抽象類還是不一樣的,介面中不能定義實例變數,而抽象類可以。

了解了靜態方法和默認方法,我們看一些利用它們實現複合函數的例子。

Comparator中的複合方法

Comparator介面定義了如下靜態方法:

public static

Function

{

Objects.requireNonNull(keyExtractor);

return (Comparator & Serializable)

(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));

}

這個方法是什麼意思呢?它用於構建一個Comparator,比如,在前面的例子中,對文件按照文件名排序的代碼為:

Arrays.sort(files,(f1, f2) -> f1.getName().compareTo(f2.getName()));

使用comparing方法,代碼可以簡化為:

Arrays.sort(files,Comparator.comparing(File::getName));

這樣,代碼的可讀性是不是大大增強了?comparing方法為什麼能達到這個效果呢?它構建並返回了一個符合Comparator介面的Lambda表達式,這個Comparator接受的參數類型是File,它使用了傳遞過來的函數代碼keyExtractor將File轉換為String進行比較。像comparing這樣使用複合方式構建並傳遞代碼並不容易閱讀和理解,但調用者很方便,也很容易理解。

Comparator還有很多默認方法,我們看兩個:

default Comparator reversed() {

return Collections.reverseOrder(this);

}

default Comparator thenComparing(Comparator

Objects.requireNonNull(other);

return (Comparator & Serializable) (c1, c2) -> {

int res = compare(c1, c2);

return (res != 0) ? res : other.compare(c1, c2);

};

}

reversed返回一個新的Comparator,按原排序逆序排。thenComparing也是一個返回一個新的Comparator,在原排序認為兩個元素排序相同的時候,使用提供的other Comparator進行比較。

看一個使用的例子,將學生列表按照分數倒序排(高分在前),分數一樣的,按照名字進行排序,代碼如下所示:

students.sort(Comparator.comparing(Student::getScore)

.reversed()

.thenComparing(Student::getName));

default FunctionandThen(Function

Objects.requireNonNull(after);

return (T t) -> after.apply(apply(t));

}

先將T類型的參數轉化為類型R,再調用after將R轉換為V,最後返回類型V。

還有如下定義:

default Function compose(Function

Objects.requireNonNull(before);

return (V v) -> apply(before.apply(v));

}

對V類型的參數,先調用before將V轉換為T類型,再調用當前的apply方法轉換為R類型返回。

Consumer, Predicate等都有一些複合方法,它們大量被用於下節介紹的函數式數據處理API中,具體我們就不探討了。

小結

本節介紹了Java 8中的一些新概念,包括Lambda表達式、函數式介面、方法引用、介面的靜態方法和默認方法等。

最重要的變化是,傳遞代碼變的簡單了,函數變為了代碼世界的一等公民,可以方便的被作為參數傳遞,被作為返回值,被複合利用以構建新的函數,看上去,這些只是語法上的一些小變化,但利用這些小變化,卻能使得代碼更為通用、更為靈活、更為簡潔易讀,這,大概就是函數式編程的奇妙之處吧。

下一節,我們來探討Java 8引入的函數式數據處理API,它們大大簡化了常見的集合數據操作。

(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic,位於包shuo.laoma.java8.c91下)

持續更新,關注"老馬說編程",老馬和你一起探索編程及計算機技術的本質。用心原創,保留所有版權。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 老馬說編程 的精彩文章:

正則表達式(中)/計算機程序的思維邏輯

TAG:老馬說編程 |

您可能感興趣

【計算機程序】Node.JS初探索
Facebook開源「Detectron」,用於AR研究的計算機視覺演算法!
加速AR對象分類,Facebook開源計算機視覺演算法Detectron
量子計算機編程工具——OpenFermion
Facebook開源物體識別工具Detectron,加速計算機視覺研究
BlackHoles@Home 將利用你的計算機分析引力波
圖像處理,計算機視覺與machine learning的區別與聯繫
從hello world初探計算機系統
微軟預計於秋季推出新款surface系列計算機 與蘋果iPad抗衡
Google doodle紀念萊布尼茲:為計算機奠定基礎
AR遠程客服公司Streem收購計算機視覺公司Selerio
OpenAI背後的領袖Ilya Sutskever:一個計算機視覺、機器翻譯、遊戲和機器人的變革者
一文概覽2017年Facebook AI Research的計算機視覺研究進展
Digi-Capital報告中國AR和計算機視覺投資激增
微軟推出Windows Vision Skills預覽 解決複雜的計算機視覺問題
「Chemputer」?計算機程序控制藥物合成?
人物 | OpenAI背後的領袖Ilya Sutskever:一個計算機視覺、機器翻譯、遊戲和機器人的變革者
《Pokemon Go》開發商收購AR與計算機視覺公司Escher
Rigetti Computing蓄力 實用型量子計算機再進一步
專欄|有趣!用計算機視覺技術與PaddlePaddle打造AI控煙項目