文章目錄
什麼是Lambda
概述
- Lambda表達式也被稱爲箭頭函數、匿名函數、閉包
- Lambda表達式體現的是輕量級函數式編程思想
- ‘->’符號式是Lambda表達式核心操作符號,符號左側是操作參數,符號右側是操作表達式
- 是JDK8提供的新特性,運行環境必須在JDK8以上的環境中
MCAD模式(Model Code as Data)
爲什麼在JDK8中提供這麼一個新的語法糖呢?因爲在項目開發中,我們經常會有代碼質量的控制,這樣的要求,讓編寫的代碼更加趨於數據的有效處理,也就是
- Model Code as Data,編碼及數據,儘可能輕量級的將代碼封裝爲數據。
傳統的解決方案
編碼及數據的傳統的解決方案:接口&實現類(匿名內部類),存在如下問題:
- 語法冗餘
- this 關鍵字作用域存在很大的迷惑
- 變量捕獲,即變量作用域會有一些特殊的要求
- 數據流的控制並不是特別友好
我們看一個示例:線程的創建
new Thread(new Runnable() {
@Override
public void run(){
System.out.println("threading..." + Thread.currentThread().getId());
}
}).start();
上面代碼可以看到,一個新線程的創建本身只需要創建這個線程即可,在數據處理的過程主要集中在中間的部分,在我們語法語義上已經出現了代碼的冗餘。這種問題應怎麼處理呢?
JDK8新特性,lambda表達式優化線程模式
new Thread(()->{
System.out.println("lambda threading..." + Thread.currentThread().getId());
});
對比傳統的實現方法,它裏面主要包含了和數據相關的代碼,其它語法語義的代碼和我們內部類相比起來少了很多。
爲什麼要使用Lambda表達式
- 它不是解決未知問題的新技術
- 對現有解決方案的語義化優化
- 根據實際需求考慮性能問題
Lambda表達式基礎知識
函數式接口(function interface)
是JDK8在傳統語法語義上抽象出來的一個新的語義化術語,就是Java類型系統中的普通接口,只不過這樣的接口只包含一個接口方法。
在處理的過程中爲了表示和正確的描述函數式接口,JDK8提供了一個獨立的註解@FunctionalInterface,用來在語義上檢查函數式接口的合法性
示例1:用戶身份認證標記接口
/**
* 用戶身份認證標記接口
*/
@FunctionalInterface
public interface IUserCredential {
/**
* 通過用戶賬號,驗證用戶身份信息的接口
* @param username 要驗證的用戶賬號
* @return 返回身份信息[系統管理員、用戶管理員、普通用戶]
*/
String verifyUser(String username);
}
看上面的代碼是符合函數式接口要求的,當我們再增加一個方法
/**
* 用戶身份認證標記接口
*/
@FunctionalInterface
public interface IUserCredential {
/**
* 通過用戶賬號,驗證用戶身份信息的接口
* @param username 要驗證的用戶賬號
* @return 返回身份信息[系統管理員、用戶管理員、普通用戶]
*/
String verifyUser(String username);
boolean test();
}
可以看到項目出現瞭如下的提示
意思是項目中出現了多個未實現的方法,由此發現函數式接口只能有一個方法
示例2:消息傳輸格式轉換接口
/**
* 消息傳輸格式轉換接口
*/
@FunctionalInterface
public interface IMessageFormat {
/**
* 消息轉換方法
* @param message 要轉換的消息
* @param format 轉換的格式[xml、json]
* @return 返回轉換後的數據
*/
String format(String message,String format);
}
上面代碼也是一個自定義的函數式接口
默認方法和靜態方法
以上一節的用戶身份認證接口爲例,說明下
public class UserCredentialImpl implements IUserCredential {
@Override
public String verifyUser(String username){
if("admin".equals(username)) {
return "系統管理員";
}else if("manager".equals(username)) {
return "用戶管理員";
}
return "普通會員";
}
}
再寫一個調用類
public class App {
public static void main(String [] args){
IUserCredential ic= new UserCredentialImpl();
System.out.println(ic.verifyUser("admin"));
}
}
運行後輸出結果
系統管理員
Process finished with exit code 0
我們做下需求改動: 所有的用戶驗證,可以同時獲取用戶的驗證信息[是否認證成功|成功~返回用戶|null]
如果我們在傳統的方式下,只能在UserCredentialImpl實現類中做下改變,但是在JDK1.8新特性中,我們可以直接對接口進行改造
默認方法
/**
* 用戶身份認證標記接口
*/
@FunctionalInterface
public interface IUserCredential {
/**
* 通過用戶賬號,驗證用戶身份信息的接口
* @param username 要驗證的用戶賬號
* @return 返回身份信息[系統管理員、用戶管理員、普通用戶]
*/
String verifyUser(String username);
default String getCredential(String username) {
//模擬方法
if("admin".equals(username)) {
return "admin + 系統管理員用戶";
}else if("manager".equals(username)) {
return "manager + 用戶管理員用戶";
}
return "commons + 普通會員用戶";
}
}
我們可以在調用地方,直接獲取到它的信息
public class App {
public static void main(String [] args){
IUserCredential ic= new UserCredentialImpl();
System.out.println(ic.verifyUser("admin"));
System.out.println(ic.getCredential("admin"));
}
}
運行下項目:
系統管理員
admin + 系統管理員用戶
Process finished with exit code 0
可以看出,添加了默認方法之後,如果需要擴展它的實現類的一些公共的方法,我們可以直接在接口中通過定義默認方法的方式來給所有實現子類增加對應的處理方法。並且這種默認方法的增加對於函數式接口語義語法並沒有產生任何的影響,函數式語義語法只要求在當前接口中有一個未實現的抽象方法即可,默認方法不再其中。
靜態方法
默認方法不在函數式語義語法的要求中,那靜態方法呢?靜態方法同樣是用於功能的擴展,但是它和默認方法不同,默認方法應用場景是給所有的子類的對象增加的一些通用方法,靜態方法和它的應用場景稍微有些差異,比如我們以上一節的消息傳輸格式轉換接口爲例,給它增加一個靜態處理方法:
/**
* 消息傳輸格式轉換接口
*/
@FunctionalInterface
public interface IMessageFormat {
/**
* 消息轉換方法
* @param message 要轉換的消息
* @param format 轉換的格式[xml、json]
* @return 返回轉換後的數據
*/
String format(String message,String format);
/**
* 消息合法性驗證方法
* @param msg 要驗證的消息
* @return 返回驗證結果
*/
static boolean verifyMessage(String msg) {
if(msg !=null){
return true;
}
return false;
}
}
定義好了靜態方法後,我們就可以對它進行一些簡單的測試
public class MessageFormatImpl implements IMessageFormat {
@Override
public String format(String message,String format){
Sysetm.out.println("消息轉換。。。");
return message;
}
}
public class App {
public static void main(String [] args){
String msg = "hello world";
if(IMessageFormat.verifyMessage(msg)){
IMessageFormat format= new MessageFormatImpl ();
format.format("hello","json");
}
}
}
這就是靜態方法的直接使用方式,可以看到對我們函數式的語義語法同樣沒有產生影響,這是JDK1.8新特性中對於接口的方法進行擴展之後,和函數式接口之間的一個擴展關係,默認方法、靜態方法、函數式接口,可以共同在一個接口中。
來自Object繼承的方法
在Java中所有的類型、所有的對象都直接間接繼承自Object,那從Object繼承過來的方法即使它是抽象的,也不會影響我們函數式接口的語法語義,什麼意思呢?
/**
* 用戶身份認證標記接口
*/
@FunctionalInterface
public interface IUserCredential {
/**
* 通過用戶賬號,驗證用戶身份信息的接口
* @param username 要驗證的用戶賬號
* @return 返回身份信息[系統管理員、用戶管理員、普通用戶]
*/
String verifyUser(String username);
boolean test();
}
在函數式接口中增加了一個抽象方法,可以看到項目出現瞭如下的提示
如果我們增加從Object繼承過來的抽象方法,函數式接口的語法語義是驗證可以通過的
/**
* 用戶身份認證標記接口
*/
@FunctionalInterface
public interface IUserCredential {
/**
* 通過用戶賬號,驗證用戶身份信息的接口
* @param username 要驗證的用戶賬號
* @return 返回身份信息[系統管理員、用戶管理員、普通用戶]
*/
String verifyUser(String username);
String toString();
}
Lambda表達式和函數式接口的關係
- 函數式接口只包含一個操作方法
- Lambda表達式,只能操作一個方法
- Java中的Lambda表達式,核心就是一個函數式接口的實現
我們以前面小節的驗證用戶身份信息的示例來說明
public class App {
public static void main(String [] args){
//匿名內部類,實現接口抽象方法
IUserCredential ic2 = new IUserCredential() {
@Override
public String verifyUser(String username){
return "admin".equals(username)?"管理員":"會員";
}
};
System.out.println(ic2.verifyUser("manager"));
System.out.println(ic2.verifyUser("admin"));
}
}
運行結果:
會員
管理員
Process finished with exit code 0
可以看到對兩個用戶的身份進行了正常的輸出,但是我們通過觀察匿名內部類以及前面的實現類的操作,會發現和數據相關的代碼只有return "admin".equals(username)?"管理員":"會員";
這點,其它的都是冗餘代碼,那對於這樣的處理能不能進行優化呢?JDK8中提供的Lambda就是專門針對這樣的冗餘代碼進行優化的,我們通過Lambda表達式來進行一個實現:
public class App {
public static void main(String [] args){
//Lambda表達式,針對函數式接口的簡單實現
//Lambda表達式中的()定義的就是函數式接口中的操作方法的方法名後()
//Lambda表達式中的->{}就是針對操作方法的實現
IUserCredential ic3 = (String username) ->{
return "admin".equals(username)?"lbd管理員":"lbd會員";
};
System.out.println(ic3.verifyUser("manager"));
System.out.println(ic3.verifyUser("admin"));
}
}
相比於前面的匿名內部類寫法,Lambda表達式的寫法語義上顯的更加簡潔,語法上對於冗餘代碼處理已經變得非常的優秀了
運行結果:
lbd會員
lbd管理員
Process finished with exit code 0
通過運行結果可以發現,它的功能和我們通過實現類或匿名內部類的實現方式都是一致的。Lambda表達式是Java1.8中提供的新特性,是針對函數式接口的一種簡單實現方式,在語法上進行了優化
jdk中常見的函數式接口
- java.lang.Runnable
- java.lang.Comparable
- java.lang.Comparator
- java.io.FileFilter
- more…
在處理的過程中,如果我們發現某一個接口符合函數式接口的定義,jdk8以後的代碼中我們都可以通過lambda表達式進行簡潔的實現。
針對lambda表達式,jdk8中源碼的實現,封裝了java.util.functional包,裏面提供了常用的函數式功能接口,我們對其中最具代表性的接口進行講解:
首先,我們分析單個方法的處理場景。方法的處理無非就是方法運算的參數、方法執行完成之後的返回值的情況,jdk8針對不同情況提供了不同函數式接口的支持
- java.util.function.Predicate
- 接收參數對象T,返回一個boolean類型的結果
public class App {
public static void main(String [] args){
Predicate<String> pre=(String username)->{
return "admin".equals(username) ;
};
System.out.println(pre.test("manager"));
System.out.println(pre.test("admin"));
}
}
- java.util.function.Consumer
- 接收參數對象T,不返回結果
public class App {
public static void main(String [] args){
Consumer<String> con=(String message)->{
System.out.println("要發送的消息:" + message);
System.out.println("消息發送完成");
};
con.accept("hello 會員們");
con.accept("lambda expression.");
}
}
如果說在我們實際項目中,或開發過程中,需要對某個類型進行功能性的處理,並且不需要任何返回值的話,可以考慮使用JDK8提供的Consumer以及它的Lambda表達式的簡要實現
- java.util.function.Function<T,R>
- 接收參數對象T,返回結果對象R
public class App {
public static void main(String [] args){
Function<String,Integer> fun=(String gender)->{
return "male".equals(gender)?1:0;
};
System.out.println(fun.apply("male"));
System.out.println(fun.apply("female"));
}
}
- java.util.function.Supplier
- 不接受參數,提供T對象的創建工廠
public class App {
public static void main(String [] args){
Supplier<String> sup=( ) -> {
return UUID.randomUUID().toString();
};
System.out.println(sup.get());
System.out.println(sup.get());
}
}
- java.util.function.UnaryOperator
- 接收參數對象T,返回結果對象T
public class App {
public static void main(String [] args){
UnaryOperator<String> uo=(String img) -> {
img += "100×200";
return img;
};
System.out.println(uo.apply("原圖--"));
}
}
- java.util.function.BinaryOperator
- 接收兩個T對象,返回一個T對象結果
public class App {
public static void main(String [] args){
//項目場景:在系統中一個用戶可以和指定任何一個用戶進行pk,
//通過綜合參數來決定它們的勝負,最後返回一個勝出的用戶
//()中不會進行自動裝箱的處理,我們需要自己使用包裝類型
BinaryOperator<Integer> bo=(Integer i1,Integer i2) -> {
return i1>i2?i1:i2;
};
System.out.println(bo.apply(12,13));
}
}
Lambda表達式基本語法
主要區分爲四個部分:
- 聲明:就是和lambda表達式綁定的接口類型
- 參數:包含在一對圓括號中,和綁定的接口中的抽象方法中的參數個數及順序一致
- 操作符:->
- 執行代碼塊:包含在一對大括號中,出現在操作符的右側
[接口聲明] = (參數) -> {執行代碼塊};
沒有參數、沒有返回值的lambda表達式綁定的接口
interface ILambda1{
void test();
}
我們接下來就可以通過Lambda1來聲明lambda表達式
ILambda1 i1= () -> {
System.out.println("hello world!");
System.out.println("welcome to world!");
};
i1.test();
如果在{}中執行代碼塊裏只有一行代碼,就可以省略掉{}
ILambda1 i2= () -> System.out.println("hello world!");
i2.test();
帶有參數,沒有返回值的lambda表達式
interface ILambda2{
void test(String name,int age);
}
我們接下來就可以通過Lambda2來聲明lambda表達式
ILambda2 i21= (String n,int a) -> {
System.out.println(n+"say: my year's old is "+a);
};
i21.test("jerry",18);
如果lambda表達式帶有多個參數,我們可以在定義參數時不寫參數的類型,JVM會在運行時根據lambda表達式綁定的接口方法,自動推導該參數是什麼類型的,上面的可以這樣的方法寫
ILambda2 i22= (n,a) -> {
System.out.println(n+"說: 我今年" + a+"歲了");
};
i22.test("tom",22);
帶有參數,帶有返回值的lambda表達式
interface ILambda3 {
int test(int x,int y);
}
我們接下來就可以通過Lambda3來聲明lambda表達式
ILambda3 i3= (x, y ) -> {
int z=x+y;
return z;
};
System.out.println(i3.test(11,22));
在寫Lambda表達式時,如果只有一行代碼並且不使用{}情況下,返回值可以不添加return關鍵字,比如
ILambda3 i31= (x, y ) -> x+y;
System.out.println(i31.test(100,200));
總結
- lambda表達式必須和接口進行綁定
- lambda表達式的參數,可以附帶0到n個參數,括號中的參數類型可以不指定,jvm在運行時,會自動根據綁定的抽象方法中的參數進行推導
- lambda表達式的返回值,如果代碼塊只有一行,並且沒有大括號,不用謝return關鍵字,單行代碼的執行結果會自動返回;如果添加了大括號,或者有多行代碼,必須通過return關鍵字返回執行結果
變量捕獲-變量的訪問操作
所謂變量捕獲,就是表達式使用過程中,對於所屬作用域範圍內的變量訪問規則,我們通過和匿名內部類型的變量捕獲進行對比,突出lambda表達式對於語法語義的優化。
匿名內部類對變量的訪問
public class App2{
String s1="全局變量";
//1.匿名內部類型中對於變量的訪問
public void testInnerClass(){
String s2="局部變量";
//在匿名內部類中訪問全局變量、局部變量
new Thread(new Runnable(){
String s3="內部變量";
@Override
public void run(){
//訪問全局變量
//System.out.println(this.s1);//this關鍵字~表示是當前內部類型的對象
System.out.println(s1);
//訪問局部變量
System.out.println(s2);//局部變量的訪問,~不能對局部變量進行數據的修改[在變量推導時候會認爲局部變量是final]
//訪問內部變量
System.out.println(s3);
System.out.println(this.s3);
}
}).start();
}
public static void main(String [] args){
App2 app=new App2();
app.testInnerClass();
}
}
運行結果:
全局變量
局部變量
內部變量
內部變量
Process finished with exit code 0
lambda表達式對變量的訪問
public class App2{
String s1="全局變量";
//1.匿名內部類型中對於變量的訪問
public void testInnerClass(){
String s2="局部變量";
//在匿名內部類中訪問全局變量、局部變量
new Thread(new Runnable(){
String s3="內部變量";
@Override
public void run(){
//訪問全局變量
//System.out.println(this.s1);//this關鍵字~表示是當前內部類型的對象
System.out.println(s1);
//訪問局部變量
System.out.println(s2);//局部變量的訪問,~不能對局部變量進行數據的修改[在變量推導時候會認爲局部變量是final]
//訪問內部變量
System.out.println(s3);
System.out.println(this.s3);
}
}).start();
}
//2. lambda表達式變量捕獲
public void testLambda(){
String s2="局部變量lambda";
new Thread(() -> {
String s3="內部變量lambda";
//訪問全局變量
System.out.println(this.s1);//this關鍵字,表示的就是所屬方法所在類型的對象
//訪問局部變量
System.out.println(s2);//同樣不能進行數據的修改
//訪問內部變量
System.out.println(s3);
}).start();
}
public static void main(String [] args){
App2 app=new App2();
//app.testInnerClass();
app.testLambda();
}
}
運行結果:
全局變量
局部變量lambda
內部變量lambda
Process finished with exit code 0
總結
lambda表達式中變量操作,優化了匿名內部類型中this關鍵字,不再單獨建立對象作用域,表達式本身就是所屬類型對象的一部分,在語法語義上使用更加簡潔
Lambda表達式類型檢查
Lambda表達式是一種簡單的語法,是函數式接口的一種實現,對於語法相同的Lambda表達式jvm在運行的過程中,在底層通過解釋及重構進行類型的自動推導,表現在代碼中就是Lambda表達式的類型檢查。
對於類型檢查我們主要說明如下兩部分:
表達式類型檢查
public class App3{
public static void test(MyInterface<String,List> inter){
List<String> list=inner.strategy("hello",new ArrayList());
System.out.println(list);
}
public static void main(String [] args){
test((x,y) -> {
y.add(x);
return y;
});
}
}
@FunctionalInterface
interface MyInterface<T,R>{
R strategy(T t,R r);
}
上面的test()方法的參數,是lambda表達式,並沒有指明類型是MyInterface ,但是在底層構建的時候,會自動進行推導,因爲test函數需要一個MyInterface類型,所以當前的表達式解釋成了MyInterface類型。
(x,y)->{...} --> test(param) --> param==MyInterface --> lambda表達式-> MyInterface類型
這就是對於lambda表達式的類型檢查,MyInterface接口就是lambda表達式的目標類型(target typing)
參數類型檢查
還是上面示例,
(x,y)->{...} --> MyInterface.strategy(T t,R r)--> MyInterface<String,List> inter --> T==String R==List --> lambda--> (x,y)==strategy(T t, R r) --> x==T==String y==R==List
這就是lambda表達式的參數類型檢查
方法重載和Lambda表達式
方法重載對Lambda表達式有什麼影響呢?
public class App4{
interface Param1 {
void outInfo(String info);
}
interface Param2{
void outInfo(String info);
}
//定義重載的方法
public void lambdaMethod(Param1 param){
param.outInfo("hello param1 imooc!");
}
public void lambdaMethod(){
param.outInfo("hello param2 imooc");
}
public static void main(String [] args){
App4 app=new App4();
app.lambdaMethod(() -> {
});
}
}
上面代碼中app.lambdaMethod
會出現報錯,
這是因爲Lambda表達式存在類型檢查(自動推導lambda表達式的目標類型)。 調用lambdaMethod時候,該方法是重載方法,它會有兩種類型的參數Param1、Param2,並且這兩種參數都是屬於函數式接口,在調用方法時候,傳遞lambda表達式,引發自動推導,在推導時候出現了混淆,因爲目前來說,有兩個函數式接口param1 | param2,當出現混淆語法檢查就不會再通過了。
通過上面示例可以看出lambda表達式的類型自動推導在某些情況下限制了傳統語法結構中的部分操作,如果在重載的類型中參數都是函數式接口的情況下,請使用匿名內部類實現,替代lambda表達式。
Lambda表達式底層構建原理
public class APP{
public static void main(String args[]){
IMarkUp mu=(message) -> System.out.println(message);
mu.markUp("lambda!");
}
}
interface IMarkUp{
void markUp(String msg);
}
對上面代碼進行編譯後,會出現App.class 、IMarkUp.class兩個class文件,我們使用javap -p App.class
命令進行反編譯,得到
Compiled from "App.java"
public class App {
public App();
public static void main(java.lang.String [] );
private static void lambda$main$0(java.lang.String);
}
反編譯之後的代碼,除了有 一個構造方法、一個main方法外,還有一個靜態私有方法,名稱是lambda$main$0,這其實就是lambda表達式中的方法實現。也就是說在底層構建時候,生成了註釋中的東西
public class APP{
public static void main(String args[]){
IMarkUp mu=(message) -> System.out.println(message);
mu.markUp("lambda!");
}
/*
private static void lambda$main$0(java.lang.String message){
System.out.println(message);
}
*/
}
interface IMarkUp{
void markUp(String msg);
}
那註釋中的私有靜態方法是如何被調用的呢?我們對於App.java內部編譯的詳細過程進行輸出,命令
java -Djdk.internal.lambda.dumProxyClasses App
,執行後發現在底層還構建了一個App$$Lambda$1.class這樣一個文件,在該文件裏面到底做了什麼樣的操作呢?運行
javap -p App$$Lambda$1.class
,得到下面代碼
final class App$$Lambda$1 implements IMarkUp{
private App$$Lambda$1();
public void markUp(java.lang.String);
}
可以看到上面代碼是一個內部類型,對於了IMarkUp接口進行了實現,底層的代碼lambda編譯時候是下面這樣的
public class APP{
public static void main(String args[]){
IMarkUp mu=(message) -> System.out.println(message);
mu.markUp("lambda!");
// new App$$Lambda$1().markUp("lambda!");
}
/*
private static void lambda$main$0(java.lang.String message){
System.out.println(message);
}
*/
/*
final class App$$Lambda$1 implements IMarkUp{
private App$$Lambda$1(){
}
public void markUp(java.lang.String msg){
App.lambda$main$0(msg);
}
}
*/
}
interface IMarkUp{
void markUp(String msg);
}
到這裏可以看出來了,lambda表達式在編譯的時候,會生成私有的靜態方法,在這個方法中,做了一個方法的基本實現,在編譯的同時會針對lambda表達式的目標接口來生成一個內部類型,實現這個接口,在實現接口的方法中完成對靜態方法具體執行過程的調用,最後在main方法中的那兩行代碼變成了註釋中的一行。
上面就是lambda表達式在jvm底層的解析過程和它的運行原理。
Lambda表達式在集合中的運用
方法引用
本身就是對方法引用的簡化,是結合Lambda表達式的一種語法特性,包含靜態方法引用、實例方法引用、構造方法引用,看示例
/**
*<p>
* 1.靜態方法引用的使用
* 類型名稱.方法名稱() --> 類型名稱::方法名稱
* 2.實例方法引用的使用
* 創建類型對應的一個對象.方法名稱() --> 對象引用::實例方法名稱
*</p>
*/
public class Test{
public static void main(String [] args){
//存儲person對象的列表
List<Person> personList=new ArrayList<>();
personList.add(new Person("tom","男",16));
personList.add(new Person("jerry","女",15));
personList.add(new Person("shuke","男",30));
personList.add(new Person("beita","女",26));
personList.add(new Person("damu","男",32));
//1.匿名內部類實現方式來排序
Collections.sort(personList,new Comparator<Person>(){
@Override
public int copare(Person p1,Person p2) {
return p1.getAge()-p2.getAge();
}
});
System.out.println(personList);
//2. lambda表達式的實現方法來排序
Collections.sort(personList,(p1,p2) -> p1.getAge() - p2.getAge());
System.out.println(personList);
//3. 靜態方法引用,比2又簡化了
Collections.sort(personList,Person::compareByAge);
System.out.println(personList);
//4.實例方法的引用
PersonUtil pu =new PersonUtil();
Collections.sort(personList,pu::compareByName);
System.out.println("tom".hashCode);
System.out.println("jerry".hashCode);
System.out.println(personList);
//5.構造方法引用:綁定函數式接口
IPerson ip=Person::new;
Person person=ip.initPerson("jerry","男",22);
System.out.println(person);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person {
private String name; //姓名
private String gender; //性別
private int age; //年齡
public static int compareByAge(Person p1,Person p2) {
return p1.getAge() - p2.getAge();
}
}
class PersonUtil {
// 增加一個實例方法
public int compareByName(Person p1,Person p2){
return p1.getName().hashCode() - p2.getName().hashCode();
}
}
interface IPerson {
//抽象方法:通過指定類型的構造方法初始化對象數據
Person initPerson(String name,String gender,int age);
}
通過上述代碼我們可以查看到,方法引用的語法處理是一種新的語法定義,結合lambda表達式能給我們帶來非常大的便利,但是在操作的過程中,代碼的可讀性會有一定程度的下降,在項目中可以針對具體的功能需求結合lambda表達式在語法語義上進行優化,但是切記不能爲了使用簡易的方法語義引用而去修改我們的業務邏輯。
Stream概述
什麼是Stream
不是IO中的數據流Stream,也不是集合元素,也不是數據結構,不能存儲數據,是和數據算法及運算有關的,jdk8中Stream流的引入是針對多個數據、數組、容器、集合等存儲批量數據的容器進行聚合操作時的複雜和冗餘流程,而提出的一套新的API,可以集合Lambda表達式通過串行或者並行兩種不同的方式完成對批量數據的操作。
Steam的作用
通過示例我們再看看有什麼作用
public class Test2{
public static void main(String [] args){
//1. 添加測試數據:存儲多個賬號的列表
List<String> accounts = new ArrayList<>();
accounts.add("tom");
accounts.add("jerry");
accounts.add("beita");
accounts.add("shuke");
accounts.add("damu");
//1.1 業務要求:長度大於等於5的有效賬號
for(String account: accounts){
if(account.length() >= 5){
System.out.println("有效賬號:"+account);
}
}
//1.2 迭代方式進行操作
Iterator<String> it=accounts.iterator();
while(it.hasNext()){
String account=it.next();
if(account.length()>=5){
System.out.println("it有效賬號:"+account);
}
}
// 上面兩種方法,和業務處理有關係的就兩行代碼,其它的和當前業務沒有太直接的關係,jdk8結合stream對上面情況進行了什麼樣的簡化呢?
//1.3 stream 結合lambda表達式完成業務處理
List validAccounts=accounts.stream().filter(s -> s.length()>=5).collect(Collectors.toList());
System.out.println(validAccounts);
}
}
Steam API
/**
* 1. 聚合操作
* 在常規業務中,針對批量業務的操作,例如在電商項目中獲取指定用戶的年平均消費額、獲取指定店鋪最便宜的商品,獲取指定商家當月的有效訂單數等等,都是批量數據操作的範疇,也是jdk提供Stream要優化的操作內容
* 2.stream的處理流程
* 數據源
* 數據轉換
* 獲取結果
* 3.獲取stream對象
* 3.1.從集合或者數組中獲取
* Collection.stream(),如accounts.stream()
* Collection.parallelStream(),獲取支持併發處理的stream對象
* Arrays.stream(T t),從數組中獲取stream對象
* 3.2.BufferReader緩衝流獲取對象
* BufferReader.lines()-> stream()
* 3.3.靜態工廠
* java.util.stream.IntStream.range()...,直接獲取到stream對象
* java.nio.file.Files.walk()...,直接獲取到stream對象
* 3.4.自定構建
* java.util.Spliterator,獲取stream對象
* 3.5.更多的方式...
* Random.ints()
* Pattern.splitAsStream()...
* 4.中間操作API{intermediate}
* 操作結果是一個Stream對象,中間操作可以有一個或者多個連續的中間操作,需要注意的是,中間操作只記錄操作方式不做具體執行,直到結束操作發生時,才做數據的最終執行。
* 中間操作:就是業務邏輯處理
* 中間操作過程:無狀態:數據處理時,不受前置中間操作的影響
* map/filter/peek/parallel/sequential/unordered
* 有狀態:數據處理時,會受到前置中間操作的影響。
* distinct/sorted/limit/skip
* 5.結束操作/終結操作{Terminal}
* 需要注意:一個stream對象,只能有一個Termial操作,這個操作一旦發生,就會真實處理數據,生成對應的數據結果。
* 終結操作:非短路操作:當前的Stream對象必須處理完集合中所有數據,才能得到處理結果。
* forEach/forEachOrdered/toArray/reduce/collect/min/max/count/iterator
* 短路操作:當前的Stream對象在處理過程中,一旦滿足某個條件,就可以得到結束。
* anyMatch/allMatch/noneMatch/findAny等
* 別名:Short-circuiting
* 使用場景:無限大的Stream->返回一個有限大的Stream,爲了性能的考慮,我們認爲包含一個短路操作是有必要的
*/
public class Test3{
}
Stream操作集合中的數據
類型轉換:其它類型—創建/獲取—>Stream對象
public class Test1{
public static void main(String[] args){
//1. 批量數據 -> Stream對象
//多個數據轉換到stream對象
Stream stream =Stream.of("admin","tom","damu");
//數組
String [] strArrays=new String[]{"xueqi","biyao"};
Stream stream2 =Arrays.stream(strArrays);
//列表
List<String> list=new ArrayList<String>();
list.add("少林");
list.add("武當");
list.add("青城");
list.add("崆峒");
list.add("峨眉");
Stream stream3=list.stream();
//集合
Set<String> set=new HashSet<>();
set.add("少林羅漢拳");
set.add("武當長拳");
set.add("青城劍法");
Stream stream4=set.stream();
//Map
Map<String,Integer> map=new HashMap<>();
map.put("tom",1000);
map.put("jerry",1200);
map.put("shuke",1500);
Stream stream5=map.entrySet().stream();
//2. Stream對象對於基本數據類型的功能封裝
// int /long / double
IntStream.of(new int []{10,20,30}).forEach(System.out::println);
//上面一行代碼,stream底層將裝箱拆箱封裝在了中間操作中,在最終只要完成一次裝箱拆箱就可以了
IntStream.range(1,5).forEach(System.out::println);
IntStream.rangeClosed(1,5).forEach(System.out::println);
}
}
類型轉換:Stream對象——>其它類型
public class Test1{
public static void main(String[] args){
//3. Stream對象 --> 轉換得到指定的數據類型
//數組
Object [] objx=stream.toArray();
//通過方法引用,轉換爲指定的String類型數組
Object [] objx=stream.toArray(String []::new);
//字符串
String str=stream.collect(Collections.joining()).toString();
System.out.println(str);
//列表
List<String> listx=(List<String>)stream.collect(Collectors.toList());
//集合
Set<String> setx=(Set<String>)stream.collect(Collectors.toSet());
//Map
Map<String, String> mapx=(Map<String,String>)stream.collect(Collectors.toMap(x->x,y->"value:"+y));
}
}
Stream常見API操作
public class Test1{
public static void main(String[] args){
//4. Stream中常見的API操作
List<String> accountList=new ArrayList<>();
accountList.add("songjiang");
accountList.add("lujunyi");
accountList.add("linchong");
accountList.add("luzhishen");
accountList.add("likui");
accountList.add("wusong");
// map() 中間操作,map()方法接收一個Functional接口,也就是傳遞給它一個參數,當這個參數運算結束之後,它會返回我們指定的某一個數據。
accountList=accountList.stream().map(x->"樑上好漢:"+x).collect(Collectors.toList());
accountList.forEach(System.out::println);
//filter() 添加過濾條件,過濾符合條件的用戶
accountList=accountList.stream().filter(x->x.length()>5).collect(Collectors.toList());
accountList.forEach(System.out::println);
//forEach 增強型循環,跟for循環不一樣,支持方法引用的操作
accountList.forEach(x-> System.out.println("forEach->"+x));
//peek() 中間操作:迭代數據完成數據的依次處理過程
accountList =accountList.stream()
.peek(x->System.out.println("peek 1:"+x))
.peek(x->System.out.println("peek 2:"+x))
.forEach(System.out::println);
//上面看着是對集合進行了3次迭代,但是peek是中間操作,forEach是終結操作,因此實際發生的迭代次數只有一次
// Stream中對於數字運算的支持
List<Integer> intList=new ArrayList<>();
intList.add(20);
intList.add(19);
intList.add(7);
intList.add(8);
intList.add(86);
intList.add(11);
intList.add(3);
intList.add(20);
//skip(), 中間操作,有狀態,跳過部分數據
intList.stream().skip(3).forEach(System.out::println);
//limit(), 中間操作,有狀態,限制輸出數據量
intList.stream().skip(3).limit(2).forEach(System.out::println);
// distinct() 中間操作,有狀態,剔除重複數據
intList.stream().distinct().forEach(System.out::println);
//sorted 中間操作,有狀態,排序
//max() 獲取最大值
Optional optional= intList.stream.max((x,y)->x-y);
System.out.println(optional.get());
//min() 獲取最小值
//reduce() 合併處理數據
Optional optional2=intList.stream().reduce((sum,x)->sum+x);
System.out.println(optional2.get());
}
}
Lambda表達式和Stream性能問題
public class Test{
public static void main(String [] args){
Random random=new Random();
//1. 基本數據類型:整數
List<Integer> integerList=new ArrayList<Integer>();
for(int i=0;i<1000000;i++){
integerList.add(random.nextInt(Integer.MAX_VALUE));
}
//1.1 stream
testStream(integerList);
//1.2 parallelStream 並行stream
testParallelStream(integerList);
//1.3 普通for
testForloop(integerList);
//1.4 增強型for
testStrongForloop(integerList);
//1.5 迭代器
testIterator(integerList);
}
public static void testStream(List<Integer> list){
long start=System.currentTimeMillis();
Optional optional=list.stream().max(Integer::compare);
System.out.println(optional.get());
long end =System.currentTimeMillis();
System.out.println("testStream:"+(end-start)+"ms");
}
public static void testParallelStream(List<Integer> list){
long start=System.currentTimeMillis();
Optional optional=list.parallelStream().max(Integer::compare);
System.out.println(optional.get());
long end =System.currentTimeMillis();
System.out.println("testParallelStream:"+(end-start)+"ms");
}
public static void testForloop(List<Integer> list){
long start=System.currentTimeMillis();
int max=Integer.MIN_VALUE;
for(int i=0;i<list.size();i++){
int current=list.get(i);
if(current>max){
max=current;
}
}
System.out.println(max);
long end =System.currentTimeMillis();
System.out.println("testForloop:"+(end-start)+"ms");
}
public static void testStrongForloop(List<Integer> list){
long start=System.currentTimeMillis();
int max=Integer.MIN_VALUE;
for(Integer integer:list){
if(integer>max){
max=integer;
}
}
System.out.println(max);
long end =System.currentTimeMillis();
System.out.println("testStrongForloop:"+(end-start)+"ms");
}
public static void testIterator(List<Integer> list){
long start=System.currentTimeMillis();
Iterator<Integer> it=list.irerator();
int max=it.next();
while(it.hasNext()){
int current=it.next();
if(current >max){
max=current;
}
}
System.out.println(max);
long end =System.currentTimeMillis();
System.out.println("testIterator:"+(end-start)+"ms");
}
}
我們執行後看結果:
在性能測試時候,我們一般會執行多次避免單次執行造成的數據誤差,在處理的過程中,我們看到並行的處理方式169ms和外部迭代的方式已經比較接近了,但是串行的方式641ms在處理基本數據的過程中時間延時是比較長的。
public class Test{
public static void main(String [] args){
//2.複雜數據類型:對象
Random random=new Random();
List<Product> productList=new ArrayList<>();
for(int i=0;i<1000000;i++){
productList.add(new Product("pro"+i,i,random.nextInt(Integer.MAX_VALUE));
}
//2.1 stream
testProductStream(integerList);
//2.2 parallelStream 並行stream
testProductParallelStream(integerList);
//2.3 普通for
testProductForloop(integerList);
//2.4 增強型for
testProductStrongForloop(integerList);
//2.5 迭代器
testProductIterator(integerList);
}
public static void testProductStream(List<Integer> list){
long start=System.currentTimeMillis();
Optional optional=list.stream().max((p1,p2)->p1.hot-p2.hot);
System.out.println(optional.get());
long end =System.currentTimeMillis();
System.out.println("testProductStream:"+(end-start)+"ms");
}
public static void testProductParallelStream(List<Integer> list){
long start=System.currentTimeMillis();
Optional optional=list.parallelStream().max((p1,p2)->p1.hot-p2.hot);
System.out.println(optional.get());
long end =System.currentTimeMillis();
System.out.println("testProductParallelStream:"+(end-start)+"ms");
}
public static void testProductForloop(List<Integer> list){
long start=System.currentTimeMillis();
Product maxHot=list.get(0);
for(int i=0;i<list.size();i++){
Product current=list.get(i);
if(current.hot>maxHot.hot){
maxHot=current;
}
}
System.out.println(maxHot);
long end =System.currentTimeMillis();
System.out.println("testProductForloop:"+(end-start)+"ms");
}
public static void testProductStrongForloop(List<Integer> list){
long start=System.currentTimeMillis();
Product maxHot=list.get(0);
for(Product current:list){
if(current.hot>maxHot.hot){
maxHot=current;
}
}
System.out.println(maxHot);
long end =System.currentTimeMillis();
System.out.println("testProductStrongForloop:"+(end-start)+"ms");
}
public static void testProductIterator(List<Integer> list){
long start=System.currentTimeMillis();
Iterator<Product> it=list.irerator();
Product maxHot=it.next();
while(it.hasNext()){
Product current=it.next();
if(current.hot >maxHot.hot){
maxHot=current;
}
}
System.out.println(maxHot);
long end =System.currentTimeMillis();
System.out.println("testProductIterator:"+(end-start)+"ms");
}
}
class Product {
String name; //暱稱
Integer stock; //庫存
Integer hot; //熱度
public Product(String name,Integer stock,Integer hot){
this.name=name;
this.stock=stock;
this.hot=hot;
}
}
運行結果
在處理複雜對象的時候,並行的stream明顯已經優於普通的for循環,已經非常接近甚至超過外部迭代方式。
針對服務商用環境,社區人員進行了更加嚴格的測試,分別從基本類型構建的數組,字符串對象,以及複雜對象進行了測試,在多核cpu以及內存足夠的情況下,可以看到stream在處理複雜對象的時候,帶來的性能提升,同時隨着cpu核心數增加,並行stream的處理方式性能提升更加明顯,最終得到這樣一個結論:
對於簡單數據的迭代處理,可以直接通過外部迭代進行操作,如果在性能上有一定的要求,可以選擇使用並行Stream進行操作;對於複雜對象的操作,stream串行操作和普通的迭代已經相差無幾,甚至操作普通的迭代,完全可以通過簡潔的Stream語法來替代普通的迭代操作。同時如果說性能有很高的要求的話,直接選擇並行的Stream進行操作,並行Stream在多核環境下更加能發揮它的處理優勢。
線程安全問題
並行Stream操作過程中,參考底層運行原理,其實是將一個操作鏈中的每個部分拆分成了多個子任務,通過多個線程執行的過程。也就是將一個大任務拆分成了多個小任務,通過線程進行完善的過程,最終在collect()時將多個線程執行的小任務進行合併的過程。
那麼這個過程在串行Stream我們完全可以通過自定義多線程,程序中的邏輯代碼進行數據的同步,來完成數據訪問的控制。但是並行Stream引發的多線程,對於數據源是否存在線程安全的問題呢?我們通過代碼進行簡單的測試,
public class Test2{
public static void main(String [] args){
//整數列表
List<Integer> lists=new ArrayList<Integer>();
//增加數據
for(int i=0;i<100;i++){
lists.add(i);
}
//串行Stream
List<Integer> list2=new ArrayList<>();
lists.stream().forEach(x->list2.add(x));
System.out.println(lists.size());
System.out.println(list2.size());
//並行Stream
List<Integer> list3=new ArrayList<>();
lists.parallelStream().forEach(x-> list3.add(x));
System.out.println(list3.size() );
}
}
運行結果:
可以看到,原來的list中有1000條數據,list2在串行Stream操作的過程中,得到的數據也是1000個,但是list3在並行Stream操作的過程中出現了數據的丟失,這個數據其實就是在多線程訪問的情況下,因爲當前的並行Stream操作的集合並不是線程安全的,所以在處理的過程中多個線程訪問共享數據出現了衝突,最後得到的數據出現了丟失。相同的程序,如果我們多次運行,會有可能出現多個線程訪問同樣的數據,導致數據溢出。
它的線程安全問題如何來解決呢?官方文檔裏面已經進行了提示,
當前的並行的stream由於在操作的過程中,使用的collections本身就不是線程安全的,所以意味着在多線程操作的情況下,它有可能會因爲多個線程的處理過程導致一些數據不一致的錯誤,在處理的過程中,collections本身提供了一些線程同步塊,通過collections本身提供的線程同步塊,我們可以完成對於數據的同步處理。另外,我們需要注意的是,如果使用collections集合本身提供的線程同步塊的話,會引起線程競爭的問題,如果我們想避免線程競爭的問題,聚合操作和parallel stream結合一起能讓我們得到一個在非線程安全的情況下,對於數據的處理過程,但是前提要求是我們在操作的過程中對於非線程安全的集合中的數據不能進行修改。
除了上面官方提供的一部分,另外當我們在操作stream時,stream中的api也對我們的非線程安全做了一些規約,比如forEach
上面的說明,官方在實現forEach操作時候,爲了提升並行流的性能,並沒有考慮線程同步的問題,但是官方同時提供的其它類型的終端操作,比如reduce、collect操作
所以,當我們在操作並行流的時候,如果我們在這裏要操作非線程安全的集合時候,在操作過程中我們完全可以通過collect這樣的終端操作來完成並行流的處理過程
public class Test2{
public static void main(String [] args){
//整數列表
List<Integer> lists=new ArrayList<Integer>();
//增加數據
for(int i=0;i<100;i++){
lists.add(i);
}
//串行Stream
List<Integer> list2=new ArrayList<>();
lists.stream().forEach(x->list2.add(x));
System.out.println(lists.size());
System.out.println(list2.size());
//並行Stream
List<Integer> list3=new ArrayList<>();
lists.parallelStream().forEach(x-> list3.add(x));
System.out.println(list3.size() );
//
List<Integer> list4=lists.parallelStream().collect(Collectors.toList());
System.out.println(list4);
}
}
運行結果:
可以看到,當我們使用collect進行操作時,它的數據是保證還原的,和前面的並行流形成了鮮明的對比,可以看到結果完全符合預期的操作。之後線程安全的複雜的業務邏輯就會在這樣的簡單的代碼基礎上進行擴展就可以了。
最後,得到這樣一個結論:Stream並行的線程安全問題,在業務處理的過程中主要通過自定義編碼加線程鎖的方式,或者是StreamAPI中提供的線程安全的終端操作來執行過程。但是在更多的場景中,如果我們遇到多線程問題,會直接使用線程安全的集合來規範數據源。