本文是博主在學習《java8實戰》一書的讀書筆記。java
java8 lambda表達式語法的兩種格式:express
語法解讀:編程
正解:
(1) 正確。若是使用匿名類(接口名統一使用IDemoLambda)表示以下:app
1 new IDemoLambda() { 2 public void test() { 3 } 4}
(2)正確。若是使用匿名類(接口名統一使用IDemoLambda)表示以下:運維
1 new IDemoLambda() { 2 public String test() { 3return "Raoul"; // 若是直接接一個值,表示返回該值 4 } 5}
(3)正確。若是使用匿名類(接口名統一使用IDemoLambda)表示以下:ide
1 new IDemoLambda() { 2 public String test() { 3return "Mario"; 4 } 5}
(4)錯誤。由於return是流程控制語句,表示返回,不是一個表達式,故不符合lambda語法,正確的表示方法應該是 (Integer i) ->{ return "Alan" + i;}。若是使用匿名類(接口名統一使用IDemoLambda)表示以下:函數式編程
1 new IDemoLambda() { 2 public String test(Integer i) { 3return "Alan" + i; 4 } 5}
(5)錯誤。由於"IronMan"是一個表達式,並非一個語句,故不能使用{}修飾,應修改成 (String s) -> "IronMan"。若是使用匿名類(接口名統一使用IDemoLambda)表示以下:函數
1 new IDemoLambda() { 2 public String test(String s) { 3return "IronMan"; 4 } 5}
在java8中,一個接口若是隻定義了一個抽象方法,那這個接口就能夠稱爲函數式接口,就可使用lambda表達式來簡化程序代碼。Lambda表達式能夠直接賦值給變量,也能夠直接做爲參數傳遞給函數,示例以下:性能
1public static void startThread(Runnable a) { 2 (new Thread(a)).start(); 3} 4 5public static void main(String[] args) { 6 // lambda表達式能夠直接賦值給變量,也能夠直接以參數的形式傳遞給方法、 7 Runnable a = () -> { 8 System.out.println("Hello World,Lambda..."); 9 }; 10 // JDK8以前使用匿名類來實現 11 Runnable b = new Runnable() { 12 @Override 13 public void run() { 14 System.out.println("Hello World,Lambda..."); 15 } 16 }; 17 startThread(a); 18 startThread(() -> { 19 System.out.println("Hello World,Lambda..."); 20 }); 21}
那能將(int a) -> {System.out.println("Hello World, Lambda…");}表達式賦值給Runnable a變量嗎?答案是不能,由於該表達式不符合函數式編程接口(Runnable)惟一抽象方法的函數簽名列表。
Runnable的函數式簽名列表爲public abstract void run();學習
舒適提示:若是咱們有留意JDK8的Runnable接口的定義,你會發現給接口相對JDK8以前的版本多了一個註解:@FunctionalInterface,該註解是一個標識註解,用來標識這個接口是一個函數式接口。若是咱們人爲在一個不知足函數式定義的接口上增長@FunctionalInterface,則會在編譯時提示錯誤。
例若有以下代碼:
1/** 2 * 處理文件:當前需求是處理文件的第一行數據 3 * @return 4 * @throws IOException 5 */ 6public static String processFile() throws IOException { 7 try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 8 return br.readLine(); 9 } 10}
當前需求爲處理文件的第一行數據,那問題來了,若是需求變化須要返回文件的第一行和第二行數據,那該如何進行改造呢?
在理想的狀況下,須要重用執行設置和關閉流的代碼,並告訴processFile()方法對文件執行不一樣的操做,換句話說就是要實現對processFile的行爲進行參數化。
Step·1:行爲參數化
要讀取文件的頭兩行,用Lambda語法如何實現呢?思考一下,下面這條語句是否能夠實現?
(BufferedReader bf) -> br.readLine() + br.readLine()
答案是固然能夠,接下來就要思考,定義一個什麼樣的方法,能接收上面這個參數。
Step2:使用函數式接口來傳遞行爲
要使用(bufferedReader bf) -> br.readLine() + br.readLine(),則須要定義一個接受參數爲BufferedReader,並返回String類型的函數式接口。
定義以下:
1@FunctionalInterface 2public interface BufferedReaderProcessor { 3 public String process(BufferedReader b) throws IoException; 4}
那把processFile方法改形成以下代碼:
1/** 2 * 處理文件:當前需求是處理文件的第一行數據 3 * @return 4 * @throws IOException 5 */ 6public static String processFile(BufferedReaderProcess brp) throws IOException { 7 try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 8 return brp.process(br); 9 } 10}
Step3:使用lambda表達式做爲參數進行傳遞
將行爲參數化後,並對方法進行改造,使方法接受一個函數式編程接口後,就能夠將Lambda表達式直接傳遞給方法,例如:
1processFile( (BufferedReader br) -> br.readLine() ); 2processFile( (BufferedReader bf) -> br.readLine() + br.readLine());
從上面的講解中咱們已然可以得知,要可以將Lambda表達式當成方法參數進行參數行爲化的一個前提條件是首先要在方法列表中使用一個函數式接口,例如上例中的BufferReaderProcess,那若是每次使用Labmbda表達式以前都要定義各自的函數式編程接口,那也夠麻煩的,那有沒有一種方式,或定義一種通用的函數式編程接口呢?答案是確定的,Java8的設計者,利用泛型,定義了一整套函數式編程接口,下面將介紹java8中經常使用的函數式編程接口。
所謂函數式編程接口就是隻能定義一個抽象方法,Predicate函數接口中定義的抽象方法爲boolean test(T t),對應的函數式行爲爲接收一類對象t,返回boolean類型,其可用的lambda表達式爲(T t) -> boolean類型的表達式,例如(Sample a) -> a.isEmpty()。
該接口一般的應用場景爲過濾。例如,要定義一個方法,從集合中進行刷選,具體的刷選邏輯(行爲)由參數進行指定,那咱們能夠定義這樣一個刷選的方法:
1public static <T> List<T> filter(List<T> list, Predicate<T> p) { 2List<T> results = new ArrayList<>(); 3for(T s: list){ 4if(p.test(s)){ 5results.add(s); 6} 7} 8return results; 9}
上述函數,咱們能夠這樣進行調用:
1Predicate<String> behaviorFilter = (String s) -> !s.isEmpty(); // lambda表達式賦值給一個變量 2filter(behaviorFilter);
其它add等方法,將在下文介紹(複合lambda表達式)。
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:IntPredicate、LongPredicate、DoublePredicate。咱們選擇LongPredicate看一下其函數接口的聲明:
1boolean test(long value);
該函數式編程接口適合對對象進行處理,但沒有返回值,對應的函數描述符:T -> void
舉例以下:
1public static <T> void forEach(List<T> list, Consumer<T> c) { 2 for(T t : list) { 3 c.accept(t); 4 } 5}
其調用示例以下:
1forEach( Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) );
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:IntConsumer、LongConsumer、DoubleConsumer。
其適合的場景是,接收一個泛型T的對象,返回一個泛型爲R的對象,其對應的函數描述符: T -> R。
示例以下:
1public static <T,R> List<R> map(List<T> list, Function<T,R> f) { 2 List<R> result = new ArrayList<>(); 3 for(T t : list) { 4 result.add( f.apply(t) ); 5 } 6 return result; 7} 8List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length );
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:IntFunction< R>、LongFunction< R>、DoubleFunction< R>、IntToDoubleFunction、IntToLongFunction、LongToIntFunction、LongToDoubleFunction、ToIntFunction< T>、ToDoubleFunction< T>、ToLongFunction< T>。
函數描述符:() -> T。適合建立對象的場景,例如 () -> new Object();
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier。
一元運算符函數式接口,接收一個泛型T的對象,一樣返回一個泛型T的對象。
示例以下:
1public static <T> List<T> map(List<T> list, UnaryOperator<T> f) { 2 List<R> result = new ArrayList<>(); 3 for(T t : list) { 4 result.add( f.apply(t) ); 5 } 6 return result; 7} 8 9map( list, (int i) -> i ++ );
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:IntUnaryOperator、LongUnaryOperator、DoubleUnaryOperator。
接收兩個參數,返回boolean類型。其對應的函數描述符:(T,U) -> boolean。
在這裏插入圖片描述
與Consume函數式接口相似,只是該接口接收兩個參數,對應的函數描述符(T,U) -> void。
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer。
與Function函數式接口相似,其對應的函數描述符:(T,U) -> R。
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:ToIntBiFunction(T,U)、ToLongBiFunction(T,U)、ToDoubleBiFunction(T,U)。
二維運算符,接收兩個T類型的對象,返回一個T類型的對象。
另外,爲了不java基本類型與包裝類型的裝箱與拆箱帶來的性能損耗,JDK8的設計者們提供了以下函數式編程接口:IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator。
上述就是JDK8定義在java.util.function中的函數式編程接口。重點關注的是其定義的函數式編程接口,其複合操做相關的API將在下文中詳細介紹。
java8是如何檢查傳入的Lambda表示式是否符合約定的類型呢?
例如
1public static <T> List<T> filter(List<T> list, Predicate<T> p) { 2 List<T> results = new ArrayList<>(); 3 for(T s: list){ 4 if(p.test(s)){ 5 results.add(s); 6 } 7 } 8 return results; 9} 10 11List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
其類型檢測的步驟:
首先查看filter函數的參數列表,得出Lambda對應的參數類型爲Predicate。
函數式接口Predicate中定義的抽象接口爲 boolean test(T t),對應的函數描述符( T -> boolean)。
注意:若是一個Lambda的主體式一個語句表達式,它就和一個返回void的函數描述符兼容(固然參數列表也必須兼容)。例如,如下兩行都是合法的,儘管List的add方法返回一個boolean,而不式Consumer上下文(T -> void)所要求的void:
1// Predicate返回了一個boolean 2Predicate<String> p = s -> list.add(s); 3// Consumer返回了一個void 4Consumer<String> b = s -> list.add(s);
思考題:以下表達式是否正確?
1Object o = () -> {System.out.println("Tricky example"); };
答案是錯誤的,該語句的含義就是把lambda表達式複製給目標對象(Object o),lambda對應的函數描述符爲() -> void,指望目標對象擁有一個惟一的抽象方法,參數列表爲空,返回值爲void的方法,顯然目標對象Object不知足該條件,若是換成以下示例,則能編譯經過:
1Runnable r = () {System.out.println("Tricky example"); };
由於Runnable的定義以下:
類型推斷
所謂的類型推斷,指的式java編譯器能根據目標類型來推斷出用什麼函數式接口來配合Lambda表達式,這也意味着它也能夠推斷出適合Lambda的簽名,由於函數描述符能夠經過目標類型獲得。
例如:
1List<Apple> greenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor())); 2也能夠寫成 3List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor())); 4 5Lambda表達式有多個參數,代碼可讀性的好處就更爲明顯。例如,你能夠這樣來建立一個Comparator 對象: 6Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 7Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
因爲java編譯器能根據目標類型來推導出Lambda的函數簽名,故lambda的函數簽名列表時,能夠去掉參數的類型。
Lambda表達式主體部分也能引入外部的變量,例如:
1int portNumber = 1337; 2Runnable r = () -> System.out.println(portNumber);
其中portNumber參數並非方法簽名參數,但這樣有一個限制條件,引入的局部變量必須是常量(實際意義上的常量,能夠不用final來定義,但不能改變其值。例如以下示例是錯誤的:
1int portNumber = 1337;
2Runnable r = () -> System.out.println(portNumber);
3portNumber = 1228; // 由於portNumber的值已改變,不符合局部變量的捕獲條件,上述代碼沒法編譯經過。
JDK8中有3中方法引用:
(1)指向靜態方法的方法引用
Integer.parseInt 對應的方法引用能夠寫成: Integer::parseInt。
(2)指向任意類型的實例方法的引用
(Strng str ) -> str.length 對應的方法引用:String::length。(注意這裏的屬性爲方法列表)
(3)lambda捕獲外部的實例對象
例如以下代碼:
1 Apple a = new Apple(); 2 process( () -> a.getColor() ); // 則能夠寫成 process ( a::getColor );
你們能夠回想一下,jdk8中定義了一個建立對象的函數式編程接口Supplier,函數描述符:() -> T。適合建立對象的場景,例如 () -> new Object();
對於沒有構造函數的,咱們能夠這樣來建立對象:
1Supplier<Apple> c1 = Apple:new; 2Apple a1 = c1.get();
若是有1個參數的構造方法呢?
1Function<Integer, Apple> c2 = Apple::new; 2Apple a2 = c2.apply(weight);
Lambda語法的基礎知識就介紹到這裏,本文詳細介紹了Lambda表達式的語法格式、函數式編程接口、lambda與函數式編程接口的關係、方法引用。
下一節主要介紹複合Lambda表達式使用。
更多文章請關注:
一波廣告來襲,做者新書《RocketMQ技術內幕》已出版上市:《RocketMQ技術內幕》已出版上市,目前可在主流購物平臺(京東、天貓等)購買,本書從源碼角度深度分析了RocketMQ NameServer、消息發送、消息存儲、消息消費、消息過濾、主從同步HA、事務消息;在實戰篇重點介紹了RocketMQ運維管理界面與當前支持的39個運維命令;並在附錄部分羅列了RocketMQ幾乎全部的配置參數。本書獲得了RocketMQ創始人、阿里巴巴Messaging開源技術負責人、Linux OpenMessaging 主席的高度承認並做序推薦。目前是國內第一本成體系剖析RocketMQ的書籍。