您好,登錄后才能下訂單哦!
這篇文章給大家介紹Java8中Lambda表達式如何使用,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
Lambda表達式是一個可傳遞的代碼塊,可以在以后執行一次或多次;
// Java8之前:舊的寫法 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("old run"); } }; Thread t = new Thread(runnable); // Java8之后:新的寫法 Runnable runnable1 = ()->{ System.out.println("lambda run"); }; Thread t1 = new Thread(runnable1);
下面分別說下語法中的三個組成部分
參數: ( Dog dog )
參數類型可省略(當編譯器可以自動推導時),比如Comparator<String> comparatorTest = (a, b)->a.length()-b.length();
,可以推導出a,b都為String
當參數類型可省略,且只有一個參數時,括弧也可以省略(但是個人習慣保留)
符號: ->
主體:{ System.out.println("javalover"); }
如果是一條語句,則需要加大括號和分號{;}
(比如上圖所示)
如果是一個表達式,則直接寫,啥也不加(比如a.length()- b.length()
)
為了簡化代碼
因為Java是面向對象語言,所以在lambda出現之前,我們需要先構造一個對象,然后在對象的方法中實現具體的內容,再把構造的對象傳遞給某個對象或方法
但是有了lambda以后,我們可以直接將代碼塊傳遞給對象或方法
現在再回頭看下開頭的例子
可以看到,用了lambda表達式后,少了很多模板代碼,只剩下一個代碼塊(最核心的部分)
就是只定義了一個抽象方法的接口
正例:有多個默認方法,但是如果只有一個抽象方法,那它就是函數式接口,示例代碼如下
@FunctionalInterface public interface FunctionInterfaceDemo { void abstractFun(); default void fun1(){ System.out.println("fun1"); } default void fun2(){ System.out.println("fun2"); } }
這里的注解@FunctionalInterface可以省略,但是建議加上,就是為了告訴編譯器,這是一個函數式接口,此時如果該接口有多個抽象方法,那么編譯器就會報錯
反例:比如A extends B,A和B各有一個抽象方法,那么A就不是函數式接口,示例代碼如下
// 編譯器會報錯,Multiple non-overriding abstract methods found in XXX @FunctionalInterface public interface NoFunctionInterfaceDemo extends FunctionInterfaceDemo{ void abstractFun2(); }
上面的父接口FunctionInterfaceDemo中已經有了一個抽象方法,此時NoFunctionInterfaceDemo又定義了一個抽象方法,結果編譯器就提示了:存在多個抽象方法
在Java8之前,其實我們已經接觸過函數式接口
比如Runnable 和 Comparable
只是沒有注解@FunctionalInterface。
那這個函數式接口要怎么用呢?
配合lambda食用,效果最佳(就是把lambda傳遞給函數式接口),示例代碼如下:
new Thread(() -> System.out.println("run")).start();
其中用到的函數式接口是Runnable
就是把行為定義成參數,行為就是函數式接口
類似泛型中的類型參數化<T>
,類型參數化是把類型定義成參數
行為參數化,通俗點來說:
就是用函數式接口做形參
然后傳入接口的各種實現內容(即lambda表達式)作為實參
最后在lambda內實現各種行為(好像又回到多態的那一節了?這也是為啥多態是Java的三大特性的原因之一,應用太廣泛了)
這樣來看的話,行為參數化和設計模式中的策略模式有點像了(后面章節會分別講常用的幾種設計模式)
下面我們手寫一個函數式接口來加深理解吧
下面我們循序漸進,先從簡單的需求開始
第一步:比如我們想要讀取某個文件,那可以有如下方法:
public static String processFile() throws IOException { // Java7新增的語法,try(){},可自動關閉資源,減少了代碼的臃腫 try( BufferedReader bufferedReader = new BufferedReader(new FileReader("D:\\JavaProject\\JavaBasicDemo\\test.txt"))){ return bufferedReader.readLine(); } }
可以看到,核心的行為動作就是 return bufferedReader.readLine();
,表示讀取第一行的數據并返回
那如果我們想要讀取兩行呢?三行?
第二步:這時就需要用到上面的函數式接口了,下面就是我們自己編寫的函數式接口
@FunctionalInterface interface FileReadInterface{ // 這里接受一個BufferedReader對象,返回一個String對象 String process(BufferedReader reader) throws IOException; }
可以看到,只有一個抽象方法process()
,它就是用來處理第一步中的核心動作(讀取文件內容)
至于想讀取多少內容,那就需要我們在lambda表達式中定義了
第三步:接下來我們定義多個lambda表達式,用來傳遞給函數式接口,其中每個lambda表達式就代表了一種不同的行為,代碼如下:
// 讀取一行 FileReadInterface fileReadInterface = reader -> reader.readLine(); // 讀取兩行 FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();
第四步:我們需要修改第一步的processFile()
,讓其接受一個函數式接口,并調用其中的抽象方法,代碼如下:
// 參數為第二步我們自己手寫的函數式接口 public static String processFile(FileReadInterface fileReadInterface) throws IOException { try( BufferedReader bufferedReader = new BufferedReader(new FileReader("./test.txt"))){ // 這里我們不再自己定義行為,而是交給函數式接口的抽象方法來處理,然后通過lambda表達式的傳入來實現多個行為 return fileReadInterface.process(bufferedReader); } }
第五步:拼接后,完整代碼如下:
public class FileReaderDemo { public static void main(String[] args) throws IOException { // 第三步: // lambda表達式1 傳給 函數式接口:只讀取一行 FileReadInterface fileReadInterface = reader -> reader.readLine(); // lambda表達式2 傳給 函數式接口:只讀取兩行 FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine(); // 最后一步: 不同的函數式接口的實現,表現出不同的行為 String str1 = processFile(fileReadInterface); String str2 = processFile(fileReadInterface2); System.out.println(str1); System.out.println(str2); } // 第四步: 讀取文件方法,接受函數式接口作為參數 public static String processFile(FileReadInterface fileReadInterface) throws IOException { try( BufferedReader bufferedReader = new BufferedReader(new FileReader("./test.txt"))){ // 調用函數式接口中的抽象方法來處理數據 return fileReadInterface.process(bufferedReader); } } // 第一步: public static String processFile() throws IOException { try( BufferedReader bufferedReader = new BufferedReader(new FileReader("./test.txt"))){ return bufferReader.readLine(); } } } // 第二步: 我們手寫的函數式接口 @FunctionalInterface interface FileReadInterface{ String process(BufferedReader reader) throws IOException; }
其實你會發現,我們手寫的這個函數式接口,其實就是Function<T>
去除泛型化后的接口,如下所示:
@FunctionalInterface public interface Function<T, R> { // 都是接受一個參數,返回另一個參數 R apply(T t); }
下面我們列出Java中常用的一些函數式接口,你會發現自帶的已經夠用了,基本不會需要我們自己去寫
這里的手寫只是為了自己實現一遍,可以加深理解程度
我們先看一個例子
前面我們寫的lambda表達式,其實還可以簡化,比如
// 簡化前 Function<Cat, Integer> function = c->c.getAge(); // 簡化后 Function<Cat, Integer> function2 = Cat::getAge;
其中簡化后的Cat::getAge
,我們就叫做方法引用
方法引用就是引用類或對象的方法;
下面我們列出方法引用的三種情況:
Object::instanceMethod(對象的實例方法)
Class::staticMethod(類的靜態方法)
Class::instanceMethod(類的實例方法)
像我們上面舉的例子就是第三種:類的實例方法
下面我們用代碼演示上面的三種方法:
public class ReferenceDemo { public static void main(String[] args) { // 第一種:引用對象的實例方法 Cat cat = new Cat(1); Function<Cat, Integer> methodRef1 = cat::getSum; // 第二種:引用類的靜態方法 Supplier<Integer> methodRef2 = Cat::getAverageAge; // 第三種:引用類的實例方法 Function<Cat, Integer> methodRef3 = Cat::getAge; } } class Cat { int age; public Cat(int age) { this.age = age; } // 獲取貓的平均年齡 public static int getAverageAge(){ return 15; } // 獲取兩只貓的年齡總和 public int getSum(Cat cat){ return cat.getAge() + this.getAge(); } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
為啥要用這個方法引用呢?
方法引用好比lambda表達式的語法糖,語法更加簡潔,清晰
一看就知道是調用哪個類或對象的哪個方法
上面介紹了方法引用,就是直接引用某個方法
這里的構造引用同理可得,就是引用某個類的構造方法
構造引用的表達式為:Class::new
,僅此一種
如果你有多個構造函數,那編譯器會自己進行推斷參數(你看看,多好,多簡潔)
比如下面的代碼:
// 這里調用 new Cat() Supplier<Cat> constructRef1 = Cat::new; // 這里調用 new Cat(Integer) Function<Integer, Cat> constructRef2 = Cat::new;
要求引入lambda表達式中的變量,必須是最終變量,即該變量不會再被修改
比如下面的代碼:
public static void main(String[] args) { String str = "javalover.cc"; Runnable runnable = ()->{ str = "1";// 這里會報錯,因為修改了str引用的指向 System.out.println(str); } }
可以看到,lambda表達式引用了外面的str引用,但是又在表達式內部做了修改,結果就報錯了
為啥要有這個限制呢?
為了線程安全,因為lambda表達式有一個好處就是只在需要的時候才會執行,而不是調用后立馬執行
這樣就會存在多個線程同時執行的并發問題
所以Java就從根源上解決:不讓變量被修改,都是只讀的
那你可能好奇,我不把str的修改代碼放到表達式內部可以嗎?
也不行,道理是一樣的,只要lambda有用到這個變量,那這個變量不管是在哪里被修改,都是不允許的
不然的話,我這邊先執行了一次lambda表達式,結果你就改了變量值,那我第二次執行lambda,不就亂了嗎
最后是lambda的必殺技:組合操作
在這里叫組合或者復合都可以
概述:組合操作就是先用一個lambda表達式,然后再在后面組合另一個lambda表達式,然后再在后面組合另另一個lambda表達式,然后。。。有點像是鏈式操作
學過JS的都知道Promise,里面的鏈式操作就和這里的組合操作很像
用過Lombok的朋友,應該很熟悉@Builder注解,其實就是構造者模式
下面我們用代碼演示下組合操作:
// 重點代碼 public class ComposeDemo { public static void main(String[] args) { List<Dog> list = Arrays.asList(new Dog(1,2), new Dog(1, 1)); // 1. 先按年齡排序(默認遞增) // Dog::getAge, 上面介紹的方法引用 // comparingInt, 是Comparator的一個靜態方法,返回Comparator<T> Comparator<Dog> comparableAge = Comparator.comparingInt(Dog::getAge); // 2. 如果有相同的年齡,則年齡相同的再按體重排序(如果年齡已經比較出大小,則下面的體重就不會再去比較) Comparator<Dog> comparableWeight = Comparator.comparingInt(Dog::getWeight);; // 3. 調用list對象的sort方法排序,參數是Comparator<? super Dog> list.sort(comparableAge.thenComparing(comparableWeight)); System.out.println(list); } } // 非重點代碼 class Dog{ private int age; private int weight; public Dog(int age, int weight) { this.age = age; this.weight = weight; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } @Override public String toString() { return "Dog{" + "age=" + age + ", weight=" + weight + '}'; } }
輸出:[Dog{age=1, weight=1}, Dog{age=1, weight=2}]
比較的流程如下所示:
lambda的語法: 參數+符合+表達式或語句,比如(a,b)->{System.out.println("javalover.cc");}
函數式接口:只有一個抽象方法,最好加@FunctionalInterface,這樣編譯器可及時發現錯誤,javadoc也說明這是一個函數式接口(可讀性)
行為參數化:就是函數式接口作為參數,然后再將lambda表達式傳給函數式接口,通過不同的lambda內容實現不同的行為
方法引用:lambda的語法糖,總共有三種:
Object::instanceMethod(對象的實例方法)
Class::staticMethod(類的靜態方法)
Class::instanceMethod(類的實例方法)
構造引用:就一種,編譯器自己可判斷是哪個構造函數,語法為Class::new
在lambda中引入外部變量,必須保證這個變量是最終變量,即不再被修改
lambda的組合操作,就是鏈式操作,組合是通過函數式接口的靜態方法來組合(靜態方法會返回另一個函數式接口的對象)
比如list.sort(comparableAge.thenComparing(comparableWeight));
關于Java8中Lambda表達式如何使用就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。