摘要:上下文比如接受它傳遞的方法的參數,或接受它的值的局部變量中表達式需要的類型稱為目標類型。但局部變量必須顯式聲明為,或事實上是。換句話說,表達式只能捕獲指派給它們的局部變量一次。注捕獲實例變量可以被看作捕獲最終局部變量。
簡介 概念
Lambda 表達式可以理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。
匿名:它不像普通方法那樣有一個明確的名稱;
函數:Lambda 表達式是函數是因為它不像方法那樣屬于某個特定的類,但和方法一樣,Lambda 有參數列表、函數主體、返回類型,還可能有可以拋出的異常列表;
傳遞:Lambda 表達式可以作為參數傳遞給方法或存儲在變量中;
簡潔:無需像匿名類那樣寫很多模板代碼;
組成Lambda 表達式由參數列表、箭頭和 Lambda 主體組成。
(Apple o1, Apple o2) -> Integer.valueOf(o1.getWeight()).compareTo(Integer.valueOf(o2.getWeight()))
參數列表:這里采用了 Comparator 中 compareTo 方法的參數;
箭頭:箭頭把參數列表和 Lambda 主體分開;
Lambda 主體:表達式就是 Lambda 的返回值;
表達式Java8中有效的 Lambda 表達式如下:
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 |
創建對象 | () -> 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{ boolean test(T t); } public interface Comparator { 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 實踐讓我們通過一個例子,看看在實踐中如何利用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(); } }第1步:行為參數化
現在上述代碼是有局限的。你只能讀文件的第一行。如果你想要返回頭兩行,甚至是返回使用最頻繁的詞, 該怎么辦呢?在理想的情況下, 你要重用執行設置和清理的代碼, 并告訴 processFile 方法對文件執行不同的操作。是的,你需要把 processFile 的行為參數化,你需要一種方法把行為傳遞給 processFile , 以便它可以利用 BufferedReader執行不同的行為。
傳遞行為正是 Lambda 的優勢。那要是想一次讀兩行,這個新的processFile方法看起來又該是什么樣的呢? 你需要一個接收BufferedReader并返回String的Lambda。例如,下面就是從 BufferedReader 中打印兩行的寫法:
String result = processFile((BufferedReader r) -> r.readLine() +r.readLine());第2步:函數式接口傳遞行為
Lambda 僅可用于上下文是函數式接口的情況。你需要創建一個能匹配 BufferedReader -> String,還可以拋出 IOException 異常的接口。讓我們把這一接口稱為 BufferedReaderProcessor。
@FunctionalInterface public interface BufferedReaderProcessor { String process(BufferedReader reader) throws IOException; }第3步:執行一個行為
任何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 對象 } }第4步:傳遞 Lambda
現在就可以通過傳遞不同的 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 三種函數式接口。
Predicatejava.util.function.Predicate
@FunctionalInterface public interface PredicateConsumer{ boolean test(T t); } public static List filter(List list, Predicate p) { List results = new ArrayList<>(); for(T s: list){ if(p.test(s)){ results.add(s); } } return results; } Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
java.util.function.Consumer
@FunctionalInterface public interface ConsumerFunction{ void accept(T t); } public static void forEach(List list, Consumer 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 的對象。如果你需要定義一個Lambda,將輸入對象的信息映射到輸出,就可以使用這個接口(比如提取蘋果的重量,或把字符串映射為它的長度)。在下面的代碼中,我們向你展示如何利用它來創建一個map方法,以將一個String列表映射到包含每個 String長度的Integer列表。
@FunctionalInterface public interface Function原始類型特化{ R apply(T t); } public static List map(List list, Function f) { List result = new ArrayList<>(); for(T s: list) { result.add(f.apply(s)); } return result; } // [7, 2, 6] List l = map( Arrays.asList("lambdas","in","action"), (String s) -> s.length() );
Java類型要么是引用類型(比如Byte、Integer、Object、List),要么是原始類型(比如int、double、byte、char)。但是泛型(比如Consumer
Java 8為我們前面所說的函數式接口帶來了一個專門的版本,以便在輸入和輸出都是原始類型時避免自動裝箱的操作。比如,使用 IntPredicate 就避免了對值 1000 進行裝箱操作,但要是用 Predicate
下表中列出 Java 8 中常用的函數式接口:
函數式接口 | 函數描述符 | 原始類型特化 |
---|---|---|
Predicate |
T -> boolean | IntPredicate,LongPredicate, DoublePredicate |
Consumer |
T -> void | IntConsumer,LongConsumer, DoubleConsumer |
Function |
T -> R | IntFunction |
Supplier |
() -> T | BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator |
(T,T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate |
(L,R) -> boolean | |
BiConsumer |
(T,U) -> R | ObjIntConsumer |
BiFunction |
(T,U) -> R | ToIntBiFunction |
Lambda 的類型是從使用 Lambda 的上下文推斷出來的。上下文(比如接受它傳遞的方法的參數,或接受它的值的局部變量)中 Lambda 表達式需要的類型稱為目標類型。下圖表示了代碼的類型檢查過程:
類型檢查過程可以分解為如下所示:
首先,找出 filter 方法的聲明;
第二,找出目標類型 Predicate
第三,Predicate
第四,test 方法描述了一個函數描述符,它可以接受一個 Apple,并返回一個 boolean。
最后,filter 的任何實際參數都必須匹配這個要求。
同樣的 Lambda,不同的函數式接口用一個 Lambda 表達式就可以與不同的函數式接口聯系起來,只要它們的抽象方法簽名能夠兼容。比如,前面提到的 Callable 和 PrivilegeAction,這兩個接口都代表著什么也不接受且返回一個泛型 T 的函數。如下代碼所示兩個賦值時有效的:
Callablec = () -> 42; PrivilegeAction p = () -> 42;
特殊的void兼容規則如果一個Lambda的主體是一個語句表達式, 它就和一個返回void的函數描述符兼容(當然需要參數列表也兼容)。例如,以下兩行都是合法的,盡管 List 的 add 方法返回了一個 boolean,而不是 Consumer 上下文(T -> void)所要求的void:
//Predicate 返回一個 boolean Predicate類型推斷p = s -> list.add(s); //Consumer 返回一個 void Consumer b = s -> list.add(s);
Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來配合 Lambda 表達式,這意味著它也可以推斷出適合Lambda 的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型,這樣就可以在Lambda語法中省去標注參數類型。
List使用局部變量greenApples = filter(inventory, a -> "green".equals(a.getColor())); //參數a沒有顯示類型 Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //無類型推斷 Comparator 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。可以這樣做:
Supplierc1 = Apple::new; //構造函數引用指向默認的 Apple() 構造函數 Apple a1 = c1.get(); //產生一個新的對象 //等價于: Supplier c1 = () -> new Apple(); //利用默認構造函數創建 Apple 的 Lambda 表達式 Apple a1 = c1.get();
如果你的構造函數的簽名是Apple(Integer weight),那么它就適合 Function 接口的簽名,于是可以這樣寫:
Functionc2 = Apple::new; //構造函數引用指向 Apple(Integer weight) 構造函數 Apple a2 = c2.apple(100); //等價于: Function c2 = (Integer weight) -> new Apple(weight); Apple a2 = c2.apple(100);
如果你有一個具有兩個參數的構造函數Apple(String color, Integer weight),那么它就適合BiFunction接口的簽名,于是可以這樣寫:
BiFunctionLambda 和方法引用實戰 第1步:傳遞代碼c3 = Apple::new; Apple a3 = c23.apple("green", 100); //等價于: BiFunction 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第2步:使用匿名類{ @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } } apples.sort(new AppleComparator())
可以使用匿名類來改進方案,而不是實現一個 Comparator 卻只實例化一次:
apples.sort(new Comparator第3步:使用 Lambda 表達式() { @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())));第4步:使用方法引用
方法引用就是替代那些轉發參數的 Lambda 表達式的語法糖。可以用方法引 用改進方案如下:
apples.sort(Comparator.comparing(Apple::getWeight));復合 Lambda 表達式 比較器復合
逆序: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函數復合notRedApple = redApple.negate(); //蘋果是紅色并且重量大于150 Predicate redAndHeavyApple = redApple.and(a -> a.getWeight() > 150); //要么是150g以上的紅蘋果,要么是綠蘋果 Predicate redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150) .or(a -> "green".equals(a.getColor()));
Function 接口的 andThen 方法Function
Functionf = x -> x + 1; Function g = x -> x * 2; Function h = f.andThen(g); //g(f(x)) int result = h.apply(1); //result = 4
Function 接口的 Compose 方法Function
Function小結f = x -> x + 1; Function g = x -> x * 2; Function h = f.compose(g); //f(g(x)) int result = h.apply(1); //result = 3
Lambda表達式可以理解為一種匿名函數:它沒有名稱,但有參數列表、函數主體、返回 類型,可能還有一個可以拋出的異常的列表。
Lambda表達式讓你可以簡潔地傳遞代碼。
函數式接口就是僅僅聲明了一個抽象方法的接口。
只有在接受函數式接口的地方才可以使用Lambda表達式。
Lambda表達式允許你直接內聯,為函數式接口的抽象方法提供實現,并且將整個表達式作為函數式接口的一個實例。
Java 8自帶一些常用的函數式接口,放在java.util.function包里,包括Predicate
為了避免裝箱操作,對Predicate
環繞執行模式(即在方法所必需的代碼中間,你需要執行點兒什么操作,比如資源分配 和清理)可以配合 Lambda 提高靈活性和可重用性。
Lambda 表達式所需要代表的類型稱為目標類型。
方法引用讓你重復使用現有的方法實現并直接傳遞它們。
Comparator、Predicate和Function等函數式接口都有幾個可以用來結合 Lambda 表達式的默認方法。
參考資料《Java 8 實戰》
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/69922.html
摘要:歐陽思海繼承接口后,又加了新的抽象方法,這個接口就不再是函數式接口默認方法在接口中添加了一個默認方法??偨Y在這篇文章中,我們講了表達式方法引用函數式接口接口中的靜態方法接口中的默認方法的使用。 今天我來聊聊 Java8 的一些新的特性,確實 Java8 的新特性的出現,給開發者帶來了非常大的便利,可能剛剛開始的時候會有點不習慣的這種寫法,但是,當你真正的熟悉了之后,你一定會愛上這些新的...
摘要:接口有一個方法,可以返回值。在上面的代碼中,就是獲取字符串的長度,然后將每個字符串的長度作為返回值返回。 今天我們還講講Consumer、Supplier、Predicate、Function這幾個接口的用法,在 Java8 的用法當中,這幾個接口雖然沒有明目張膽的使用,但是,卻是潤物細無聲的。為什么這么說呢? 這幾個接口都在 java.util.function 包下的,分別是Con...
摘要:很多語言等從設計之初就支持表達式。注意此時外部局部變量將自動變為作為方法返回值例子返回判斷字符串是否為空判斷字符串是否為空今天關于新特性表達式就講到這里了,接下來我會繼續講述新特性之函數式接口。 上一篇文章我們了解了Java8新特性-接口默認方法,接下來我們聊一聊Java8新特性之Lambda表達式。 Lambda表達式(也稱為閉包),它允許我們將函數當成參數傳遞給某個方法,或者把代碼...
摘要:大家好,我是樂字節的小樂,上一次我們說到了核心特性之函數式接口,接下來我們繼續了解又一核心特性方法引用。方法引用是一種更簡潔易懂的表達式。感謝光臨閱讀小樂的,敬請關注樂字節后續將繼續講述等前沿知識技術。 大家好,我是樂字節的小樂,上一次我們說到了Java8核心特性之函數式接口,接下來我們繼續了解Java8又一核心特性——方法引用。 showImg(https://segmentfaul...
摘要:一表達式匿名內部類最大的問題在于其冗余的語法,比如前面的中五行代碼僅有一行是在執行任務。總結基于詞法作用域的理念,表達式不可以掩蓋任何其所在上下文的局部變量。 轉載請注明出處:https://zhuanlan.zhihu.com/p/20540175 在介紹Lambda表達式之前,我們先來看只有單個方法的Interface(通常我們稱之為回調接口): public interface...
閱讀 2993·2021-10-19 11:46
閱讀 987·2021-08-03 14:03
閱讀 2946·2021-06-11 18:08
閱讀 2915·2019-08-29 13:52
閱讀 2765·2019-08-29 12:49
閱讀 490·2019-08-26 13:56
閱讀 932·2019-08-26 13:41
閱讀 855·2019-08-26 13:35