樂位元組-Java8核心實戰之三:函式式介面
Java8 引入 Lambda 表示式,允許開發者將函式當成引數傳遞給某個方法,或者把程式碼本身當作資料進行處理。使用 Lambda 表示式,使得應用變得簡潔而緊湊。 很多語言(Groovy、Scala 等)從設計之初就支援 Lambda 表示式。但是 java 中使用的是匿名內部類代替。最後藉助強大的社群力量,找了一個折中的 Lambda 實現方案,可以實現簡潔而緊湊的語言結構。
一、匿名內部類到Lambda的演化
匿名內部類,即一個沒有名字的,存在於一個類或方法內部的類。當我們需要用某個類且只需要用一次,建立和使用合二為一時,我們可以選擇匿名內部類,省掉我們定義類的步驟。
匿名內部類會隱士的繼承一個類或實現一個介面,或者說匿名內部類是一個繼承了該類或者實現了該介面的子類匿名物件。下面看一個匿名內部類的例子
package com.java8; /* 定義和使用匿名內部類 */ public class NoNameClass { public static void main(String[] args) { Model m = new Model(){ @Override public void func() { System.out.println("方法的實現"); } }; m.func(); } } // 需要被實現的介面 interface Model{ void func(); } 需要更多資料+我們程式設計師小姐姐v:lezijie007(加好友時備註: 66,不備註拒絕新增喲)
等價的 Lambda 程式碼
package com.java8;
/*
定義和使用Lambda 簡化程式碼
*/
public class NoNameClass {
public static void main(String[] args) {
Model m = new Model(){()->{
System.out.println("方法的實現");
}};
m.func();
}
}
可以看出使用 Lambda 表示式替代了匿名內部類程式碼,使得程式碼更加簡化、緊湊。
二、語法
(parameters) -> expression 或 (parameters) ->{ statements; }
-
可選型別宣告
不需要宣告引數型別,編譯器可以統一識別引數值。
-
可選的引數圓括號
一個引數無需定義圓括號,但多個引數需要定義圓括號。
-
可選的大括號
如果主體包含了一個語句,就不需要使用大括號。
-
可選的返回關鍵字
如果主體只有一個表示式返回值則編譯器會自動返回值,大括號需要指明表示式返回了一個數值。
Lambda 表示式示例:
三、Lambda使用形式
使用 Lambda 時,實現方法可以有引數,也可以有返回值,如果沒指定引數型別,則由編譯器自行推斷得出。
1、無參帶返回值
生成[1,10]之間的任意整數
interface Model2{
int func();
}
Model2 md2 = () -> {return (int)(Math.random()*10+1)};
Lambda 的改寫需要有對應的抽象方法,當沒有引數時需要使用()佔位,當表示式只有一行程式碼時,可以省略return和{}
以上的 Lambda 等價於:
Model2 md2 = () -> (int)(Math.random()*10+1);
2、帶參帶返回值
返回一個對數字描述的字串。
interface Model3{
String func(int a);
}
Model3 md3 = (int a) -> {
return "This is a number " + a;
};
形參寫在()內即可,引數的型別可以省略,此時將由編譯器自行推斷得出,同時還可以省略()
以上的 Lambda 等價於:
md3 = a -> "This is a number " + a;
省略了引數型別,小括號,同時連帶實現體的括號和 return 一併省去。
3、帶多個引數
根據輸入的運算子計算兩個數的運算,並返回結果
interface Model4{
String func(int a, int b, String oper);
}
Model4 md4 = (a, b, s) -> {
String res = "";
if("+".equals(s)){
res = ( a+b ) + "";
}else if("-".equals(s)){
res = ( a-b ) + "";
}else if("*".equals(s)){
res = ( a*b ) + "";
}else if("/".equals(s)){
res = ( a/b ) + ""; // 暫不考慮除0的情況
}else{
res = "操作有失誤";
}
return res;
};
System.out.println(md4.func(1,1,"+"));
以上例子為多個引數的 Lambda 表示式,其中省略掉了每一個引數的型別,編譯器自動推斷。多條語句時實現體的{}不能省。
四、Lambda作為引數
在 Java8 之前,介面可以作為方法引數傳入,執行時必須提供介面實現類的例項。從 java8 開始,Lambda 可以作為介面方法實現,當作引數傳入,無論從形式上還是實際上都省去了物件的建立。使程式碼更加的緊湊簡單高效。
定義介面
在介面中,必須有且僅有一個抽象方法,以確定 Lambda 模板
// 無參無返回值的方法
interface LambdaInterface1{
void printString();
}
// 帶參無返回值的方法
interface LambdaInterface2{
void printString(String str);
}
定義方法接收引數
在某方法中需要使用介面作為引數
// 無參
public static void testLambda(LambdaInterface1 lam1){
lam1.printString();
}
// 帶參
public static void testLambda2(String s,LambdaInterface2 lam2){
lam2.printString(s);
}
Lambda表示式作為引數傳入
// 無參Lambda作為引數
testLambda(()->{
System.out.println("可以簡單,可以複雜");
});
// 帶參Lambda作為引數
testLambdaParam("hello",(a)->{
System.out.println(a);
});
五、Lambda中使用變數
在 Lambda 中可以定義自己的區域性變數,也可以使用外層方法的區域性變數,還可以使用屬性。這一點也不難理解,既然是一個方法的實現,只寫了一個程式碼塊,那麼使用本身所屬方法的區域性變數和類的屬性也並不過分。
public static void main(String[] args) {
List<String> strs = new ArrayList<String>(){
{
add("aaa");
add("bbb");
add("ccc");
}
};
int j = 1;
strs.forEach((str)->{
int i = 0;
System.out.println(str + " " + i + " " + j);
});
}
六、Lambda型別推斷
型別檢查
Lambda 的型別是從使用 Lambda 的上下文推斷出來的。 Lambda 表示式的引數與函式式介面內方法的引數,返回值型別相互對應。Lambda 表示式需要的型別,或者說 Lambda 實現的那個函式式介面稱之為目標型別。
型別推斷
利用目標型別來檢查一個 Lambda 是否可以用於某個特定的上下文,推斷 Lambda 引數的型別。
七、Lambda表示式實戰
1、熱銷商品排序
排序對於久經開發的你來說可能並不陌生,假如原來你做過電商專案,相信對於電商場景下的商品記錄排序操作很有感情,下面我們使用Lambda 來看看熱銷商品排序的操作。
測試資料這裡以手機測試資料為例
/**
* 實際開發資料通常從資料庫獲取
* 這裡使用測試資料
*/
Goods g01=new Goods(1,"小米9",1789,200, BigDecimal.valueOf(2500));
Goods g02=new Goods(2,"華為Mate20",5000,3000, BigDecimal.valueOf(7000));
Goods g03=new Goods(3,"OPPO R17",2000,2827, BigDecimal.valueOf(1500));
Goods g04=new Goods(4,"魅族 Note9",2000,1600, BigDecimal.valueOf(1600));
Goods g05=new Goods(5,"一加6T",8000,5000, BigDecimal.valueOf(3500));
List<Goods> goods= Arrays.asList(g01,g02,g03,g04,g05);
Collections.sort 靜態方法實現排序
Collections.sort(goods,(g1,g2)->g1.getSale()-g2.getSale());
List.sort 預設方法實現集合排序
// 使用 Lambda 對商品記錄按銷量進行排序
goods.sort((g1,g2)->g1.getSale()-g2.getSale());
Stream.sorted 方法實現元素排序
// 多個條件排序情況 Lambda 配置Stream 銷量+價格排序 銷量相等時按照價格排序
goods =goods.stream().sorted((g1,g2)->g1.getSale()-g2.getSale())
.sorted((g1,g2)->g1.getPrice().compareTo(g2.getPrice()))
.collect(Collectors.toList());
2、日誌輸出優化
對於專案開發日誌列印是一項不可獲取的模組,無論是在開發階段還是專案部署上線後,日誌資訊的輸出對於開發人員來以及運維人員來說都是一項重要的參考指標。
日誌輸出場景這裡以使用者模組 UserService 為例,以下為優化前的日誌輸出程式碼:
public String login(String userName, String userPwd) {
logger.info("UserService 接收到引數-->" + userName + "," + userPwd);
/**
* 登入邏輯省略
*/
return "login";
}
日誌級別設定到 debug,在開發階段方便檢視後端接收到的引數資訊。仔細分析這裡的日誌程式碼,可以看到當日志級別設定為 info 時 debug 日誌不應該執行輸出操作,同時這裡呼叫 debug 方法時,對於傳入的字串引數需要作對應的拼接操作,才會傳入過來。當訪問的情況在商城專案做活動情況下 這裡的情況有可能會變得很糟糕:所有的 debug 資訊全部輸出 同時會有大量字串拼接操作,會影響整個應用程式的執行效能。
日誌輸出場景這裡以使用者模組 UserService 為例,日誌輸出程式碼優化
- 輸出日誌前判斷日誌輸出級別
- 藉助 Lambda 延遲日誌內容輸出
/**
* 新增info方法
* 判斷日誌列印級別
* 當條件成立時 輸出日誌資訊
* @param logger
* @param message
*/
public void info(Log logger, Supplier<String> message){
if(logger.isInfoEnabled()){
logger.info(message.get());
}
}
public String login(String userName, String userPwd) {
//logger.info("UserService 接收到引數-->" + userName + "," + userPwd);
// 延遲Lambda 表示式執行 只有確定
info(logger,()->"UserService 接收到引數-->" + userName + "," + userPwd);
return "login";
}
八、Lambda優勢與使用場景
Lambda 表示式的引入取代了匿名內部類,使得程式碼變得簡潔、緊湊,同時 Lambda 的惰性特點,在開發時能夠提高應用程式的執行效能。
對於 Lambda 的應用場景,從程式碼結構來說通常是結合函式式介面來使用,使得開發是面向函式來進行程式設計,也是 Java8 引入的一種新的思想-函數語言程式設計(後續會介紹)。同時還會結合前面講到的介面預設方法提現到應用開發中。
接收到引數-->" + userName + "," + userPwd);
// 延遲Lambda 表示式執行 只有確定
info(logger,()->"UserService 接收到引數-->" + userName + "," + userPwd);
return "login";
}
## 八、Lambda優勢與使用場景
Lambda 表示式的引入取代了匿名內部類,使得程式碼變得簡潔、緊湊,同時 Lambda 的惰性特點,在開發時能夠提高應用程式的執行效能。
對於 Lambda 的應用場景,從程式碼結構來說通常是結合函式式介面來使用,使得開發是面向函式來進行程式設計,也是 Java8 引入的一種新的思想-函數語言程式設計(後續會介紹)。同時還會結合前面講到的介面預設方法提現到應用開發中。