《Java8实战》笔记(03):Lambda表达式
本文源碼
Lambda 管中窺豹
可以把Lambda表達式理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。
Lambda表達式可以讓你十分簡明地傳遞代碼。
Lambda組成結構
- 參數列表——這里它采用了Comparator中compare方法的參數,兩個Apple。
- 箭頭——箭頭->把參數列表與Lambda主體分隔開。
- Lambda主體——比較兩個Apple的重量。表達式就是Lambda的返回值了。
Lambda的基本語法是
(parameters) -> expression或(請注意語句的花括號)
(parameters) -> { statements; }Java8先前:
Comparator<Apple> byWeight = new Comparator<Apple>() {public int compare(Apple a1, Apple a2){return a1.getWeight().compareTo(a2.getWeight());} };Java8之后(用了Lambda表達式):
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());Java 8中有效的Lambda表達式
(String s) -> s.length()(Apple a) -> a.getWeight() > 150(int x, int y) -> {System.out.println("Result:");System.out.println(x+y); }() -> 42(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())| 布爾表達式 | (List list) -> list.isEmpty() |
| 創建對象 | () -> new Apple(10) |
| 消費一個對象 | (Apple a) -> {System.out.println(a.getWeight());} |
| 從一個對象中選擇/抽取 | (String s) -> s.length() |
| 組合兩個值 | (int a, int b) -> a * b |
| 比較兩個對象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
在哪里以及如何使用Lambda
你可以在函數式接口上使用Lambda表達式。
在上面的代碼中, 你可以把Lambda 表達式作為第二個參數傳給filter 方法, 因為它這里需要Predicate<T>,而這是一個函數式接口。
函數式接口
Predicate僅僅定義了一個抽象方法
public interface Predicate<T>{boolean test (T t); }一言以蔽之,函數式接口就是只定義一個抽象方法的接口。
Java API中的一些其他函數式接口,
//java.util.Comparator public interface Comparator<T> {int compare(T o1, T o2); }//java.lang.Runnable public interface Runnable{void run(); }//java.awt.event.ActionListener public interface ActionListener extends EventListener{void actionPerformed(ActionEvent e); }//java.util.concurrent.Callable public interface Callable<V>{V call(); }//java.security.PrivilegedAction public interface PrivilegedAction<V>{V run(); }用函數式接口可以干什么呢?Lambda表達式允許你直接以內聯的形式為函數式接口的抽象方法提供實現,并把整個表達式作為函數式接口的實例(具體說來,是函數式接口一個具體實現的實例)。
你用匿名內部類也可以完成同樣的事情,只不過比較笨拙:需要提供一個實現,然后再直接內聯將它實例化。下面的代碼是有效的,因為Runnable是一個只定義了一個抽象方法run的函數式接口
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); process(r2);process(() -> System.out.println("Hello World 3"));函數描述符
函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名。我們將這種抽象方法叫作函數描述符
為什么只有在需要函數式接口的時候才可以傳遞Lambda呢?語言設計者選擇了現在這種方式,因為這種方式自然且能避免語言變得更復雜。
@FunctionalInterface
如果你去看看新的Java API,會發現函數式接口帶有@FunctionalInterface的標注。這個標注用于表示該接口會設計成一個函數式接口。
如果你用@FunctionalInterface定義了一個接口,而它卻不是函數式接口的話,編譯器將返回一個提示原因的錯誤。
例如,錯誤消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多個抽象方法。
請注意,@FunctionalInterface不是必需的,但對于為此設計的接口而言,使用它是比較好的做法。它就像是@Override標注表示方法被重寫了。
把Lambda 付諸實踐:環繞執行模式
資源處理(例如處理文件或數據庫)時一個常見的模式就是打開一個資源,做一些處理,然后關閉資源。
這個設置和清理階段總是很類似,并且會圍繞著執行處理的那些重要代碼。這就是所謂的環繞執行(execute around)模式
public static String processFile() throws IOException {try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {return br.readLine();//這就是做有用工作的那行代碼} }把processFile的行為參數化。需要一種方法把行為傳遞給processFile,以便它可以利用BufferedReader執行不同的行為。
第1步:記得行為參數化
從
public static String processFile() throws IOException {try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {return br.readLine();//這就是做有用工作的那行代碼} }轉化成
String result = processFile((BufferedReader br) -> br.readLine());第2步:使用函數式接口來傳遞行為
@FunctionalInterface public interface BufferedReaderProcessor {String process(BufferedReader b) throws IOException; }現在你就可以把這個接口作為新的processFile方法的參數了:
public static String processFile(BufferedReaderProcessor p) throws IOException {… }第3步:執行一個行為
public static String processFile(BufferedReaderProcessor p) throws IOException {try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {return p.process(br);} }第4步:傳遞Lambda
現在你就可以通過傳遞不同的Lambda重用processFile方法,并以不同的方式處理文件了。
處理一行:
String oneLine = processFile((BufferedReader br) -> br.readLine());處理兩行:
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());使用函數式接口
函數式接口的抽象方法的簽名稱為函數描述符。所以為了應用不同的Lambda表達式,你需要一套能夠描述常見函數描述符的函數式接口。
Predicate
java.util.function.Predicate<T>接口定義了一個名叫test的抽象方法,它接受泛型T對象,并返回一個boolean。
@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);Consumer
java.util.function.Consumer定義了一個名叫accept的抽象方法,它接受泛型T的對象,沒有返回(void)。你如果需要訪問類型T的對象,并對其執行某些操作,就可以使用這個接口。比如,你可以用它來創建一個forEach方法,接受一個Integers的列表,并對其中每個元素執行操作。
@FunctionalInterface public interface Consummer<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,6,7), (Integer i)->System.out.println(i));Function
java.util.function.Function<T, R>接口定義了一個叫作apply的方法,它接受一個泛型T的對象,并返回一個泛型R的對象。如果你需要定義一個Lambda,將輸入對象的信息映射到輸出,就可以使用這個接口(比如提取蘋果的重量,或把字符串映射為它的長度)
@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; }List<Integer> list2 = map(Arrays.asList("","1234","asd"),(String s)->s.length()); System.out.println(list2);原始類型特化
泛型(比如Consumer<T>中的T)只能綁定到引用類型。這是由泛型內部的實現方式造成的。因此,在Java里有一個將原始類型轉換為對應的引用類型的機制。這個機制叫作裝箱(boxing)。相反的操作,也就是將引用類型轉換為對應的原始類型,叫作拆箱(unboxing)。
但這在性能方面是要付出代價的。裝箱后的值本質上就是把原始類型包裹起來,并保存在堆里。因此,裝箱后的值需要更多的內存,并需要額外的內存搜索來獲取被包裹的原始值。
Java 8 提供特殊的函數式接口,以便在輸入和輸出都是原始類型避免自動裝箱操作
public interface IntPredicate{boolean test(int t); }//無裝箱 IntPredicate evenNumbers = (int i) -> i % 2 == 0; evenNumbers.test(1000);//裝箱 Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1; oddNumbers.test(1000);Java 8 中的常用函數式接口
//助記//喂,消費者,生產者,我們一起玩(fun)吧 Predicate,Consumer,Supplier,Function//數學 一元符,二元符 UnaryOperator,BinaryOperator//(喂,消費者,生產者)* 2 BiPredicate,BiConsumer,BiFunction| 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,R> | ()->T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
| UnaryOperator<T,R> | T->T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
| BinaryOperator<T,R> | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
| BiPredicate<L,R> | (L,R)->boolean | - |
| BiConsumer<T,U> | (T,U)->void | ObjIntConsumer<T,R>, ObjLongConsumer<T,R>, ObjDoubleConsumer<T,R> |
| BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
Lambda示例
| 布爾表達式 | (List<String> list) -> list.isEmpty() | Predicate<List<String>> |
| 創建對象 | () -> new Apple(10) | Supplier<Apple> |
| 消費一個對象 | (Apple a) -> { System.out.println(a.getWeight()); } | Consumer<Apple> |
| 從一個對象中選擇/抽取 | (String s) -> s.length() | Function<String, Integer>或 ToIntFunction<String> |
| 組合兩個值 | (int a, int b) -> a * b | IntBinaryOperator |
| 比較兩個對象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator或 BiFunction<Apple, Apple, Integer> 或 ToIntBiFunction<Apple, Apple> |
異常
任何函數式接口都不允許拋出受檢異常(checked exception)。如果你需要Lambda表達式來拋出異常,有兩種辦法:
類型檢查、類型推斷以及限制
類型檢查
Lambda的類型是從使用Lambda的上下文推斷出來的。上下文(比如,接受它傳遞的方法的參數,或接受它的值的局部變量)中Lambda表達式需要的類型稱為目標類型。
- 首先,你要找出filter方法的聲明。
- 第二,要求它是Predicate(目標類型)對象的第二個正式參數。
- 第三,Predicate是一個函數式接口,定義了一個叫作test的抽象方法。
- 第四,test方法描述了一個函數描述符,它可以接受一個Apple,并返回一個boolean。
- 最后,filter的任何實際參數都必須匹配這個要求。
同樣的Lambda,不同的函數式接口
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());特殊的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);類型檢查——為什么下面的代碼不能編譯呢?
你該如何解決這個問題呢?
答案:Lambda表達式的上下文是Object(目標類型)。但Object不是一個函數式接口。
為了解決這個問題,你可以把目標類型改成Runnable,它的函數描述符是() -> void:
類型推斷
Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來配合Lambda表達式,這意味著它也可以推斷出適合Lambda的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型,這樣就可以在Lambda語法中省去標注參數類型。
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));Lambda表達式有多個參數,代碼可讀性的好處就更為明顯。例如,你可以這樣來創建一個Comparator對象
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());//有類型推斷 Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());使用局部變量
不鼓勵使用外部變量
Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。 它們被稱作捕獲Lambda。
int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); portNumber = 31337;錯誤: Lambda表達式引用的局部變量必須是最終的(final)
或事實上最終的
為什么局部變量有這些限制?
實例變量和局部變量背后的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之后,去訪問該變量。因此,Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什么區別了——因此就有了這個限制。
這一限制不鼓勵你使用改變外部變量的典型命令式編程模式。
方法引用
方法引用的基本思想是,如果一個Lambda代表的只是“直接調用這個方法”,那最好還是用名稱來調用它,而不是去描述如何調用它。
事實上,方法引用就是讓你根據已有的方法實現來創建
Lambda表達式。
但是,顯式地指明方法的名稱,你的代碼的可讀性會更好。
PS. I don’t think so.
它是如何工作的呢?當你需要使用方法引用時,目標引用放在分隔符::前,方法的名稱放在后面。例如,Apple::getWeight就是引用了Apple類中定義的方法getWeight。
先前:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));之后(使用方法引用和java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight));管中窺豹
Lambda及其等效方法引用的例子
| (Apple a) -> a.getWeight() | Apple::getWeight |
| () -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
| (str, i) -> str.substring(i) | String::substring |
| (String s) -> System.out.println(s) | System.out::println |
你可以把方法引用看作針對僅僅涉及單一方法的Lambda的語法糖,因為你表達同樣的事情時要寫的代碼更少了。
如何構建方法引用
方法引用主要有三類。
指向靜態方法的方法引用(例如Integer的parseInt方法,寫作Integer::parseInt)
指向任意類型實例方法的方法引用(例如String的length方法,寫作String::length)。//(String s) -> s.toUppeCase()
指向現有對象的實例方法的方法引用(假設你有一個局部變量expensiveTransaction用于存放Transaction類型的對象,它支持實例方法getValue,那么你就可以寫expensiveTransaction::getValue)//()->expensiveTransaction.getValue()
例子
第二種
List<String> str = Arrays.asList("a","b","A","B"); str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));List<String> str = Arrays.asList("a","b","A","B"); str.sort(String::compareToIgnoreCase);第二種
Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);Function<String, Integer> stringToInteger = Integer::parseInt;第二種
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);BiPredicate<List<String>, String> contains = List::contains;構造函數引用
無參構造函數的
Supplier<Apple> c1 = Apple::new; Apple a1 = c1.get();等同
Supplier<Apple> c1 = () -> new Apple(); Apple a1 = c1.get();若構造函數是Apple(Integer weight)
Function<Integer, Apple> c2 = Apple::new; Apple a2 = c2.apply(110);等同
Function<Integer, Apple> c2 = (weight) -> new Apple(weight); Apple a2 = c2.apply(110);若構造函數是Apple(String color, Integer weight)
BiFunction<String, Integer, Apple> c3 = Apple::new; Apple c3 = c3.apply("green", 110);等同
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight); Apple c3 = c3.apply("green", 110);運用實例
List<Integer> weights = Arrays.asList(7, 3, 4, 10); List<Apple> apples = map(weights, Apple::new);public static List<Apple> map(List<Integer> list,Function<Integer, Apple> f){List<Apple> result = new ArrayList<>();for(Integer e: list){result.add(f.apply(e));}return result; }static Map<String, Function<Integer, Fruit>> map = new HashMap<>();static {map.put("apple", Apple::new);map.put("orange", Orange::new); // etc... }public static Fruit giveMeFruit(String fruit, Integer weight){return map.get(fruit.toLowerCase()).apply(weight); }
構造函數引用
你已經看到了如何將有零個、一個、兩個參數的構造函數轉變為構造函數引用。那要怎么樣才能對具有三個參數的構造函數,比如Color(int, int, int),使用構造函數引用呢?
構造函數引用的語法是ClassName::new,那么在這個例子里面就是Color::new。但是你需要與構造函數引用的簽名匹配的函數式接口。但是語言本身并沒有提供這樣的函數式接口,你可以自己創建一個:
public interface TriFunction<T, U, V, R>{R apply(T t, U u, V v); }現在你可以像下面這樣使用構造函數引用了:
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;Lambda和方法引用實戰
以排序為例
inventory.sort(comparing(Apple::getWeight));第1步:傳遞代碼
void sort(Comparator<? super E> c)public class AppleComparator implements Comparator<Apple> {public int compare(Apple a1, Apple a2){return a1.getWeight().compareTo(a2.getWeight());} }inventory.sort(new AppleComparator());第2步:使用匿名類
inventory.sort(new Comparator<Apple>() {public int compare(Apple a1, Apple a2){return a1.getWeight().compareTo(a2.getWeight());} });第3步:使用Lambda表達式
inventory.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight()));//進一步簡化 inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));//現在你可以把代碼再改得緊湊一點了:Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());import static java.util.Comparator.comparing; inventory.sort(comparing((a) -> a.getWeight()));第4步:使用方法引用
inventory.sort(comparing(Apple::getWeight));復合Lambda表達式的有用的方法
比較器復合
逆序
inventory.sort(comparing(Apple::getWeight).reversed());比較器鏈
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry)); //兩個蘋果一樣重時,進一步按國家排序謂詞復合
謂詞接口包括三個方法: negate、 and和or,讓你可以重用已有的Predicate來創建更復雜的謂詞。
//產生現有Predicate對象redApple的非 Predicate<Apple> notRedApple = redApple.negate();Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getColor()));函數復合
最后,你還可以把Function接口所代表的Lambda表達式復合起來。 Function接口為此配了andThen和compose兩個默認方法,它們都會返回Function的一個實例。
Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.andThen(g); int result = h.apply(1);//x=1,結果是4Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.compose(g); int result = h.apply(1);//x=1,結果是3做文本轉換 為例
public class Letter{public static String addHeader(String text){return "From Raoul, Mario and Alan: " + text;}public static String addFooter(String text){return text + " Kind regards";}public static String checkSpelling(String text){return text.replaceAll("labda", "lambda");} }函數復合
Function<String, String> addHeader = Letter::addHeader; Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling).andThen(Letter::addFooter);數學中類似的思想
積分
在這個例子里,函數f是一條直線,因此你很容易通過梯形方法(畫幾個三角形)來算出面積:
1/2 × ((3 + 10) + (7 + 10)) × (7 – 3) = 60運用Lambda表達式
integrate((double x) -> x + 10, 3, 7)public double integrate(DoubleFunction<Double> f, double a, double b) {return (f.apply(a) + f.apply(b)) * (b-a) / 2.0; }小結
- 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表達式的默認方法。
總結
以上是生活随笔為你收集整理的《Java8实战》笔记(03):Lambda表达式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python(10)- 格式化输出%
- 下一篇: 《Java8实战》笔记(16):结论以及