大話 Java 中的 Lambda 表達式丨【奔跑吧!Java】(大話西游)
2014年3月18日,Oracle公司發布了Java SE 8。最近正好抽空整理了Java 8的特性如下:
接口的默認方法
Lambda 表達式
函數式接口
方法與構造函數引用
Lambda 作用域
訪問局部變量
訪問對象字段與靜態變量
訪問接口的默認方法
Date API
Annotation 注解
本文將會重點講解Java 8中的Lambda表達式,其他特性將會在后續文章中講解。lambda 表達式,又被成為“閉包”或“匿名方法”。
背景
Java 是一門面向對象編程語言。面向對象編程語言和函數式編程語言中的基本元素(Basic Values)都可以動態封裝程序行為:面向對象編程語言使用帶有方法的對象封裝行為,函數式編程語言使用函數封裝行為。但這個相同點并不明顯,因為Java 對象往往比較“重量級”:實例化一個類型往往會涉及不同的類,并需要初始化類里的字段和方法。
不過有些 Java 對象只是對單個函數的封裝。例如下面這個典型用例:Java API 中定義了一個接口(一般被稱為回調接口),用戶通過提供這個接口的實例來傳入指定行為。
public interface ActionListener { void actionPerformed(ActionEvent e); }
這里并不需要專門定義一個類來實現 ActionListener,因為它只會在調用處被使用一次。用戶一般會使用匿名類型把行為內聯(inline):
button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ui.dazzle(e.getModifiers()); } });
很多庫都依賴于上面的模式。對于并行 API 更是如此,因為我們需要把待執行的代碼提供給并行 API,并行編程是一個非常值得研究的領域,因為在這里摩爾定律得到了重生:盡管我們沒有更快的 CPU 核心(core),但是我們有更多的 CPU 核心。而串行 API 就只能使用有限的計算能力。
匿名內部類
隨著回調模式和函數式編程風格的日益流行,我們需要在Java中提供一種盡可能輕量級的將代碼封裝為數據(Model code as data)的方法。匿名內部類并不是一個好的 選擇,因為:
語法過于冗余
匿名類中的 this 和變量名容易使人產生誤解
類型載入和實例創建語義不夠靈活
無法捕獲非 final 的局部變量
無法對控制流進行抽象
函數式接口
盡管匿名內部類有著種種限制和問題,但是它有一個良好的特性,它和Java類型系統結合的十分緊密:每一個函數對象都對應一個接口類型。之所以說這個特性是良好的,是因為:
接口是 Java 類型系統的一部分
接口天然就擁有其運行時表示(Runtime representation)
接口可以通過 Javadoc 注釋來表達一些非正式的協定(contract),例如,通過注釋說明該操作應可交換(commutative)
接口只有一個方法,大多數回調接口都擁有這個特征:比如 Runnable 接口和 Comparator 接口。我們把這些只擁有一個方法的接口稱為 函數式接口。(之前它們被稱為 SAM類型,即 單抽象方法類型(Single Abstract Method))
實現函數式類型的另一種方式是引入一個全新的 結構化 函數類型,我們也稱其為“箭頭”類型。例如,一個接收 String 和 Object 并返回 int 的函數類型可以被表示為 (String, Object) -> int。我們仔細考慮了這個方式,但出于下面的原因,最終將其否定:
它會為Java類型系統引入額外的復雜度,并帶來 結構類型(Structural Type) 和 指名類型(Nominal Type) 的混用。(Java 幾乎全部使用指名類型)
它會導致類庫風格的分歧——一些類庫會繼續使用回調接口,而另一些類庫會使用結構化函數類型
它的語法會變得十分笨拙,尤其在包含受檢異常(checked exception)之后
每個函數類型很難擁有其運行時表示,這意味著開發者會受到 類型擦除(erasure) 的困擾和局限。比如說,我們無法對方法 m(T->U) 和 m(X->Y) 進行重載(Overload)
所以我們選擇了“使用已知類型”這條路——因為現有的類庫大量使用了函數式接口,通過沿用這種模式,我們使得現有類庫能夠直接使用 lambda 表達式。例如下面是 Java SE 7 中已經存在的函數式接口:
java.lang.Runnable java.util.concurrent.Callable java.security.PrivilegedAction java.util.Comparator java.io.FileFilter java.beans.PropertyChangeListener
除此之外,Java SE 8中增加了一個新的包:java.util.function,它里面包含了常用的函數式接口,例如:
Predicate
除了上面的這些基本的函數式接口,我們還提供了一些針對原始類型(Primitive type)的特化(Specialization)函數式接口,例如 IntSupplier 和 LongBinaryOperator。(我們只為 int、long 和 double 提供了特化函數式接口,如果需要使用其它原始類型則需要進行類型轉換)同樣的我們也提供了一些針對多個參數的函數式接口,例如 BiFunction
lambda表達式
匿名類型最大的問題就在于其冗余的語法。有人戲稱匿名類型導致了“高度問題”。lambda表達式是匿名方法,它提供了輕量級的語法,從而解決了匿名內部類帶來的“高度問題”。
(int x, int y) -> x + y () -> 42 (String s) -> { System.out.println(s); }
第一個 lambda 表達式接收 x 和 y 這兩個整形參數并返回它們的和;第二個 lambda 表達式不接收參數,返回整數 ‘42’;第三個 lambda 表達式接收一個字符串并把它打印到控制臺,不返回值。
lambda 表達式的語法由參數列表、箭頭符號 -> 和函數體組成。函數體既可以是一個表達式,也可以是一個語句塊:
表達式:表達式會被執行然后返回執行結果。
語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣——
return 語句會把控制權交給匿名方法的調用者
break 和 continue 只能在循環中使用
如果函數體有返回值,那么函數體內部的每一條路徑都必須返回值
表達式函數體適合小型 lambda 表達式,它消除了 return 關鍵字,使得語法更加簡潔。
lambda 表達式也會經常出現在嵌套環境中,比如說作為方法的參數。為了使 lambda 表達式在這些場景下盡可能簡潔,我們去除了不必要的分隔符。不過在某些情況下我們也可以把它分為多行,然后用括號包起來,就像其它普通表達式一樣。
實戰應用
Function
Function 接口有一個參數并且返回一個結果,并附帶了一些可以和其他函數組合的默認方法:andThen和compose。
@Test public void testFun() { //Function 接口有一個參數并且返回一個結果 Function
compose和andThen中定義的Function應用順序正好相反,首先應用compose中的方法,其次才會應用當前Function。
Supplier
Supplier 接口返回一個任意范型的值,和Function接口不同的是該接口沒有任何參數。代碼如下:
@Test public void testSupplier() { //Supplier 接口返回一個任意范型的值,和Function接口不同的是該接口沒有任何參數 Supplier sp = () -> "sp"; System.out.println(sp.get()); }
如上代碼將會返回一個字符串“sp”,通過get方法獲取到返回的值。
Predicate
Predicate 接口只有一個參數,返回boolean類型。該接口包含多種默認方法來將Predicate組合成其他復雜的邏輯(比如:與,或,非):
@Test public void testPredicate() { Predicate
如上代碼判斷了字符串是否為空,并應用了與、非操作。
Consumer
Consumer 接口表示執行在單個參數上的操作,主要的方法為andThen和accept。
@Test public void testConsumer() { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式 Consumer
accept表示接收指定的參數執行操作,andThen表示當前操作結束之后附加的操作。
Comparator
Comparator接口用于比較, Java 8在此之上添加了多種默認方法,如reversed和thenComparing等。
@Test public void testComparator() { Comparator
Optional
用來防止NullPointerException異常的輔助類型,現在看看這個接口能干什么:
Optional被定義為一個簡單的容器,其值可能是null或者不是null。在Java 8之前一般某個函數應該返回非空對象但是偶爾卻可能返回了null,而在Java 8中,不推薦你返回null而是返回Optional。
@Test public void testOptional() { //用來防止NullPointerException異常的輔助類型 List
optional.orElse用來對異常情況返回預設的返回結果。
Stream
java.util.Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分為中間操作或者最終操作兩種,最終操作返回一特定類型的計算結果,而中間操作返回Stream本身,這樣你就可以將多個操作依次串起來。Stream 的創建需要指定一個數據源,比如 java.util.Collection的子類,List或者Set, Map不支持。
@Test public void testSort() { List
Map
中間操作map會將元素根據指定的Function接口來依次將元素轉成另外的對象,下面的示例展示了將字符串轉換為大寫字符串。你也可以通過map來講對象轉換成其他類型,map返回的Stream類型是根據你map傳遞進去的函數的返回值決定的。
@Test public void testMap() { List
Match
Stream提供了多種匹配操作,允許檢測指定的Predicate是否匹配整個Stream。所有的匹配操作都是最終操作,并返回一個boolean類型的值。
@Test public void testMatch() { List
Reduce
這是一個最終操作,允許通過指定的函數來講stream中的多個元素規約為一個元素,規越后的結果是通過Optional接口表示的:
@Test public void testReduce() { List
ParallelStream
串行Stream上的操作是在一個線程中依次完成,而并行Stream則是在多個線程上同時執行。
@Test public void testParallelStream() { int max = 1000000; List
如上所示為并行排序,排序這個Stream耗時明顯低于串行。
Map方法
Map類型不支持stream,不過Map提供了一些新的有用的方法來處理一些日常任務。
@Test public void testMapFun() { Map
UnaryOperator
繼承自Function接口,表示對單個操作數的操作,該操作生成與其操作數相同類型的結果。
@Test public void testUnaryOperator() { UnaryOperator
小結
本文主要介紹了Java8中的Lambda表達式,選擇其中常用的方法進行了簡單的應用講解。Lambda表達式是Java SE 8中一個重要的新特性。Lambda表達式允許你通過表達式來代替功能接口。Lambda表達式就和方法一樣,它提供了一個正常的參數列表和一個使用這些參數的主體(body,可以是一個表達式或一個代碼塊)。
Lambda表達式還增強了集合庫,包括java.util.function 包以及java.util.stream包。Lambda表達式非常簡潔,大大簡化代碼行數,使代碼在一定程度上變的簡潔干凈,但是同樣的,這可能也會是一個缺點,由于省略了太多東西,代碼可讀性有可能在一定程度上會降低,這個完全取決于你使用lambda表達式的位置所設計的API是否被你的代碼的其他閱讀者所熟悉。
參考文檔
Java 8中一些常用的全新的函數式接口
深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)
【奔跑吧!JAVA】有獎征文火熱進行中:https://bbs.huaweicloud.com/blogs/265241
API Java 面向對象編程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。