Lambda 表達式能夠理解爲簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個能夠拋出的異常列表。java
匿名:它不像普通方法那樣有一個明確的名稱;程序員
函數:Lambda 表達式是函數是由於它不像方法那樣屬於某個特定的類,但和方法同樣,Lambda 有參數列表、函數主體、返回類型,還可能有能夠拋出的異常列表;數據庫
傳遞:Lambda 表達式能夠做爲參數傳遞給方法或存儲在變量中;編程
簡潔:無需像匿名類那樣寫不少模板代碼;app
Lambda 表達式由參數列表、箭頭和 Lambda 主體組成。ide
(Apple o1, Apple o2) -> Integer.valueOf(o1.getWeight()).compareTo(Integer.valueOf(o2.getWeight()))
參數列表:這裏採用了 Comparator 中 compareTo 方法的參數;函數
箭頭:箭頭把參數列表和 Lambda 主體分開;性能
Lambda 主體:表達式就是 Lambda 的返回值;this
Java8中有效的 Lambda 表達式以下:spa
Lambda 表達式 | 含義 |
---|---|
(String s) -> s.length() |
表達式具備一個 String 類型的參數並返回一個 int。 Lambda 沒有 return 語句,由於已經隱含的 return,能夠顯示調用 return。 |
(Apple a) -> a.getWeight() > 150 |
表達式有一個 Apple 類型的參數並返回一個 boolean 值 |
(int x, int y) -> { System.out.printn("Result"); System.out.printn(x + y)} |
表達式具備兩個 int 類型的參數而沒有返回值(void返回),Lambda 表達式能夠包含多行語句,但必需要使用大括號包起來。 |
() -> 42 |
表達式沒有參數,返回一個 int 類型的值。 |
(Apple o1, Apple o2) -> Integer.valueOf(o1.getWeight()) .compareTo (Integer.valueOf(o2.getWeight())) |
表達式具備兩個 Apple 類型的參數,返回一個 int 比較重要。 |
下面提供一些 Lambda 表達式的使用案例:
使用案例 | Lambda 示例 |
---|---|
布爾表達式 | (List<String> list) -> list.isEmpty() |
建立對象 | () -> new Apple(10) |
消費對象 | (Apple a) -> { System.out.println(a.getWeight) } |
從一個對象中選擇/抽取 | (String s) -> s.lenght() |
組合兩個值 | (int a, int b) -> a * b |
比較兩個對象 | `(Apple o1, Apple o2) -> Integer.valueOf(o1.getWeight()) .compareTo(Integer.valueOf(o2.getWeight())) |
到底在哪裏可使用 Lambda 呢?你能夠在函數式接口上使用 Lambda 表達式。
函數式接口就是隻定義一個抽象方法的接口,好比 Java API 中的 Predicate、Comparator 和 Runnable 等。
public interface Predicate<T> { boolean test(T t); } public interface Comparator<T> { int compare(T o1, T o2); } public interface Runnable { void run(); }
用函數式接口能夠幹什麼呢?Lambda 表達式容許你直接之內聯的形式爲函數式接口的抽象方法提供實現,並把整個表達式做爲函數式接口的實例(具體說來,是函數式接口一個具體實現 的實例)。你用匿名內部類也能夠完成一樣的事情,只不過比較笨拙:須要提供一個實現,而後 再直接內聯將它實例化。下面的代碼是有效的,由於Runnable是一個只定義了一個抽象方法run 的函數式接口:
//使用Lambda Runnable r1 = () -> System.out.println("Hello World 1"); //匿名類 Runnable r2 = new Runnable(){ public void run(){ System.out.println("Hello World 2"); } }; public static void process(Runnable r){ r.run(); } process(r1); //打印 "Hello World 1" process(r2); //打印 "Hello World 2" //利用直接傳遞的 Lambda 打印 "Hello World 3" process(() -> System.out.println("Hello World 3"));
函數式接口的抽象方法的簽名基本上就是 Lambda 表達式的簽名。咱們將這種抽象方法叫做函數描述符。例如,Runnable 接口能夠看做一個什麼也不接受什麼也不返回(void)的函數的簽名,由於它只有一個叫做 run 的抽象方法,這個方法什麼也不接受,什麼也不返回(void)。
讓咱們經過一個例子,看看在實踐中如何利用Lambda和行爲參數化來讓代碼更爲靈活,更爲簡潔。
資源處理(例如處理文件或數據庫)時一個常見的模式就是打開一個資源,作一些處理,而後關閉資源。這個設置和清理階段老是很相似,而且會圍繞着執行處理的那些重要代碼。這就是所謂的環繞執行(execute around)模式。
例如,在如下代碼中,高亮顯示的BufferedReader reader = new BufferedReader(new FileReader("data.txt"))
就是從一個文件中讀取一行所需的模板代碼(注意你使用了Java 7中的帶資源的try語句,它已經簡化了代碼,由於你不須要顯式地關閉資源了)。
public static String processFile() throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) { return reader.readLine(); } }
如今上述代碼是有侷限的。你只能讀文件的第一行。若是你想要返回頭兩行,甚至是返回使用最頻繁的詞, 該怎麼辦呢?在理想的狀況下, 你要重用執行設置和清理的代碼, 並告訴 processFile 方法對文件執行不一樣的操做。是的,你須要把 processFile 的行爲參數化,你須要一種方法把行爲傳遞給 processFile , 以便它能夠利用 BufferedReader執行不一樣的行爲。
傳遞行爲正是 Lambda 的優點。那要是想一次讀兩行,這個新的processFile方法看起來又該是什麼樣的呢? 你須要一個接收BufferedReader並返回String的Lambda。例如,下面就是從 BufferedReader 中打印兩行的寫法:
String result = processFile((BufferedReader r) -> r.readLine() +r.readLine());
Lambda 僅可用於上下文是函數式接口的狀況。你須要建立一個能匹配 BufferedReader -> String
,還能夠拋出 IOException 異常的接口。讓咱們把這一接口稱爲 BufferedReaderProcessor。
@FunctionalInterface public interface BufferedReaderProcessor { String process(BufferedReader reader) throws IOException; }
任何BufferedReader -> String
形式的 Lambda 均可以做爲參數來傳遞,由於它們符合 BufferedReaderProcessor 接口中定義的 process 方法的簽名。如今只須要編寫一種方法在 processFile主體內執行 Lambda 所表明的代碼。
public static String processFile(BufferedReaderProcessor processor) throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) { return processor.process(reader); //處理 BufferedReader 對象 } }
如今就能夠經過傳遞不一樣的 Lambda 重用 processFile 方法,並以不一樣的方式處理文件了。
//打印一行 String result = processFile((BufferedReader r) -> r.readLine()); System.out.println(result); //打印2行 result = processFile((BufferedReader r) -> r.readLine() +r.readLine());
Java 8的庫幫你在java.util.function
包中引入了幾個新的函數式接口。咱們接下來介紹 Predicate、Consumer和Function 三種函數式接口。
java.util.function.Predicate<T>
接口定義了一個名叫 test 的抽象方法,它接受泛型 T對象,並返回一個 boolean。這偏偏和你先前建立的同樣,如今就能夠直接使用了。在你須要 表示一個涉及類型T的布爾表達式時,就可使用這個接口。好比,你能夠定義一個接受String 對象的Lambda表達式,以下所示。
@FunctionalInterface public interface Predicate<T>{ boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p) { List<T> results = new ArrayList<>(); for(T s: list){ if(p.test(s)){ results.add(s); } } return results; } Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
java.util.function.Consumer<T>
定義了一個名叫 accept 的抽象方法,它接受泛型 T 的對象,沒有返回(void)。你若是須要訪問類型T的對象,並對其執行某些操做,就可使用 這個接口。好比,你能夠用它來建立一個forEach方法,接受一個Integers的列表,並對其中 每一個元素執行操做。在下面的代碼中,你就可使用這個forEach方法,並配合Lambda來打印 列表中的全部元素。
@FunctionalInterface public interface Consumer<T> { void accept(T t); } public static <T> void forEach(List<T> list, Consumer<T> c){ for(T i: list){ c.accept(i); } } forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) );
java.util.function.Function<T, R>
接口定義了一個叫做apply的方法,它接受一個
泛型 T 的對象,並返回一個泛型 R 的對象。若是你須要定義一個Lambda,將輸入對象的信息映射到輸出,就可使用這個接口(好比提取蘋果的重量,或把字符串映射爲它的長度)。在下面的代碼中,咱們向你展現如何利用它來建立一個map方法,以將一個String列表映射到包含每一個 String長度的Integer列表。
@FunctionalInterface public interface Function<T, R>{ R apply(T t); } public static <T, R> List<R> map(List<T> list, Function<T, R> f) { List<R> result = new ArrayList<>(); for(T s: list) { result.add(f.apply(s)); } return result; } // [7, 2, 6] List<Integer> l = map( Arrays.asList("lambdas","in","action"), (String s) -> s.length() );
Java類型要麼是引用類型(好比Byte、Integer、Object、List),要麼是原始類型(好比int、double、byte、char)。可是泛型(好比Consumer<T>中的T)只能綁定到引用類型。這是由泛型內部的實現方式形成的。所以,在Java裏有一個將原始類型轉換爲對應的引用類型的機制。這個機制叫做裝箱(boxing)。相反的操做,也就是將引用類型轉換爲對應的原始類型,叫做拆箱(unboxing)。Java還有一個自動裝箱機制來幫助程序員執行這一任務:裝箱和拆箱操做是自動完成的。好比一個int被裝箱成爲 Integer。但這在性能方面是要付出代價的。裝箱後的值本質上就是把原始類型包裹起來,並保存在堆裏。所以,裝箱後的值須要更多的內存,並須要額外的內存搜索來獲取被包裹的原始值。
Java 8爲咱們前面所說的函數式接口帶來了一個專門的版本,以便在輸入和輸出都是原始類型時避免自動裝箱的操做。好比,使用 IntPredicate 就避免了對值 1000 進行裝箱操做,但要是用 Predicate<Integer> 就會把參數 1000 裝箱到一個 Integer 對象中。通常來講,針對專門的輸入參數類型的函數式接口的名稱都要加上對應的原始類型前綴,比 如 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function 接口還有針對輸出參數類型的變種:ToIntFunction<T>、IntToDoubleFunction等。
下表中列出 Java 8 中經常使用的函數式接口:
函數式接口 | 函數描述符 | 原始類型特化 |
---|---|---|
Predicate<T> |
T -> boolean | IntPredicate,LongPredicate, DoublePredicate |
Consumer<T> |
T -> void | IntConsumer,LongConsumer, DoubleConsumer |
Function<T,R> |
T -> R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T> |
() -> T | BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator<T> |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator<T> |
(T,T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L,R> |
(L,R) -> boolean | |
BiConsumer<T,U> |
(T,U) -> R | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
BiFunction<T,U,R> |
(T,U) -> R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
Lambda 的類型是從使用 Lambda 的上下文推斷出來的。上下文(好比接受它傳遞的方法的參數,或接受它的值的局部變量)中 Lambda 表達式須要的類型稱爲目標類型。下圖表示了代碼的類型檢查過程:
類型檢查過程能夠分解爲以下所示:
首先,找出 filter 方法的聲明;
第二,找出目標類型 Predicate<Apple>
。
第三,Predicate<Apple>
是一個函數式接口,定義了一個叫做 test 的抽象方法。
第四,test 方法描述了一個函數描述符,它能夠接受一個 Apple,並返回一個 boolean。
最後,filter 的任何實際參數都必須匹配這個要求。
用一個 Lambda 表達式就能夠與不一樣的函數式接口聯繫起來,只要它們的抽象方法簽名可以兼容。好比,前面提到的 Callable 和 PrivilegeAction,這兩個接口都表明着什麼也不接受且返回一個泛型 T 的函數。以下代碼所示兩個賦值時有效的:
Callable<Integer> c = () -> 42; PrivilegeAction<Integer> p = () -> 42;
特殊的void兼容規則若是一個Lambda的主體是一個語句表達式, 它就和一個返回void的函數描述符兼容(固然須要參數列表也兼容)。例如,如下兩行都是合法的,儘管 List 的 add 方法返回了一個 boolean,而不是 Consumer 上下文(T -> void)所要求的void:
//Predicate 返回一個 boolean Predicate<String> p = s -> list.add(s); //Consumer 返回一個 void Consumer<String> b = s -> list.add(s);
Java編譯器會從上下文(目標類型)推斷出用什麼函數式接口來配合 Lambda 表達式,這意味着它也能夠推斷出適合Lambda 的簽名,由於函數描述符能夠經過目標類型來獲得。這樣作的好處在於,編譯器能夠了解Lambda表達式的參數類型,這樣就能夠在Lambda語法中省去標註參數類型。
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor())); //參數a沒有顯示類型 Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //無類型推斷 Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); //類型推斷
Lambda表達式也容許使用自由變量(不是參數,而是在外層做用域中定義的變量),就像匿名類同樣。 它們被稱做捕獲Lambda。例如,下面的Lambda捕獲了portNumber變量:
int num = 1337; Runnable r = () -> System.out.println(num);
Lambda能夠沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須顯式聲明爲final, 或事實上是final。換句話說,Lambda表達式只能捕獲指派給它們的局部變量一次。(注:捕獲 實例變量能夠被看做捕獲最終局部變量this。) 例如,下面的代碼沒法編譯,由於portNumber 變量被賦值兩次:
int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); portNumber = 31337; //錯誤:Lambda表達式引用的局 部變量必須是最終的(final) 或事實上最終的
爲何局部變量有這些限制?第一,實例變量和局部變量背後的實現有一 個關鍵不一樣。實例變量都存儲在堆中,而局部變量則保存在棧上。若是Lambda能夠直接訪問局部變量,並且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回以後,去訪問該變量。所以,Java在訪問自由局部變量時,其實是在訪問它的副本,而不是訪問原始變量。若是局部變量僅僅賦值一次那就沒有什麼區別了——所以就有了這個限制。第二,這一限制不鼓勵你使用改變外部變量的典型命令式編程模式(這種模式會阻礙很容易作到的並行處理)。
方法引用讓你能夠重複使用現有的方法定義,並像Lambda同樣傳遞它們。在一些狀況下,比起使用 Lambda 表達式,它們彷佛更易讀,感受也更天然。下面就是咱們藉助更新的Java 8 API,用方法引用寫的一個排序的例子:
lists.sort(comparing(Apple::getWeight);
方法引用能夠被看做僅僅調用特定方法的Lambda的一種快捷寫法。它的基本思想是,若是一個Lambda表明的只是「直接調用這個方法」,那最好仍是用名稱來調用它,而不是去描述如何調用它。事實上,方法引用就是讓你根據已有的方法實現來建立 Lambda表達式。可是,顯式地指明方法的名稱,你的代碼的可讀性會更好。它是如何工做的呢? 當你須要使用方法引用時, 目標引用放在分隔符 :: 前, 方法的名稱放在後面。 例如, Apple::getWeight
就是引用了Apple類中定義的方法getWeight
。請記住,不須要括號,由於 你沒有實際調用這個方法。方法引用就是Lambda表達式(Apple a) -> a.getWeight()
的快捷寫法,下表給出了Java 8中方法引用的其餘一些例子。
Lambda | 等效的引用方法 |
---|---|
(Apple a) -> a.getWeight() |
Apple::getWeight |
() -> Thread.currentThread().dumpStack() |
Thread.currentThread()::dumpStack |
(str,i) -> str.substring(i) |
String::substring |
(String i) -> System.out.println(s) |
System.out::println |
方法引用主要分爲三類:
指向靜態方法的引用(例如 Integer 的 parseInt 方法,寫做 Integer::parseInt
)
指向任意類型實例方法的方法引用(例如 String 的 length 方法,寫做 String::length
)
指向現有對象的實例方法的引用(假設有一個局部變量 expensiveTransaction 用於存放 Transaction 類型的對象,它支持實例方法 getValue,那麼就能夠寫 expensiveTransaction::getValue
)
注意,編譯器會進行一種與Lambda表達式相似的類型檢查過程,來肯定對於給定的函數 式接口,這個方法引用是否有效:方法引用的簽名必須和上下文類型匹配。
對於一個現有構造函數,能夠利用它的名稱和關鍵字 new 來建立它的一個引用: ClassName::new
。它的功能與指向靜態方法的引用相似。
例如,假設有一個構造函數沒有參數。 它適合 Supplier 的簽名() -> Apple
。能夠這樣作:
Supplier<Apple> c1 = Apple::new; //構造函數引用指向默認的 Apple() 構造函數 Apple a1 = c1.get(); //產生一個新的對象 //等價於: Supplier<Apple> c1 = () -> new Apple(); //利用默認構造函數建立 Apple 的 Lambda 表達式 Apple a1 = c1.get();
若是你的構造函數的簽名是Apple(Integer weight)
,那麼它就適合 Function 接口的簽名,因而能夠這樣寫:
Function<Integer, Apple> c2 = Apple::new; //構造函數引用指向 Apple(Integer weight) 構造函數 Apple a2 = c2.apple(100); //等價於: Function<Integer, Apple> c2 = (Integer weight) -> new Apple(weight); Apple a2 = c2.apple(100);
若是你有一個具備兩個參數的構造函數Apple(String color, Integer weight)
,那麼它就適合BiFunction接口的簽名,因而能夠這樣寫:
BiFunction<Integer, Integer, Apple> c3 = Apple::new; Apple a3 = c23.apple("green", 100); //等價於: BiFunction<Integer, Apple> c3 = (String color, Integer weight) -> new Apple(color, weight); Apple a3 = c3.apple("green", 100);
Java 8的API已經爲你提供了一個 List 可用的 sort 方法,那麼如何把排序策略傳遞給 sort 方法呢?sort方法的簽名是這樣的:
void sort(Comparator<? super E> c)
它須要一個 Comparator 對象來比較兩個Apple!這就是在Java中傳遞策略的方式:它們必須包裹在一個對象裏。咱們說 sort 的行爲被參數化了:傳遞給它的排序策略不一樣,其行爲也會 不一樣。
第一個解決方案能夠是這樣的:
public class AppleComparator implements Comparator<Apple> { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } } apples.sort(new AppleComparator())
可使用匿名類來改進方案,而不是實現一個 Comparator 卻只實例化一次:
apples.sort(new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } });
接下來使用 Lambda 表達式來改進方案:
apples.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator 具備一個叫做 comparing 的靜態輔助方法,它能夠接受一個 Function 來提取 Comparable 鍵值,並生成一個 Comparator 對象,它能夠像下面這樣用(注意你如今傳遞的Lambda只有一 個參數:Lambda說明了如何從蘋果中提取須要比較的鍵值):
apples.sort(Comparator.comparing(((Apple apple) -> apple.getWeight())));
方法引用就是替代那些轉發參數的 Lambda 表達式的語法糖。能夠用方法引 用改進方案以下:
apples.sort(Comparator.comparing(Apple::getWeight));
逆序:Comparator 接口有一個默認方法 reversed 可使給定的比較器逆序。
apples.sort(Comparator.comparing(Apple::getWeight).reversed()); //按重量遞減排序
比較器鏈:Comparator 接口的 thenComparing 方法接受一個函數做爲參數(就像 comparing方法同樣),若是兩個對象用第一個Comparator比較以後是相等的,就提供第二個 Comparator。
apples.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor)); //按重量遞減排序,同樣重時,按顏色排序
謂詞接口包括三個方法:negate、and和or。
//蘋果不是紅的 Predicate<Apple> notRedApple = redApple.negate(); //蘋果是紅色而且重量大於150 Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150); //要麼是150g以上的紅蘋果,要麼是綠蘋果 Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150) .or(a -> "green".equals(a.getColor()));
Function 接口的 andThen 方法Function<T, V> andThen(Function<? super R, ? extends V> after)
會返回一個函數,它先計算 andThen 的調用函數,將輸入函數的結果應用於 andThen 方法的 after 函數。
Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.andThen(g); //g(f(x)) int result = h.apply(1); //result = 4
Function 接口的 Compose 方法Function<V, R> compose(Function<? super V, ? extends T> before)
先計算 compose 的參數裏面給的那個函數,而後再把結果用於 compose 的調用函數。
Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.compose(g); //f(g(x)) int result = h.apply(1); //result = 3
Lambda表達式能夠理解爲一種匿名函數:它沒有名稱,但有參數列表、函數主體、返回 類型,可能還有一個能夠拋出的異常的列表。
Lambda表達式讓你能夠簡潔地傳遞代碼。
函數式接口就是僅僅聲明瞭一個抽象方法的接口。
只有在接受函數式接口的地方纔可使用Lambda表達式。
Lambda表達式容許你直接內聯,爲函數式接口的抽象方法提供實現,而且將整個表達式做爲函數式接口的一個實例。
Java 8自帶一些經常使用的函數式接口,放在java.util.function
包裏,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>
和BinaryOperator<T>
。
爲了不裝箱操做,對Predicate<T>
和Function<T, R>
等通用函數式接口的原始類型特化:IntPredicate、IntToLongFunction等。
環繞執行模式(即在方法所必需的代碼中間,你須要執行點兒什麼操做,好比資源分配 和清理)能夠配合 Lambda 提升靈活性和可重用性。
Lambda 表達式所須要表明的類型稱爲目標類型。
方法引用讓你重複使用現有的方法實現並直接傳遞它們。
Comparator、Predicate和Function等函數式接口都有幾個能夠用來結合 Lambda 表達式的默認方法。
《Java 8 實戰》